Skip to main content
Version: Next

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.

caution

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.

info

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:

src/components/custom-text-input.tsx
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.

Resources