Building Form-Associated Components in Stencil
As of v4.5.0, Stencil has 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 not universal1 and the Stencil team has no plans at present to support or incorporate any polyfills for the browser functionality. Before you ship form-associated Stencil components make sure that 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)}
/>
)
}
}
If this component is rendered within a <form>
element like so:
<form>
<custom-text-input name="my-custom-input"></custom-text-input>
</form>
then it will automatically be linked up to the surrounding form. The
ElementInternals
object found at this.internals
will have a bunch of
methods on it for interacting with that form and getting key information out of
it.
In our <custom-text-input>
example above we use the
setFormValue
method to set a value in the surrounding form. This will read the name
attribute off of the element and use it when setting the value, so the value
typed by a user into the input
will added to the form under the
"my-custom-input"
name.
This example just scratches the surface, and 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.
Resources
- WHATWG specification for form-associated custom elements
- ElementInternals and Form-Associated Custom Elements from the WebKit blog
- See https://caniuse.com/?search=attachInternals for up-to-date adoption estimates.↩