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");
A2InputFieldState keeps the full FieldState surface:
| Member | Description |
|---|
$ | The validated value. |
dirty | true once the value changes from its initial value. |
error | The current error message, or undefined. |
hasError | true when the last validation found an error. |
onChange | Sets 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. |
value | The current value bound to the input. |
Added behavior
A2InputFieldState adds the following on top of FieldState:
| Member | Description |
|---|
addValidator(key, validator) | Adds a validator once per key, guarding against duplicate registration. |
disabled | true 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. |
initValue | The field’s initial value, for comparing against the current value. |
onChangeHandler | A @servicetitan/form-compatible handler: (event, { value }). |
onChangeNativeHandler | A @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. |
updateMode | An optional FieldState<UpdateMode> sub-field for value-update controls. |
warning | The 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