Docs

Binding Data to Forms

Binding data to UI components in TypeScript forms.

Hilla provides a means of binding UI components in TypeScript form views.

The client-side Binder supports Java back-end endpoints for loading and saving the form data, and reuses the metadata from Java Bean validation annotations for client-side validation.

API Basics

The form binding API consists of three key concepts:

  • The field() directive to bind the field components in Lit form view templates

  • The generated TypeScript models for POJO classes used in endpoints, which are used as field references and provide the necessary metadata

  • The client-side Binder TypeScript class, which is responsible for keeping track of the form state, the default and current values, and validation of the data.

Note
See the Form Binding Reference for more details.

How to Bind Form Data

For example, let us consider a Java endpoint with methods for loading and saving a Person bean:

/**
 * A Hilla endpoint for the person-view.ts form view.
 */
@Endpoint
public class PersonEndpoint {
    /**
     * Loads a Person to edit into the view.
     * @return default form data
     */
    public Person loadPerson() {
        // ...
    }

    /**
     * Saves the edited Person from the view.
     * @param person form data to save
     */
    public void savePerson(Person person) {
        // ...
    }
}

To bind data to a form, follow these steps in your frontend/views/person/person-view.ts client-side LitElement view:

  1. Import the Binder class and the field() template directive from the @hilla/form package. Import your PersonEndpoint data endpoint and the generated PersonModel from the frontend/generated folder:

    import { Binder, field } from '@hilla/form';
    
    import { PersonEndpoint } from 'Frontend/generated/PersonEndpoint';
    import PersonModel from 'Frontend/generated/com/example/application/PersonModel';
  2. Create a Binder instance for your view using the generated PersonModel:

    @customElement('person-form')
    class PersonForm extends LitElement {
      // ...
    
      private binder = new Binder(this, PersonModel);
    
      // ...
    }

    The PersonModel here is generated alongside a Person TypeScript data interface from the Person.java bean. This describes the structure of the data and the validation-related metadata for the form binding.

  3. Bind the UI components in the template using the ${field()} syntax:

    class PersonForm extends LitElement {
      // ...
    
      render() {
        return html`
          <vaadin-text-field
            label="Full name"
            ${field(this.binder.model.fullName)}
          ></vaadin-text-field>
        `;
      }
    }

    In this example, this.binder.model is an instance of PersonModel.

    Note
    Models don’t contain any actual data. Use this.binder.value or this.binder.defaultValue to access the actual current or default value of the form respectively.

Form Binding in Dialogs

Although binding works as expected inside a Dialog, its content isn’t re-rendered when the validation status changes. This can be fixed by explicitly requesting a content update in the binder declaration:

Open in a
new tab
private binder = new Binder(this, NewsletterSubscriptionModel, {
  onChange: () => {
    // Does the update outside the dialog, remove it if not needed
    this.requestUpdate();
    // Does the update inside the dialog
    this.dialog?.requestContentUpdate();
  },
});