Building Form-Associated Components in Stencil
As of $VERSION Stencil has experimental support for form-associated custom elements. This allows Stencil components to participate in a rich way in HTML forms, integrating with native browser features for validation and accessibility while maintaining encapsulation and control over their styling and presentation.
Browser support for the APIs that this feature depends on is still relatively low (~84% as of this writing) and the Stencil team has no plans at present support or incorporate any polyfills for the browser functionality. Accordingly, this is a 'use at your own risk' feature: it is up to you as a developer to ensure the browsers you need to support have shipped the necessary APIs.
Creating a form-associated component
A form-associated Stencil component is one which sets the new formAssociated
option in the argument to the @Component
decorator to true
, like so:
import { Component } from '@stencil/core';
@Component({
tag: 'my-face',
formAssociated: true,
})
export class MyFACE {
}
This element will now be marked as a form-associated custom element via the
formAssociated
static property, but by itself this is not terribly useful.
In order to meaningfully interact with a <form>
element that is an ancestor
of our custom element we'll need to get access to an
ElementInternals
object corresponding to our element instance. Stencil provides a decorator,
@AttachInternals
, which does just this, allowing you to decorate a property on
your component and bind an ElementInternals
object to that property which you
can then use to interact with the surrounding form.
Under the hood the AttachInternals
decorator makes use of the very similarly
named
attachInternals
method on HTMLElement
to associate your Stencil component with an ancestor
<form>
element. During compilation, Stencil will generate code that calls
this method at an appropriate point in the component lifecycle for both
lazy and custom
elements builds.
A Stencil component using this API to implement a custom text input could look like this:
import { Component, h, AttachInternals, State } from '@stencil/core';
@Component({
tag: 'custom-text-input',
shadow: true,
formAssociated: true
})
export class CustomTextInput {
@State() value: string;
@AttachInternals() internals: ElementInternals;
handleChange(event) {
this.value = event.target.value;
this.internals.setFormValue(event.target.value);
}
componentWillLoad() {
this.internals.setFormValue("a default value");
}
render() {
return (
<input
type="text"
value={this.value}
onInput={(event) => this.handleChange(event)}
/>
)
}
}
This component doesn't do a whole lot as-is, but luckily a great deal more is
possible with the ElementInternals
API, including setting the element's
validity,
reading the validity state of the form, reading other form values, and more.