Skip to main content
A2InputFieldState<T> extends formstate’s FieldState<T> and adds non-blocking warnings, dynamic disablers, soft validation, and value-confirmation helpers. It is the field state every Form.* wrapper binds to.

Creating a field state

Pass an initial value to the constructor, then attach validators:
import { A2InputFieldState } from "@servicetitan/anvil2-ext-common";

const email = new A2InputFieldState("");
email.validators((value) => !value && "Email is required");

Inherited from formstate

A2InputFieldState keeps the full FieldState surface:
MemberDescription
$The validated value.
dirtytrue once the value changes from its initial value.
errorThe current error message, or undefined.
hasErrortrue when the last validation found an error.
onChangeSets a new value (no-op while the field is disabled).
reset(value?)Restores the initial value (or value) and clears the warning.
validate()Runs validators and resolves with the result.
validators()Registers the field’s validators.
valueThe current value bound to the input.

Added behavior

A2InputFieldState adds the following on top of FieldState:
MemberDescription
addValidator(key, validator)Adds a validator once per key, guarding against duplicate registration.
disabledtrue when a disabler matched the current value.
disablers(...validators)Registers disabler validators. A truthy result disables the field.
disableUpdateMode()Removes the updateMode sub-field.
enableUpdateMode(mode?)Creates the updateMode sub-field (defaults to UpdateMode.Keep).
hardConfirmValue()Marks the current value as the initial value and clears dirty.
hardSetValue(value)Sets the value, bypassing the disabled guard, and confirms it as the new initial value.
hasValueChanged()Returns true when the value differs from its initial value.
initValueThe field’s initial value, for comparing against the current value.
onChangeHandlerA @servicetitan/form-compatible handler: (event, { value }).
onChangeNativeHandlerA @servicetitan/form-compatible handler that reads event.currentTarget.value.
onReset(handler)Registers a handler that runs after reset(). Returns the field state for chaining.
seedValue(value)Silently sets a clean baseline value (no onChange/onDidChange); for hydrating from loaded data.
softValidate()Runs disablers and warnings without committing a full validation.
updateModeAn optional FieldState<UpdateMode> sub-field for value-update controls.
warningThe current non-blocking warning message, or undefined.
warnings(...validators)Registers warning validators. Each returns a message string or a falsy value.
Warnings and disablers are evaluated during validate() and softValidate():
const quantity = new A2InputFieldState<number | null>(null);

quantity.validators((value) => value == null && "Quantity is required");
quantity.warnings((value) => (value ?? 0) > 100 && "That is a large quantity");
quantity.disablers((value) => value === 0 && "Zero is not allowed");

await quantity.validate();
// quantity.hasError, quantity.warning, and quantity.disabled now reflect the value.

Selection field states

SelectableOptionsFieldState<T, G> (single) and SelectableOptionsArrayFieldState<T, G> (multiple) extend A2InputFieldState for option-based selection, such as Form.RadioGroup and Form.CheckboxGroup. Both hold an options list and validate the selected value against option-derived rules.
import { SelectableOptionsFieldState } from "@servicetitan/anvil2-ext-common";

type Option = { id: number; label: string; active: boolean };

const role = new SelectableOptionsFieldState<number, Option>(undefined as never, {
  validationRules: [
    { shouldTrack: (option) => !option.active, errorCode: "Role is inactive" },
  ],
});

role.setOptions([
  { id: 1, label: "Admin", active: true },
  { id: 2, label: "Legacy", active: false },
]);
Call setOptions whenever the available options change. When the selected value matches a tracked option, validate() reports the rule’s errorCode as the error.
Last modified on June 12, 2026