October 26, 2023
  • All
  • Announcements
  • Stencil
  • stencil

Announcing Support for Form-Associated Custom Elements in Stencil v4.5.0

Stencil Team

We’re happy to announce that as of version 4.5.0, Stencil now has support for building form-associated custom elements! 🎉 This new feature allows you to build rich, new user experiences leveraging form-related functionality that’s already built-in to the browser, like validation, accessibility, and more.

What are form-associated custom elements?

The browser comes out-of-the-box with several form controls that you can use to declaratively build out a form without having to manually wire anything up. You just nest your <input>, <select>, and <textarea> elements inside a <form> element and the values from those inputs will be automatically bundled up together. These inputs can also report whether they are in a valid or invalid state, they can be labeled for accessibility, and more.

With form-associated custom elements and the ElementInternals interface, these built-in browser features are opened up to web component developers, who can use them to build custom elements which participate in form elements in a rich way, just like the inputs already shipped by the browser.

If you want to learn more about form-associated custom elements, check out this blog post from the WebKit team, this article from web.dev, or read through the WHATWG specification.

Show me an example!

To bring support for form-associated custom elements to Stencil we’re adding two things: a new option, formAssociated, for the @Component decorator, and a new decorator, @AttachInternals, which will give you access to the ElementInternals for your component.

A Stencil component using this API to implement a custom text input could look

like this:

import { AttachInternals, Component, h, 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, and the ElementInternals object found at this.internals will have methods on it for interacting with that form and accessing key information.

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 on the element and use it when setting the value, so the value typed by a user into the input will be added to the form under the "my-custom-input" name.

With the above example, you could demonstrate this by printing the form data like so:

const form = document.querySelector("form");
const formData = new FormData(form);

console.log("~~ current form state ~~")
for (let entry of formData.entries()) {
  console.log(`${entry[0]}: ${entry[1]}`);
}

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.

This feature brings a lot of functionality to Stencil components that interact with forms and we can’t wait to see what you build with it!


Stencil Team