Documentation

Documentation versions (currently viewingVaadin 24)

Vaadin Forms: Data Binding & Validation

How to use Binder to bind a data object to input fields and validate input.

On the Create a Component part of this tutorial, you created the input fields and buttons that you need for editing contacts. On this part, you’ll bind those inputs to a Contact object to create a fully functional form with validation.

This part covers creating a Vaadin Binder, binding input fields, and field validation.

Use Vaadin Binder to Create a Form & Validate Input

A form is a collection of input fields that are connected to a data model, a Contact in this case. Forms validate user input and make it easy to get an object filled with input values from the UI.

Vaadin Binder binds UI fields to data object fields by name. For instance, it takes a UI field named firstName and maps it to the firstName field of the data object, and the lastName field to the lastName field, and so on. This is why the field names in Contact and ContactForm are the same. Vaadin uses the Binder class to build forms.

Note
Advanced Binder API

Binder also supports an advanced API where you can configure data conversions and additional validation rules. For this application, though, the simple API is sufficient.

Binder can use validation rules that are defined on the data object in the UI. This means you can run the same validations both in the browser and before saving to the database, without duplicating code.

Bean Validation Rules in Java

You can define data validation rules as Java Bean Validation annotations on the Java class. You can see all of the applied validation rules by inspecting Contact.java. The validations are placed above the field declarations like this:

@Email
@NotEmpty
private String email = "";

Create the Binder

Instantiate a Binder and use it to bind the input fields like this:

// Other fields omitted
BeanValidationBinder<Contact> binder = new BeanValidationBinder<>(Contact.class); // (1)

public ContactForm(List<Company> companies, List<Status> statuses) {
    addClassName("contact-form");
    binder.bindInstanceFields(this); // (2)
    // Rest of constructor omitted
}
  1. BeanValidationBinder is a Binder that’s aware of bean validation annotations. By passing it in the Contact.class, you define the type of object to which you’re binding.

  2. bindInstanceFields() matches fields in Contact and ContactForm based on their names.

With these two lines of code, you’ve prepared the UI fields to be connected to a contact, which is the next step.

Set the Contact

You’re ready now to create a setter for the contact. Unlike the companies and statuses, it can change over time as a user browses through the contacts.

To do this, add the following method in the ContactForm class:

public class ContactForm extends FormLayout {

    public void setContact(Contact contact) {
        binder.setBean(contact); // (1)
    }
}
  1. Calls binder.setBean() to bind the values from the contact to the UI fields. The method also adds value change listeners to update changes in the UI back to the domain object.

Set Up Component Events

Vaadin comes with an event-handling system for components. You’ve already used it to listen to value-change events from the filter Text Field in the main view. The form component should have a similar way of informing parent components of events.

A few events can be fired: SaveEvent; DeleteEvent; and CloseEvent. To define new events, add the following code at the end of the ContactForm class:

// Events
public static abstract class ContactFormEvent extends ComponentEvent<ContactForm> {
  private Contact contact;

  protected ContactFormEvent(ContactForm source, Contact contact) { // (1)
    super(source, false);
    this.contact = contact;
  }

  public Contact getContact() {
    return contact;
  }
}

public static class SaveEvent extends ContactFormEvent {
  SaveEvent(ContactForm source, Contact contact) {
    super(source, contact);
  }
}

public static class DeleteEvent extends ContactFormEvent {
  DeleteEvent(ContactForm source, Contact contact) {
    super(source, contact);
  }

}

public static class CloseEvent extends ContactFormEvent {
  CloseEvent(ContactForm source) {
    super(source, null);
  }
}

public Registration addDeleteListener(ComponentEventListener<DeleteEvent> listener) { // (2)
  return addListener(DeleteEvent.class, listener);
}

public Registration addSaveListener(ComponentEventListener<SaveEvent> listener) {
  return addListener(SaveEvent.class, listener);
}
public Registration addCloseListener(ComponentEventListener<CloseEvent> listener) {
  return addListener(CloseEvent.class, listener);
}
  1. ContactFormEvent is a common superclass for all of the events. It contains the contact that was edited or deleted.

  2. The add*Listener() methods that passes the well-typed event type to Vaadin’s event bus to register the custom event types. Select the com.vaadin import for Registration if IntelliJ asks.

Save, Delete, & Close Form

With the event types defined, you can now inform anyone using ContactForm of relevant events. To add save, delete, and close event listeners, add the following to the ContactForm class:

private Component createButtonsLayout() {
  save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
  delete.addThemeVariants(ButtonVariant.LUMO_ERROR);
  close.addThemeVariants(ButtonVariant.LUMO_TERTIARY);

  save.addClickShortcut(Key.ENTER);
  close.addClickShortcut(Key.ESCAPE);

  save.addClickListener(event -> validateAndSave()); // (1)
  delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); // (2)
  close.addClickListener(event -> fireEvent(new CloseEvent(this))); // (3)

  binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); // (4)
  return new HorizontalLayout(save, delete, close);
}

private void validateAndSave() {
  if(binder.isValid()) {
    fireEvent(new SaveEvent(this, binder.getBean())); // (5)
  }
}
  1. The save button calls the validateAndSave() method.

  2. The delete button triggers a delete event and passes the active contact.

  3. The cancel button fires a close event.

  4. Validates the form every time it changes. If it’s invalid, it disables the save button to avoid invalid submissions.

  5. Fire a save event, so the parent component can handle the action.

Now, build the project and verify that it compiles. There won’t be, though, any visible changes yet.

On the next part of this tutorial, you’ll connect the form to the list view to complete the first view.

migration assistance

Download free e-book.
The complete guide is also available in an easy-to-follow PDF format.

Open in a
new tab
export class RenderBanner extends HTMLElement {
  connectedCallback() {
    this.renderBanner();
  }

  renderBanner() {
    let bannerWrapper = document.getElementById('tocBanner');

    if (bannerWrapper) {
      return;
    }

    let tocEl = document.getElementById('toc');

    // Add an empty ToC div in case page doesn't have one.
    if (!tocEl) {
      const pageTitle = document.querySelector(
        'main > article > header[class^=PageHeader-module--pageHeader]'
      );
      tocEl = document.createElement('div');
      tocEl.classList.add('toc');

      pageTitle?.insertAdjacentElement('afterend', tocEl);
    }

    // Prepare banner container
    bannerWrapper = document.createElement('div');
    bannerWrapper.id = 'tocBanner';
    tocEl?.appendChild(bannerWrapper);

    // Banner elements
    const text = document.querySelector('.toc-banner-source-text')?.innerHTML;
    const link = document.querySelector('.toc-banner-source-link')?.textContent;

    const bannerHtml = `<div class='toc-banner'>
          <a href='${link}'>
            <div class="toc-banner--img"></div>
            <div class='toc-banner--content'>${text}</div>
          </a>
        </div>`;

    bannerWrapper.innerHTML = bannerHtml;

    // Add banner image
    const imgSource = document.querySelector('.toc-banner-source .image');
    const imgTarget = bannerWrapper.querySelector('.toc-banner--img');

    if (imgSource && imgTarget) {
      imgTarget.appendChild(imgSource);
    }
  }
}

D788B762-1531-4C0C-A207-BB01672A413F