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:
- How to register a web component as a form element and attach "internals"
- How to report values to be included in form data
- 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:
-
Set a static property
formAssociated
on the web component class and set that property totrue
. -
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. -
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 callingsetFormValue
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