Binding Data to Custom Components

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. creating 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. creating a custom binder subclass that associates the new custom field strategy with the custom web component tag.

Example of a Web Component

Let us consider the following LitElement component for using in form binding. Note that it does not expose properties like 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 example below:

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

export class MyTextFieldStrategy implements FieldStrategy {
  constructor(public element: MyTextField) {}

  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 '@vaadin/form';
import type { AbstractModel, FieldStrategy, ModelConstructor } from '@vaadin/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);
  }

  getFieldStrategy(element: any): FieldStrategy {
    if (element.localName === 'my-text-field') {
      return new MyTextFieldStrategy(element);
    }
    return super.getFieldStrategy(element);
  }
}

After that you can 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 '@vaadin/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>
    `;
  }
  //...
}