Reacting to Form State Changes
- Example: Disable Form while Submission in Progress
- Example: List Validation Errors
- Example: Populate a Field Based on Another
You can display some parts of a form. differently — depending on the form state. You can, for example, disable the Submit button when a form has validation errors, or show an 'operation in progress' spinner while an async form submission is in progress.
export default function ProfileView() {
const form = useForm(EntityModel);
return (
<>
...
<Button disabled={form.invalid}>submit</Button>
</>
);
}
The following properties are available both for the form as a whole, and for each field, independently. They’re a part of the BinderNode
interface.
You can access form status properties for the whole form from the UseFormResult
instance returned from useForm
:
const form = useForm(EntityModel);
const invalid = form.invalid;
const dirty = form.dirty;
const errors = form.errors;
...
To access form status properties for individual fields, use the UseFormPartResult
instance returned from useFormPart
:
const form = useForm(EntityModel);
const nameField = useFormPart(form.model.name);
const invalid = nameField.invalid;
const dirty = nameField.dirty;
const errors = nameField.errors;
-
dirty
: true if the value of the form or field has been modified by the user. -
visited
: true if the form or field has been focused by the user. -
invalid
: true if the form or field has validation errors. -
required
: true if the form or field is required. -
errors
: a list of all validation errors for a form or field and its sub-fields. -
ownErrors
: a list of all the validation errors for a field without sub-fields or for the form, not specific to any field.
The following properties are available for the form as a whole, but not for individual fields. They’re a part of the Binder
interface. For example, you can access these properties via binder.validating
.
-
validating
: true if the form is performing some validation. -
submitting
: true if the form is submitting the data to a callback.
Example: Disable Form while Submission in Progress
If form submission could take a long time, it’s good to give users indication that something is happening. You may also want to prevent more form submissions until the first one is completed (e.g., in a payment form).
With the TypeScript Binder
API, this can be done using the submitting
property. In the following example, binder.submitting
is bound to the disabled
property of the Submit button to disable repeating form submissions. It’s also used as a condition to render an additional 'submitting' message.
export default function ProfileView() {
const { model, submit, field, invalid, submitting } = useForm(PersonModel, {
onSubmit: async (e) => {
await PersonEndpoint.sendEntity(e);
}
});
return (
<>
<VerticalLayout theme="spacing padding">
<TextField label="First name" {...field(model.firstName)} />
<TextField label="Last name" {...field(model.lastName)} />
</VerticalLayout>
<HorizontalLayout theme="spacing padding">
<Button theme="primary" onClick={submit} disabled={invalid || submitting}>Save</Button>
<span className="label" style={{visibility: submitting ? 'visible' : 'hidden' }}>submitting</span>
<div className="spinner" style={{visibility: submitting ? 'visible' : 'hidden' }}></div>
</HorizontalLayout>
</>
);
}
Example: List Validation Errors
Sometimes you may want to list all validation errors in one place. This is convenient especially in large forms, where it can be difficult to find the one field that failed the validation.
With the TypeScript Binder
API, this can be done using the errors
property. In the following example, the form template iterates over the form.errors
list and renders the full list under the form.
<dl>
{form.errors.map(error => (
<>
<dt>{error.property as string}</dt>
<dt>{error.message as string}</dt>
</>
))}
</dl>
Example: Populate a Field Based on Another
Sometimes you may want to populate a field based on the value of another field. For example, you may want to set the value of a city
select field based on the value of a country
select field.
In such cases, you can use the useEffect
Hook to react to changes in the value of the first field, and update the value of the second field, accordingly. The following simplified example shows how to do this:
const form = useForm(MyModel);
useEffect(() => {
// Do something here to change the value of the second
// field based on the first field value
}, [form.value.firstField]);
For a more complete example, open the following section.
Interdependent Fields Example
const form = useForm(CompanyOfficeModel, {
onSubmit: async (companyOffice) => {
await OfficeService.saveCompanyOffice(companyOffice);
},
});
const countries = useSignal<Country[]>([]);
const cities = useSignal<City[]>([]);
useEffect(() => {
OfficeService.loadCompanyOffice().then(form.read);
OfficeService.loadCountries().then((loaded) => {
countries.value = loaded;
});
}, []);
useEffect(() => {
if (form.value.country?.name) {
OfficeService.loadCities(form.value.country.name).then((loaded) => {
cities.value = loaded;
form.value.city = loaded[0];
});
}
}, [form.value.country]);
return (
<section>
<ComboBox
{...form.field(form.model.country)}
label={'Country'}
items={countries.value}
itemLabelPath={'name'}
itemValuePath={'name'}
/>
<ComboBox
{...form.field(form.model.city)}
label={'City'}
items={cities.value}
itemLabelPath={'name'}
itemValuePath={'name'}
/>
<Button onClick={form.submit}>Save</Button>
</section>
);