Docs

Documentation versions (currently viewingVaadin 24)

Fields & Binding

This guide introduces the fundamentals of building forms and binding fields in Hilla. It covers how to lay out form fields, choose the appropriate input components, and use the useForm hook to connect those components to your application’s data model. For more in-depth information about forms in Hilla, see the Hilla Reference Guide.

Tip
Part of a Series
This is part 1 of the Add a Form series. You should also read the Overview.

Laying Out the Fields

In Hilla, you build forms visually by adding input components to a layout component. The most common layout components for forms are Form Layout, Vertical Layout, and an ordinary <div> (with some CSS). The Form Layout component supports multiple columns, whereas Vertical Layout lays out the components in a single column.

Tip
Layout or <div>?
If you are used to working with Flow, you can continue to work with the familiar layout components in Hilla. If you have a background in React or some other client-side framework, you may feel more comfortable working with <div>.

The following example shows how to build a two-column project proposal form with various input components:

import { ComboBox, DatePicker, FormLayout, TextArea, TextField } from "@vaadin/react-components";
import ProposalType from "Frontend/generated/com/example/application/domain/ProposalType";

export default function ProposalForm() {
    return (
        <FormLayout>
            <TextField label="Title"/>
            <ComboBox label="Proposal Type" items={Object.values(ProposalType)}/> 1
            <TextArea label="Description" data-colspan="2"/> 2
            <DatePicker label="Start Date"/>
            <DatePicker label="End Date"/>
        </FormLayout>
    );
}
  1. ProposalType is an enum in this example.

  2. Since FormLayout has two columns by default, this makes the description field take up the full width of the layout.

The finished form would look like this:

A screenshot of a form

Form Layout is responsive — when space is limited, it automatically adjusts to a single-column layout, placing each input on its own row. It also includes additional features not covered in this example. For more information see the Form Layout documentation.

Common Field Components

Vaadin provides a set of built-in field components that you can use in your forms:

FDOs and Form Models

In Hilla, FDOs are fetched from and submitted to a browser-callable application service for further processing. FDOs are implemented in Java, typically as records, although JavaBeans are also possible.

Hilla automatically generates the necessary TypeScript interfaces and handles the serialization between Java and JSON. Hilla also generates a form model, which is a description of the FDO in TypeScript. This form model is then used by the Hilla binder.

Continuing with the earlier Project Proposal form example, you can create a record to represent the FDO:

public record Proposal(
    @Nullable Long proposalId,
    String title,
    ProposalType type,
    String description,
    LocalDate startDate,
    LocalDate endDate
) {
}

To get Hilla to find this FDO, you have to create a browser-callable service that either returns it or accepts it as a parameter:

@BrowserCallable
@AnonymousAllowed
public class ProposalService {

    public void save(Proposal proposal) {
        // Will be implemented later
    }
}

For more information about browser-callable services, see the Add a Service guide.

The useForm Hook

Hilla provides a useForm hook that binds fields to form model properties, ensuring that changes made in the form update the FDO. When you implement your form as its own component, you should initialize it outside the form and pass the result in as a prop.

For example, here is a form component that accepts the result of useForm as a required prop:

import { UseFormResult } from "@vaadin/hilla-react-form";
import ProposalModel from "Frontend/generated/com/example/application/tutorial/service/ProposalModel";
// (Other imports omitted for brevity)

export type ProposalFormProps = {
    form: UseFormResult<ProposalModel> 1
}

export default function ProposalForm({form}: ProposalFormProps) {
    return (
        <FormLayout>
            ...
        </FormLayout>
    );
}
  1. ProposalModel is a Hilla form model generated from the Proposal FDO.

A parent component that uses the form could look something like this:

import { useForm } from "@vaadin/hilla-react-form";
// (Other imports omitted for brevity)

export default function ProposalDrawer() {
    const form = useForm(ProposalModel);
    return (
        <section>
            <h2>Edit Proposal</h2>
            <ProposalForm form={form}/>
        </section>
    );
}

Binding Fields

To bind a field to a form model property, use the field method from the useForm hook. This method generates an object containing all necessary props (like value, event handlers, and validation state) for the field component. You then use React’s spread syntax ({…​}) to apply these props to the component.

When using the field() method, you need to specify which property from the form model you want to bind by passing it as an argument. These properties are available in the model object returned by the useForm hook.

Here is an example that binds each field in the project proposal form to the corresponding form model property:

// (Imports omitted for brevity)

export type ProposalFormProps = {
    form: UseFormResult<ProposalModel>
}

export default function ProposalForm({form}: ProposalFormProps) {
    return (
        <FormLayout>
            <TextField label="Title"
                       {...form.field(form.model.title)}/>
            <ComboBox label="Proposal Type"
                      items={Object.values(ProposalType)}
                      {...form.field(form.model.type)}/>
            <TextArea label="Description"
                      data-colspan="2"
                      {...form.field(form.model.description)}/>
            <DatePicker label="Start Date"
                        {...form.field(form.model.startDate)}/>
            <DatePicker label="End Date"
                        {...form.field(form.model.endDate)}/>
        </FormLayout>
    );
}

Accessing the FDO

To access the FDO itself, use the value variable returned by the useForm hook:

const form = useForm(ProposalModel);

useEffect(() => {
    // Do something whenever the user selects a different type.
}, [form.value.type]);

In the example above, value is an instance of type Proposal. Hilla makes sure the FDO and the form are always in sync.

Note
In Hilla, there is always an FDO. If you haven’t initialized your form with an existing FDO, Hilla creates a new, empty one.

Clearing the Form

To clear the form, the useForm hook provides a clear() method:

// (Imports omitted for brevity)

export default function ProposalDrawer() {
    const form = useForm(ProposalModel);
    return (
        <section>
            <h2>Edit Proposal</h2>
            <ProposalForm form={form}/>
            <Button onClick={form.clear}>Clear Form</Button>
        </section>
    );
}

Clearing the form also clears the FDO, including unbound properties.

Reading from an FDO

To populate a form with data from an existing FDO, the useForm hook provides a read() method:

// (Imports omitted for brevity)

export type ProposalDrawerProps = {
    proposal?: Proposal
}

export default function ProposalDrawer({proposal}: ProposalDrawerProps) {
    const form = useForm(ProposalModel);

    useEffect(() => {
        form.read(proposal);
    }, [proposal]);

    return (
        <section>
            <h2>Edit Proposal</h2>
            <ProposalForm form={form}/>
        </section>
    );
}

Reading an undefined or null value clears the form.