> Markdown version of [Reacting to Form State Changes](https://vaadin.com/docs/v25/hilla/guides/forms/form-status-tracking). Section index: [llms.txt](https://vaadin.com/docs/v25/hilla/llms.txt)

# Reacting to Form State Changes

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.

```tsx
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`](https://vaadin.com/docs/v25/hilla/guides/forms/reference.md#binder-node) interface.

You can access form status properties for the whole form from the `UseFormResult` instance returned from `useForm`:

```tsx
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`:

```tsx
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`](https://vaadin.com/docs/v25/hilla/guides/forms/reference.md#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.

## <a id="example-disable-form-while-submission-in-progress"></a>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.

```tsx
export default function ProfileView() {

  const { model, submit, field, invalid, submitting } = useForm(PersonModel, {
    onSubmit: async (e) => {
      await PersonService.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>
    </>
  );
}
```

[Image: Disable the form while submission is in progress]

## <a id="example-list-validation-errors"></a>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.

```tsx
<dl>
  {form.errors.map(error => (
      <>
        <dt>{error.property as string}</dt>
        <dt>{error.message as string}</dt>
      </>
  ))}
</dl>
```

[Image: Disable the form while submission is in progress]

## <a id="example-populate-a-field-based-on-another"></a>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:

```tsx
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

CompanyOfficeView\.tsx

`CompanyOfficeView.tsx`

```tsx
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>
);
```

`OfficeService.java`

`OfficeService.java`

```java
package com.vaadin.demo.fusion.forms.formchange;

import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.BrowserCallable;

import java.util.List;

@BrowserCallable
@AnonymousAllowed
public class OfficeService {

    private final List<Country> countries = List.of(
            new Country("Germany",
                    List.of(new City("Berlin"), new City("Hamburg"),
                            new City("Munich"))),
            new Country("France",
                    List.of(new City("Paris"), new City("Marseille"),
                            new City("Lyon"))),
            new Country("Spain",
                    List.of(new City("Madrid"), new City("Barcelona"),
                            new City("Valencia"))),
            new Country("Italy",
                    List.of(new City("Rome"), new City("Milan"),
                            new City("Naples"))),
            new Country("United Kingdom", List.of(new City("London"),
                    new City("Birmingham"), new City("Glasgow"))));

    private CompanyOffice companyOffice = new CompanyOffice(countries.get(0),
            countries.get(0).getCities().get(0));

    public CompanyOffice loadCompanyOffice() {
        return companyOffice;
    }

    public void saveCompanyOffice(CompanyOffice companyOffice) {
        this.companyOffice = companyOffice;
    }

    public List<Country> loadCountries() {
        return countries;
    }

    public List<City> loadCities(String countryName) {
        // Find the cities by country
        return countries.stream()
                .filter(country -> country.getName().equals(countryName))
                .findFirst().map(Country::getCities).orElseThrow(
                        () -> new IllegalArgumentException("No such country"));
    }

}
```

`CompanyOffice.java`

`CompanyOffice.java`

```java
package com.vaadin.demo.fusion.forms.formchange;

public class CompanyOffice {

    private Country country;
    private City city;

    public CompanyOffice() {
    }

    public CompanyOffice(Country country, City city) {
        this.country = country;
        this.city = city;
    }

    public Country getCountry() {
        return country;
    }

    public void setCountry(Country country) {
        this.country = country;
    }

    public City getCity() {
        return city;
    }

    public void setCity(City city) {
        this.city = city;
    }
}
```

`Country.java`

`Country.java`

```java
package com.vaadin.demo.fusion.forms.formchange;

import java.util.List;

public class Country {
    private String name;

    List<City> cities;

    public Country() {
    }

    public Country(String name, List<City> countries) {
        this.name = name;
        this.cities = countries;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<City> getCities() {
        return cities;
    }

    public void setCities(List<City> cities) {
        this.cities = cities;
    }
}
```

`City.java`

`City.java`

```java
package com.vaadin.demo.fusion.forms.formchange;

public class City {
    private String name;

    public City() {
    }

    public City(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
```
