The Form.* components bind Anvil2 input fields to a formstate field state, removing the boilerplate of wiring value, change, error, and warning props by hand. Each wrapper is a MobX observer, so it re-renders when its bound A2InputFieldState changes.
Installation
Form fields rely on formstate, mobx, and mobx-react as peer dependencies. Install them alongside the package:
npm install @servicetitan/anvil2-ext-common formstate mobx mobx-react
Usage
Create an A2InputFieldState, then pass it to the matching Form.* component. The wrapper reads value, reports validation error/warning, and writes changes back to the field state.
import { Form, A2InputFieldState } from "@servicetitan/anvil2-ext-common";
const name = new A2InputFieldState("");
name.validators((value) => !value && "Name is required");
function NameField() {
return <Form.TextField fieldState={name} label="Name" />;
}
The field state is the source of truth. Read name.value to get the current value, call name.validate() to run validation, and check name.hasError / name.error to inspect the result. See Field state for the full API.
Available wrappers
Each wrapper accepts a fieldState prop plus the underlying Anvil2 component’s props. The field state’s value type matches the field:
| Component | fieldState value type |
|---|
Form.DateFieldRange | { startDate, endDate } | null |
Form.DateFieldSingle | string | null |
Form.DateFieldYearless | YearlessDate | null |
Form.DateFieldYearlessRange | { startDate, endDate } |
Form.NumberField | number | null |
Form.Textarea | string |
Form.TextField | string |
Form.TimeField | string | null |
Selection fields
Form.Select, Form.MultiSelect, and Form.TreeSelect wrap the beta Anvil2 select components and ship from the package root alongside the rest of the namespace. Provide a loadOptions function and bind the selected option(s) to the field state:
import { Form, A2InputFieldState } from "@servicetitan/anvil2-ext-common";
import type { SelectFieldOption } from "@servicetitan/anvil2/beta";
const fruit = new A2InputFieldState<SelectFieldOption | null>(null);
const loadOptions = async (search: string) =>
[
{ id: 1, label: "Apple" },
{ id: 2, label: "Banana" },
].filter((option) => option.label.toLowerCase().includes(search.toLowerCase()));
function FruitSelect() {
return <Form.Select fieldState={fruit} label="Fruit" loadOptions={loadOptions} />;
}
Form.Select and Form.MultiSelect support every loader mode (eager, page-lazy, offset-lazy, and group-lazy) — pass the loadOptions function that matches your strategy.
Group fields
Checkbox and radio inputs are provided as the group wrappers Form.RadioGroup and Form.CheckboxGroup. These render an Anvil2 group with a FieldMessage, so validation errors and warnings surface as text. Pass an items array; the field state holds the selected id (Form.RadioGroup) or ids (Form.CheckboxGroup).
import {
Form,
SelectableOptionsArrayFieldState,
} from "@servicetitan/anvil2-ext-common";
const items = [
{ id: 1, label: "Email" },
{ id: 2, label: "SMS" },
];
const channels = new SelectableOptionsArrayFieldState<number, (typeof items)[number]>(
[],
);
channels.validators((value) => value.length === 0 && "Select at least one channel");
function ChannelGroup() {
return (
<Form.CheckboxGroup fieldState={channels} items={items} legend="Channels" />
);
}
Limitations
- Single checkbox and radio inputs have no standalone wrapper. Use
Form.CheckboxGroup or Form.RadioGroup — a bare Anvil2 Checkbox/Radio exposes a boolean error (styling only) with no message area, so the group is the form pattern that renders validation messages.
- The following Anvil2 input types have no
Form.* wrapper: SearchField, InputMask, Switch, ButtonToggle, SelectCard, Combobox, SegmentedControl, and RichTextEditor.
Last modified on June 12, 2026