Hilla Documentation

Binding Data to Custom Components

How to create a form with a custom web component.

While Vaadin components are supported out of the box, the client-side Form binder can be configured to support any web component. This article explains how to create a form with a custom <my-text-field> web component.

Using a custom web component in a form requires two steps:

  1. create a field strategy that tells the form binder how to get and set the component value and other properties, such as required and errorMessage;

  2. create a custom binder subclass that associates the new custom field strategy with the custom web component tag.

Example of a Web Component

Consider the following LitElement component for use in form binding. It doesn’t expose properties such as required, invalid and errorMessage, which is why the default field strategy that works for all Vaadin components would not work here.

@customElement('my-text-field')
export class MyTextField extends LitElement {
  @property({ type: String }) label = '';
  @property({ type: String }) value = '';

  // custom properties that do not work with the default Binder
  @property({ type: Boolean }) mandatory = false;
  @property({ type: Boolean }) hasError = false;
  @property({ type: String }) error = '';

  //...
}

Defining a Field Strategy

The first step towards using a web component as a field is to define a field strategy.

You need to implement the FieldStrategy interface, as in the following example:

import type { FieldStrategy } from '@hilla/form';
import type { MyTextField } from './my-text-field';

export class MyTextFieldStrategy implements FieldStrategy {
  public element: MyTextField;

  public constructor(element: MyTextField) {
    this.element = element;
  }

  set required(required: boolean) {
    this.element.mandatory = required;
  }

  set invalid(invalid: boolean) {
    this.element.hasError = invalid;
  }

  set errorMessage(errorMessage: string) {
    this.element.error = errorMessage;
  }
  // ...
}

Using the Field Strategy

Subclass the Binder class and override the getFieldStrategy(element) method to use a custom field strategy for any my-text-field components it finds in a form:

import { Binder } from '@hilla/form';
import type { AbstractModel, FieldStrategy, ModelConstructor } from '@hilla/form';
import { MyTextFieldStrategy } from './my-text-field-strategy';

export class MyBinder<T, M extends AbstractModel<T>> extends Binder<T, M> {
  constructor(context: Element, model: ModelConstructor<T, M>) {
    super(context, model);
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  getFieldStrategy(element: any): FieldStrategy {
    if (element.localName === 'my-text-field') {
      return new MyTextFieldStrategy(element);
    }
    return super.getFieldStrategy(element);
  }
}

You can now use my-text-field components in a form, provided that you use the extended MyBinder class to handle data binding in that form.

import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { field } from '@hilla/form';
import './my-text-field';
import { MyBinder } from './my-binder';
import SamplePersonModel from 'Frontend/generated/com/vaadin/demo/fusion/forms/fieldstrategy/SamplePersonModel';

@customElement('person-form-view')
export class PersonFormViewElement extends LitElement {
  private binder = new MyBinder(this, SamplePersonModel);

  render() {
    return html`
      <h3>Personal information</h3>
      <vaadin-form-layout>
        <my-text-field
          label="First name"
          ...="${field(this.binder.model.firstName)}"
        ></my-text-field>
      </vaadin-form-layout>
    `;
  }
  //...
}