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 |