Web Components and You (part 8): Making a form element web component!

Published 2024-04-26, About 5 minute read.

One thing you may notice while building web components is that once you put a form input in the shadow DOM it doesn't get automatically recognized by the form it might be in. Also, it doesn't seem to enjoy the priviledges of form elements, like :disabled or :valid CSS psuedo selectors

Below is an example of what I'm talking about. We would expect that the value of the input would be in the formData, but it isn't! There's not even an entry with an empty value- the component is not even part of the form's consideration.

You'll see the form data appended to the DOM as JSON in this example, but there's nothing in the form data! Contrast this with a plain old text element example below. Here the containing form handles the form element's value and adds it to the form data of the form:

So can we have web components as form elements? Yes! and it's not too difficult to get them set up. In this article I want to discuss:

  1. How to register a web component as a form element and attach "internals"
  2. How to report values to be included in form data
  3. How to hook into custom validations with internals

Configuring a web component to participate in form stuffs

It's really easy to configure a web component to participate in forms. I think the actual way to do this is not documented all that well. Here are the three things you need to do to make a web component play nicely with a form and form data:

  1. Set a static property formAssociated on the web component class and set that property to true.
  2. Attach internals using this.attachInternals() in the constructor, saving the return value to a property on the class. In my examples I do this by instantiating a property directly, but this is just shorthand for doing it in the constructor.
  3. Lastly, and the most complicated, you must update the form value manually that you want stored in the form data. Here we are looking for an input event on the input element and then manually calling setFormValue off of our internals property.

You'll notice that the "key" that this value is saved under in the form data is whatever name the component was given with the name attribute.

But this may not be enough. At this point, the web component doesn't have a value property to set or get the value of the text input!

We should probably have the web component manage some state when the value changes in the text input. Here's one way to do this with Lit:

So why did we add the updated() method and update the form values there? If we were to update the form values only in the setValue() method, any form value changes that were done programatically wouldn't update the form! If someone queried this web component and set the value, the form wouldn't know. So we set up the form to update any time the component updates and value changes.

Focus management

Ideally, the form should interact with our web component as a black box. The form shouldn't have to reach into the web component to focus the right area. You want your text box to be selected if the web component is clicked. For example, in the following web component we don't have this. If we click the text that is part of the web component (not the label,) it doesn't focus on the pertinent input box.

Along with this issue, if we programatically try to focus the web component itself, it doesn't automatically focus on the the pertinent input box. Check it out by clicking the focus button below:

This is why delegatesFocus exists. Setting this to be true on a custom element allows automatic implementation of focus and blur methods for your web component. This is a shadow DOM property, so in Lit you need to set this using the shadowRootOptions static property:

Yay! Now the web component acts like a form element. The values are reported to the form data, calling focus() or blur() should work, and clicking on the component should correctly focus the first focusable element.

Validity

The component can now also participate in form states like "valid" or "invalid"! The easiest way to see this is to manually invalidate the component state, and then see that :invalid CSS is triggered:

If you take away the form associated flag on line 37, you'll notice that the component invalidation no longer triggers.

You might also notice that both the web component and the form have the style for :invalid. This is because the form validity is dependent on the validity of each of its registered components. If your web component is invalid, the form overall will be invalid too!

You can easily make only your component invalid by chaning the style:

Form related psuedo-selectors for CSS

We get a lot of other nice psuedo-selectors for free! This includes :disabled, :read-only, :required, etc!

Here's an example of a disabled web component with :disabled working. Note that we needed to wire up a disabled property on the web component to make the text input disabled internally:

We have first-class form web components!

Hopefully that fills in the blanks and will help when making form components.

What do you think? Is there something I missed? Am I just plain wrong? Add a comment below or find me on Bluesky or Mastadon. I also have an RSS feed here

⬅️ Previous Post Next Post ➡️
Back to top