Data Binding, Forms & Validation
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 & 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:
-
Save
model
into a local variable for easier access (using object destructuring syntax). -
Bind each field to its corresponding model value using the
${field(model.property)}
syntax. Thefield()
directive binds all the required validations and value change listeners to the field. -
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 theitem-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 Editor when No Contacts 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.
