DocumentationAll ComponentsContact usChangelog

Form Integration

We are providing web components that encapsulate styling and additional behavior over standard HTML form controls, such as <input type="text"> or <input type="number">. Corresponding ZUi components would be <zui-textfield type="text"> and <zui-textfield type="number">.

Although they have a different visual experience, they are basically doing the same things as their HTML counterparts and thus they should behave as much as possible as those. Achieving this is not an easy task, as there are multiple concerns, that must be taken care of:

  • Form Submission
  • Form Reset
  • Disabled Controls
  • Form Validation
  • Form Styling
  • Form Participation

Form Submission

Form Submission revolves around that inherent ability of built-in form controls having a name and value attribute that, whenever a form is submitted, that its value get serialized into a data structure determined by enctype and thus possibly send to the server without any further JavaScript needed.

<form method="get" target="_self">
  <input type="text" name="text"/>
  <button type="submit">Submit!</button>  
</form>

Hitting the SubmitButton will issue a GET request, encoding the value of the <input type="text"> with name into a URL.

This is the same behavior for a <zui-example-form-control>:

<form method="get" target="_self">
  <zui-example-form-control name="text"></zui-example-form-control>
  <button type="submit">Submit!</button>  
</form>

N.B. Invoking Enter on a text-field, should trigger submission.

Form Reset

Form reset revolves around the concept, that whenever a <button type="reset"> or a <form> is reset(), that <input> will revert to either the value attribute, if set or to an empty value:

<form>
  <input type="text" name="text" value="initial"/>
  <input type="text" name="text-two"/>
  <button type="reset">Reset!</button>  
</form>

This is the same with <zui-example-form-control>

<form>
  <zui-example-form-control name="text" value="initial"></zui-example-form-control>
  <zui-example-form-control name="text-too"></zui-example-form-control>
  <button type="reset">Reset!</button>  
</form>

N.B. Our controls slightly differ in behavior, because value is always reflected, i.e. if you want to set an explicit resetValue you must do this by using the attribute reset-value:

<form>
  <zui-example-form-control name="text" value="initialValue" reset-value="resetValue"></zui-example-form-control>
  <button type="reset">Reset!</button>  
</form>

You will likely want to use this, if you want to reset it to empty, i.e. '' if you have supplied value initially:

<form>
  <zui-example-form-control name="text" value="initialValue" reset-value=""></zui-example-form-control>
  <button type="reset">Reset!</button>  
</form>

Disabled Form Controls

Disabled form controls, are ignoring submission.

<form method="get" target="_self">
  <input type="text" disabled name="text" value="hey"/>
  <input type="text" name="text-two"/>
  <button type="submit">Submit!</button>
</form>

This is the same with <zui-example-form-control>

<form method="get" target="_self">
  <zui-example-form-control disabled name="text" value="hey" reset-value=""></zui-example-form-control>
  <zui-example-form-control name="text-two"></zui-example-form-control>
  <button type="submit">Submit!</button>
</form>

Form Validation

Prevent submission for invalid form controls

There are several APIs available for FormValidation. The default API for form controls is the Constraint validation API, built-in form controls like <input> <select> and <textarea> support constraints via attributes (e.g. required) for setting basic constraints and report them accordingly.

The following example, won't allow submission, if the basic constraints (required, pattern) are not met.

<form>
  <label for="parent-submission-name">Enter username (upper and lowercase letters): </label>
  <input type="text" name="name" id="parent-submission-name" required pattern="[A-Za-z]+">
  <input type="text" name="lastname">
  <button type="submit">Submit!</button>
</form>

If a ZUi form control is providing constraints (via attributes) on its own, it should behave accordingly:

<form>
  <label>Enter username (upper and lowercase letters): </label>
  <zui-example-form-control name="name" required pattern="[A-Za-z]+"></zui-example-form-control>
  <zui-example-form-control name="lastname"></zui-example-form-control>
  <button type="submit">Submit!</button>
</form>

Imperative validation

Native HTML form controls will trigger the validation, if their associated form is either submitted or if checkValidity() is called imperatively on them. If checkValidity() returns false, an invalid event is dispatched at the element.

<script>
  setTimeout(()=> {
    document.querySelector('#imperative-validation-name').addEventListener('invalid', console.log);
  }, 100);
</script>
<form>
  <label for="imperative-validation-name">Enter username (upper and lowercase letters): </label>
  <input type="text" name="name" id="imperative-validation-name" required pattern="[A-Za-z]+">
  <button type="submit" >Submit</button>
  <button type="button" onclick="console.log(document.getElementById('imperative-validation-name').checkValidity())">Test</button>
</form>

ZUi form controls behave in the same way:

<script>
  setTimeout(()=> {
    document.querySelector('#zui-imperative-validation-name').addEventListener('invalid', console.log);
  }, 100);
</script>
<form>
  <label>Enter username (upper and lowercase letters): </label>
  <zui-example-form-control name="lastname" id="zui-imperative-validation-name" required></zui-example-form-control>
  <button type="submit" >Submit</button>
  <button type="button" onclick="console.log(document.getElementById('zui-imperative-validation-name').checkValidity())">Test</button>
</form>

Custom validation

For custom form controls, custom validation should be possible. The Form Participation API builts on top of the Constraint API, by providing a callback that gets called for form-associated elements, called validationCallback that must be overwritten to implement custom validation. Custom validation behaves exactly as basic constraints, i.e. checkValidity() returns validation status and triggers invalid events:

<script>
  setTimeout(()=> {
    document.querySelector('#zui-custom-validation-name').addEventListener('invalid', console.log);
    document.querySelector('#zui-custom-validation-name').validationCallback = function() {
      return this.value === 'custom';
    };
  }, 100);
</script>
<form>
  <label>Enter username (must be custom): </label>
  <zui-example-form-control name="lastname" id="zui-custom-validation-name"></zui-example-form-control>
  <button type="submit" >Submit</button>
  <button type="button" onclick="console.log(document.getElementById('zui-custom-validation-name').checkValidity())">Test</button>
</form>

Mixed Validation

Custom validation should be mixable with basic constraints, i.e. both should trigger an invalid event and support checkValidity():

<script>
  setTimeout(()=> {
    document.querySelector('#zui-mixed-validation-name').addEventListener('invalid', console.log);
    document.querySelector('#zui-mixed-validation-name').validationCallback = function() {
      return this.value === 'custom';
    };
  }, 100);
</script>
<form>
  <label>Enter username (must be custom): </label>
  <zui-example-form-control required name="lastname" id="zui-mixed-validation-name"></zui-example-form-control>
  <button type="submit" >Submit!</button>
  <button type="button" onclick="console.log(document.getElementById('zui-mixed-validation-name').checkValidity())">Test</button>
</form>

Asynchronous Validation

It is possible to set up asynchronous validation by utilizing setCustomValidity() method. For example if validation requires an API call. Just like in the native API you must also call the reportValidity() method on the same element to update validity.

<script>
    function validateOnServer() {
        const resultPromise = ... // some server call
        resultPromise.then((result) => {
            if (result.isValid) {
                document.querySelector('#zui-input').setCustomValidity('');
            } else {
                document.querySelector('#zui-input').setCustomValidity(result.message);
            }
            document.querySelector('#zui-input').reportValidity();
        })
    }
</script>

<form id="zui-form">
    <label>Enter username: </label>
    <zui-textfield id="zui-input" type="text"></zui-textfield>
    <button onclick="validateOnServer()">Validate Asynchronously</button>
</form>

ValidityState

// CustomValidity('Custom Text')
{
    "valid": false,
    "customError": true,
    "badInput": false,
    "rangeOverflow": false,
    "rangeUnderflow": false,
    "stepMismatch": false,
    "tooLong": false,
    "tooShort": false,
    "typeMismatch": false,
    "valueMissing": false,
    "patternMismatch": false
}
// CustomValidity('')
{
    "valid": true,
    "customError": false,
    "badInput": false,
    "rangeOverflow": false,
    "rangeUnderflow": false,
    "stepMismatch": false,
    "tooLong": false,
    "tooShort": false,
    "typeMismatch": false,
    "valueMissing": false,
    "patternMismatch": false
}

Validation State

Constraints are classified by the ValidityState interface. ZUi components match constraints to the equivalent ones of native form controls; e.g. a basic constraint required will set the valueMissing property to true, if the element fails this validation.

<script>
  setTimeout(()=> {
    document.querySelector('#zui-required-validity').addEventListener('invalid', ({target}) => console.log(target.validity);
    document.querySelector('#zui-custom-validation-name').validationCallback = function() {
      return this.value === 'custom';
    };
  }, 100);
</script>
<form>
  <label>Enter username: </label>
  <zui-example-form-control name="lastname" id="zui-required-validity" required></zui-example-form-control>
  <button type="submit" >Submit</button>
  <button type="button" onclick="console.log(document.getElementById('zui-required-validity').checkValidity())">Test</button>
</form>

Form Styling

Form Control State

Native HTML form controls are providing pseudo-classes :disabled, :invalid and :valid. Pseudo-classes cannot be implemented properly by Web Components, thus equivalent attributes (disabled, invalid and valid) are set on ZUi form controls. disabled is user-configurable and valid/ invalid are automatically set, depending on validation state. They can be used to style ZUi form controls, if they provide certain style hooks in form of CSS custom properties. Of course mixed validation, should work as well:

<style>
  zui-example-form-control[valid] {
    --zui-form-control-input-background-color: green
  }
  zui-example-form-control[invalid] {
    --zui-form-control-input-background-color: red
  }
</style>
<form>
  <label>Enter username: </label>
  <zui-example-form-control required name="username" id="zui-styled-validation-name"></zui-example-form-control>
  <button type="submit" >Submit!</button>
</form>

N.B. Built-in form controls can be initially invalid, if their basic constraints are violated. ZUi form controls are only validated after the first render, i.e. most likely after the value changes or if constraints are updated.

Validation Messages

Native form controls will show default error messages, if basic constraints are not met. This is built into the browser. ZUi form controls, either are validation-aware, i.e. they provide in-place validation messages and do actually show them or validation-ready, i.e. they set proper validityState but neither provide messages or actually show them.

The current validation message, can be retrieved by accessing validationMessage on both native as well as ZUi form controls.

Changing validation messages

Validation messages can be changed on runtime, by imperatively calling setValidityMessages() with the proper validityKey (e.g. valueMissing) on ZUi form controls:

<script>
  setTimeout(()=> {
    document.querySelector('#zui-custom-required-message').setValidityMessages({valueMissing: 'Username is required!'});
  }, 100);
</script>

<form>
  <label>Enter username: </label>
  <zui-example-form-control required name="username" id="zui-custom-required-message"></zui-example-form-control>
  <button type="submit" >Submit!</button>
  <button type="button" onclick="console.log(document.getElementById('zui-custom-required-message').validationMessage)">Test</button>
</form>

You will most likely want to use this, to localize validation messages for validation-aware components.

Form Participation

There is a new spec for better integration of Custom Elements into HTML form, called Form Participation API. Its current status is kind of unclear, because it does not seem to be a standard, nor does any other browser than Chrome implement it. It complements the Constraint API and allows for "native" integration of custom form controls and offers additional features, such as labelling support (see above link for more details).

The current implementation is exposing some of the proposed API methods by relying on existing support in browsers. Form Participation cannot be completely polyfilled; but if it becomes a later standard the existing API methods will be hopefully the same. Currently implemented is only the following callbacks:

  • formResetCallback()

Overview of form-enabled ZUi form controls

ZUi component Supported constraints native HTML counterpart validationAware Description
<zui-checkbox> required <input type="checkbox"> no Basic Checkbox, allows mixed state
<zui-toggle-switch> required <input type="checkbox"> no Styled Checkbox, doesn't allow mixed state
<zui-radio-button-group> required <input type="radio"> no Basic group of radio buttons
<zui-toggle-bar> required <input type="radio"> no A styled radio-button group
<zui-textfield input-type="number"> min, max, step, required <input type="number"> yes A basic input type number
<zui-textfield input-type="text"> maxlength, minlength, pattern, required <input type="text"> yes A basic input type text
<zui-textfield input-type="email"> maxlength, minlength, pattern, required <input type="email"> yes A basic input type email
<zui-slider> min, max, step <input type="range"> no A basic slider
<zui-slider-range> min, max, step <input type="range"> no A slider, with two handles
<zui-textfield-datepicker> min, max, required <input type="date"> yes A basic input type date
<zui-textfield-time-picker> min, max, required <input type="time"> yes A basic input type time
<zui-select> required, multiple <select> no A basic select component
<zui-textarea> maxlength, minlength, required <textarea> no A basic textarea
<zui-searchbar-input> maxlength, minlength, pattern, required <input type="search"> no A basic input type search
<zui-button type="submit|reset|button"> <input type="submit">, <input type="reset">, <button> no Button component to submit or reset a form