Hilla Documentation

Data Binding, Forms, and Validation

Learn how to bind input fields to a data model, validate input, and submit the value.

You now have a view that lists, filters, and selects contacts. In this chapter, you use the Hilla Binder to bind the selected contact to the form component and to validate form input.

This chapter covers:

  • creating a Hilla Binder,

  • binding input fields,

  • field validation,

  • using CSS to improve user experience.

Using Hilla Binder to Create a Form and Validate Input

Hilla uses Binder to bind form input fields to a model. Binder also performs input validation and form submission.

Defining Bean Validation Rules in Java

You can define data validation rules as Java Bean Validation annotations on the Java class. The benefit of defining the validations on the Java class is that Hilla runs them in the client form, and revalidates the received value on the server using the same rules.

You can see all the applied validation rules by inspecting Contact.java. The validations are placed above the field declarations as follows:

@Email
@NotEmpty
private String email = "";

Instantiating a Binder

Instantiate a new Binder in contact-form.ts:

import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { View } from 'Frontend/views/view';
import '@vaadin/button';
import '@vaadin/combo-box';
import '@vaadin/text-field';
import { Binder, field } from '@hilla/form';
import ContactModel from 'Frontend/generated/com/example/application/data/entity/ContactModel';
import { crmStore } from 'Frontend/stores/app-store';
import { listViewStore } from './list-view-store';

@customElement('contact-form')
export class ContactForm extends View {
  protected binder = new Binder(this, ContactModel);
  // render() omitted
}

Binder takes in two parameters: the element that contains the form, and a model descriptor. Hilla generates ContactModel based on the Java Contact class. It includes information on fields and validations.

Next, update the render() method to bind the input fields to the model using Binder.

render() {
  const { model } = this.binder;

  return html`
    <vaadin-text-field
      label="First name"
      ${field(model.firstName)}
    ></vaadin-text-field>
    <vaadin-text-field
      label="Last name"
      ${field(model.lastName)}
    ></vaadin-text-field>
    <vaadin-text-field
      label="Email"
      ${field(model.email)}
    ></vaadin-text-field>
    <vaadin-combo-box
      label="Status"
      ${field(model.status)}
      item-label-path="name"
      .items=${crmStore.statuses}
    ></vaadin-combo-box>
    <vaadin-combo-box
      label="Company"
      ${field(model.company)}
      item-label-path="name"
      .items=${crmStore.companies}
    ></vaadin-combo-box>

    <div class="flex gap-s">
      <vaadin-button theme="primary">Save</vaadin-button>
      <vaadin-button theme="error">Delete</vaadin-button>
      <vaadin-button theme="tertiary">Cancel</vaadin-button>
    </div>
  `;
}

The important changes to the method are:

  1. Save model into a local variable for easier access (using object destructuring syntax).

  2. Bind each field to its corresponding model value using the ${field(model.property)} syntax. The field() directive binds all the required validations and value change listeners to the field.

  3. The status and companies combo boxes are populated with items from the crmStore. Because statuses and companies are objects, you need to specify which property on the object should be used for the label using the item-label-path property.

Populating the Form With the Selected Contact

In the previous chapter, you added a listener on the grid that saves the selected contact in the list view store. You can use a MobX autorun to automatically update the binder with the selected contact.

constructor() {
  super();
  this.autorun(() => {
    if (listViewStore.selectedContact) {
      this.binder.read(listViewStore.selectedContact);
    } else {
      this.binder.clear();
    }
  });
}

Remember to call super() when overriding the constructor in a view to make sure the view component gets initialized.

Use this.autorun() instead of importing and using autorun() directly from MobX. To avoid memory leaks, the helper method on View takes care of disposing of the listener when you navigate away from the view.

autorun() takes a function as a parameter. The function runs immediately, and any time an observable value it depends on changes, in this case any time selectedContact changes.

Creating New Contacts

Add support for creating new contacts by adding two new actions to list-view-store.ts:

editNew() {
  this.selectedContact = ContactModel.createEmptyValue();
}

cancelEdit() {
  this.selectedContact = null;
}

To edit a new contact, use ContactModel to create an empty Contact and set it as the selected contact.

Bind the click event of the Add Contact button in list-view.ts to the editNew() action.

<vaadin-button @click=${listViewStore.editNew}>
  Add Contact
</vaadin-button>

Hiding the Editor When No Contacts Are Selected

Right now, the editor is constantly visible. You want to hide it while it’s not active. Add a boolean hidden attribute on the <contact-form> element in list view to hide it when no contacts are selected.

<contact-form
  class="flex flex-col gap-s"
  ?hidden=${!listViewStore.selectedContact}
></contact-form>

Maximizing the Form on Narrow Viewports

You can improve usability on narrow screens by hiding the grid and the toolbar while editing.

First, add an autorun() to the list view connectedCallback() to add an editing CSS class name to the element when there is a selected contact.

connectedCallback() {
  super.connectedCallback();
  // this.classList.add(...);
  this.autorun(() => {
    if (listViewStore.selectedContact) {
      this.classList.add("editing");
    } else {
      this.classList.remove("editing");
    }
  });
}

Then, add a CSS media query for narrow screens to styles.css.

[hidden] {
  display: none !important;
}

@media (max-width: 700px) {
  list-view.editing .toolbar,
  list-view.editing .grid {
    display: none;
  }

  list-view.editing contact-form {
    width: 100%;
  }
}

The rule hides the grid and toolbar when the editor is active if the viewport is 700px or narrower.

Update the Cancel button in the contact form to call the cancelEdit() action, so users have a way of exiting the editor.

<vaadin-button theme="tertiary" @click=${listViewStore.cancelEdit}>
  Cancel
</vaadin-button>

In your browser, try selecting different contacts to make sure the form is updated correctly. Verify that the responsive layout works by opening the application on your phone or by resizing your browser window.

Form open on a phone