vaadin / proposal-for-vaadin-form

Small and fast framework-agnostic library for creating forms with Web Components
Apache License 2.0
9 stars 0 forks source link

Vaadin Form: Proposal

We are planning to build a small and fast framework-agnostic library for creating forms with Web Components. Strong typing using TypeScript. Lots of examples to use good UX patterns.

code-sample

Vaadin Form goals

Do you need a form in your app? Not sure how to do it best? Vaadin Form docs explain what makes a good form UX, and how to implement it using the vaadin-form web component so that the code is clean, bug-free and easy to maintain.

Vaadin Form should:

Status

We are validating the idea. There is no implementation yet. Your feedback is welcome! Please create issues in this repo.

Overview and motivation

Forms are overwhelmingly common on the Web and even more so in line-of-business apps. Yet, it's surprisingly difficult to make forms with a good UX. And for complex forms, the DX often suffers as well.

Vaadin helps developers build web apps that users love. Excellent support for making forms has been a strong side of Vaadin, and we want that to remain as the front-end technology used in Vaadin evolves. When building UIs with Web Components on the client-side, developers would be looking to answer the same questions about making forms:

There are many good (and bad) examples out there for developers building apps with React, Angular and Vue. But there are not so many examples that are not tied to a framework. At Vaadin we see a lack of resources focusing on building forms with Web Components, e.g. with Vaadin or on the open-wc tech stack using libraries like lit-element or haunted. A form component is currently missing from the Vaadin components set, and there has been an open request for it for a while.

The Vaadin Form library would become such a resource: helping developers create forms that users love, out of standard Web Components.

Code samples

Some people say a code snippet is worth a thousand words.

The specific APIs used in the code snippets below are a very early sketch and may very well change in the final product. The snippets are here to illustrate the key concepts and use cases the library may support.

Listing features separately allows discussing them separately and eventually prioritizing between them. If you think that the feature would be useful to you, please +1 it (each feature has its own GitHub issue that allows you to add reacrions and comments). If the feature you want to see in the Vaadin Form library is missing from this list, please open a new issue in this repo.

API access: Custom Elements <vaadin-form> and <vaadin-form-field> (#3)

This is an HTML-first (template-driven) approach to the API. It is intended for use with a templating library like lit-html because such libraries simplify binding JavaScript handlers and values to DOM events and properties. However, there is no dependency on lit-html and it can as well be used with plain JavaScript or other frontend libraries.

import { html } from 'lit-html';

html`<vaadin-form @submit=${this.onSubmit}>
  <vaadin-form-field .validator=${this.validateUsername}>
    <label>User Name <input type="text" name="username"></label>
  </vaadin-form-field>

  <vaadin-form-field>
    <input type="submit" value="Submit">
  </vaadin-form-field>
</vaadin-form>`;

API access: JavaScript classes VaadinForm and VaadinFormField (#4)

This is a code-first approach to the API. It is intended for use with a statically typed language like TypeScript because in order for type-checker to work with forms they need to be declared in TypeScript, not in HTML. Depending on the implementation choices this approach may be combined with a templating library like lit-html.

Form and field properties: basic (#5)

A form instance detects user interaction with the form DOM and updates its state accordingly. It has a set of properties available through the API so that developers can create good user experiences with immediate response to user input and interactions.

Most properties are available both for the entire form and for individual fields. However, some properties make sence only for a form or for a field.

Form and field properties: fine details (#6)

In addition to the form and field properties lised in the basic set, there may be cases that require more fine-graned details of the form state.

Form properties: field lists for each flag (#7)

When a boolean flag like touched is set on a form instance, it may be useful to know which field(s) contribute to that. It can be done by iterating through all fields and checking the flag on each of them, but there could be a more conventient way. There are special properties listing the fields for each flag, already available on the form instance:

Form rendering

When a form instance detects user interactions with its linked DOM and updates its state, the form may need to be re-rendered (e.g. to show that a field has an invalid value). This section contains several alternative approaches to form rendering: library-independent, optimized for use with the lit-element library, and two optimised for use with the lit-html library (one that introduces custom extentions to it, and one that does not).

Form rendering: library-independent renderer() property (#8)

A renderer() functional property is a universal approach for reactive form rendering, independent from any library.

This approach is intended for use with the HTML-first API because a custom renderer property affecting the DOM children of a form works better with custom Web Components, than with standard DOM elements.

Form rendering: LitElement integration (#9)

This approach to reactive form rendering depends on the lit-element library. Vaadin Forms taps into the LitElement's change detection mechanism and calls the renderer() method on any form property change.

import { LitElement, customElement, html } from 'lit-element';
import { VaadinForm, form } from '@vaadin/form';

@customElement('field-validation')
class MyComponent extends LitElement {

  @form() form = new VaadinForm();

  render() {
    return html`
      <!-- well-known form inputs just work -->
      <div class="form-field">
        <label>User Name <input type="text" .value=${form.username}></label>
        ${form.username.touched && form.username.invalid
          ? html`<p class="error">${form.username.message}</p>`
          : ''}
      </div>

      <!-- arbitrary form inputs need explicit bindings between
          the DOM and the Vaadin Form's field instance -->
      <div class="form-field">
        <x-custom-form-field
          name="customProp"
          .value=${form.customProp.value}
          @input=${form.customProp.onInput}
          @change=${form.customProp.onChange}
          @blur=${form.customProp.onBlur}
          @focus=${form.customProp.onFocus}
        ></x-custom-form-field>
      </div>

      <button @click=${form.submit} ?disabled=${!form.canSubmit}>
        Submit
      </button>
    `;
  }
}

Form rendering: a custom lit-html directive (#10)

This approach to reactive form rendering depends on the lit-html library. Vaadin Form comes with a set of custom directives for lit-html that allow linking DOM elements to a form instance and using the form properties in the template.

import { VaadinForm } from '@vaadin/form';
import { html } from 'lit-html';

const form = new VaadinForm();

const template = html`
  ${form(form => html`
    <!-- well-known form inputs just work -->
    <div class="form-field">
      ${form.field(field => html`
        <label>User Name <input type="text" name="username"></label>
        ${field.touched && field.invalid
          ? html`<p class="error">${field.message}</p>` : ''}
      `)}
    </div>

    <!-- arbitrary form inputs need explicit bindings between
         the DOM and the Vaadin Form's field instance -->
    <div class="form-field">
      ${form.field(field => html`
        <x-custom-form-field
          name="customProp"
          .value=${field.value}
          @input=${field.onInput}
          @change=${field.onChange}
          @blur=${field.onBlur}
          @focus=${field.onFocus}
        ></x-custom-form-field>
      `)}
    </div>

    <button @click=${form.submit} ?disabled=${!form.canSubmit}>
      Submit
    </button>
  `)}
`;

Form rendering: 2-way binding extention to lit-html (#19)

This approach to reactive form rendering depends on the lit-html library. Vaadin Form comes with a custom form directive and an extention to the lit-html syntax to allow 2-way data binding.

import { VaadinForm } from '@vaadin/form';
import { html } from 'lit-html';

const form = new VaadinForm();

const template = html`
  ${form(form => html`
    <!-- well-known form inputs just work -->
    <div class="form-field">
      <label>User Name <input type="text" !value=${form.username}></label>
      ${form.username.touched && form.username.invalid
        ? html`<p class="error">${form.username.message}</p>`
        : ''}
    </div>

    <button @click=${form.submit} ?disabled=${!form.canSubmit}>
      Submit
    </button>
  `)}
`;

Form validation: a validator() function (#11)

form-validation

const form = new VaadinForm();
// init the form

const passwordRepeatedCorrectly = values => {
  if (values.password !== values.passwordRepeated) {
    return `Please check that you've repeated the password correctly.`;
  }
};

form.validator = [passwordRepeatedCorrectly];

Basic validators (#12)

The Vaadin Form library comes with a set of ready-to-use validator functions: min , max , required , notBlank , pattern , minLength , maxLength , numeric , etc

Async validators (#14)

const isAvailableName = async (value) => {
  const response = await fetch(`/validate/name/${encodeURIComponent(value)}`);
  const result = await response.json();
  if (result.error) {
    return `Please pick another name. '${value}' is not available.`;
  }
};

Limiting and debouncing validators (#13)

field-validation

import {VaadinForm, onBlur} from '@vaadin/vaadin-form';

const form = new VaadinForm();
// init the form

const isAvailableName = async (value) => {
  const response = await fetch(`/validate/name/${encodeURIComponent(value)}`);
  const result = await response.json();
  if (result.error) {
    return `Please pick another name. '${value}' is not available.`;
  }
};

form.username.validator = [onBlur(isAvailableName)];

Pluggable validators (#15)

Temporary disabling validation (#1)

Vaadin Form has a novalidate boolean property (false by default) that lets temporary disabling form validation (e.g. to save the intermediary form state even if it's invalid to be able to continue editing the form later)

Support for the formdata event: use VaadinFormField inside native <form>s (#16)

The Vaadin Form library helps creating form-assocciated custom elements by providing a VaadinFormFieldMixin (to be used with LitElement). With this mixin custom elements can participate in the native form validation and submission pipeline.

Type-safe forms (#17)

The VaadinForm and VaadinFormField API have TypeScript type definitions that inclcude a type parameter to define the types of the form fields. That allows build-time type checking of all form-handling code.

import {VaadinForm} from '@vaadin/vaadin-form';
import {Order} from './entities/order';

const form = new VaadinForm<Order>();
form.value = new Order(); // type-checked

// form properties are based on the form entity type
form.username.validator = [...];

Cross-field validation (#18)

Prior art

When working on this library the core team has studied the examples, API designs and best practices from a number of other libraries, including the form libraries widely used in React, Angular and Vue apps.

Out of scope

To be added later.

I want this page to clearly state which use cases are out of scope so that it is easy to point to this list when steering the discussions on this proposal.