Form Association and Web Components – Today

Estimated Reading Time: 3 minutes

Recently I’ve joined a team that’s creating a UI library using web components. This is great – but it has its challenges. One of these challenges is form association behind Shadow DOM.

When we create a form, we expect the following from input elements:

  • Add the input element’s value to submission if it has a name property
  • Enable validation is set (required/pattern/min/max/length/custom)

We also expect buttons of type submit and reset to work out of the box and actually validate and submit and reset respectively.

As shown in various other articles (Arthur Evans on web.dev, Paul H. H. Hansen) – when the input field is hiding behind a shadow DOM (as in many web component based UI components libraries), the above features just wouldn’t work.

The examples in this article are based on a simple webpack project. The project setup is described in details in former articles. If you want to play with the code, simply clone the repo and checkout the form-association-demo branch.

A Native Form

Let’s look at an example of how it works without Shadow DOM.

Example 1: A simple form with two fields and a submit button. The two fields are required and the email field validates for a valid email address. The onsubmit is set to return false in order to prevent a page refresh on submit. When submitting the form, the form’s data shows in the Form Output section.

In Example 1, when we click the submit button, we see that the form invalidates and does not submit because one of the fields is required.

Filling all of the fields and clicking the submit button actually submits the form, as can be seen from the output of the submit event.

The code is very simple and can be seen here:

<style>
form div {
margin: 5px 0;
}
</style>
<form name="simple-form" id="simple-form">
<div>
<label for="name-input">Name: </label>
<input required id="name-input" name="name" />
</div>
<div>
<label for="email-input">Email: </label>
<input required type="email" id="email-input" name="email" />
</div>
<button type="submit">Submit</button>
</form>
<h2>Form Output</h2>
<p id="form-output"></p>
view raw demo.html hosted with ❤ by GitHub
import template from "./demo.html";
document.body.innerHTML = template;
(function () {
const output = document.getElementById("form-output");
const form = document.getElementById("simple-form");
form.onsubmit = () => false;
console.log("Ready");
form.addEventListener("submit", () => {
const formData = new FormData(form);
let outputHTML = "";
for (var pair of formData.entries()) {
outputHTML += `<div>${pair[0]}: ${pair[1]}</div>`;
}
output.innerHTML = outputHTML;
});
})();
view raw index.js hosted with ❤ by GitHub

You can view the whole branch here: https://github.com/YonatanKra/web-components-ui-elements/tree/simple-form

A Shadow Form

Our form is not so impressive UX-wise. Our awesome designers have decided we should go with a material look to our form.

Because our company has lots of apps, each built with a different technology (vue, angular, react, etc.), we are going to use the “use everywhere” technology: Web Components.

We have decided to pick a project with a very promising name: Material Web Components.

Adding the Button

Let’s start with something simple. We’d like our button to look better.

That’s pretty simple:

  1. yarn add @material/mwc-button
  2. In the index.js file we add import @material/mwc-button
  3. In the demo.html file we replace the button tag with mwc-button

Click here to view the git diff.

The result is this:

Example 2: The same form from Example 1 with the mwc-button. You can try to click the button – and even though its type is submit nothing happens.

Now our app is broken – the form doesn’t submit if we click the button. The same would apply to a reset button as well as to a button that is explicitly attached to a form (by adding form="simple-form" attribute to the button).

Supporting Button Types

In order to support form submission and reset, we’d need to change our web component a bit.

We will create a new component that extends the mwc-button and add the following:

  1. Handle button click according to the button’s type (submit/reset)
  2. Handle a form attribute to allow association to an external form

We will register a new mfa-button (mfa – material form associated) and replace mwc-button with mfa-button in the template file.

You can see it working in Example 3:

Example 3: Same simple form shown in Example 2, this time with the mfa-button. Note that validation works again as well as submission and reset.

Click here for the full commit diff

Here’s the extended button’s code:

import { Button as MWCButton } from '@material/mwc-button';
export class MFAButton extends MWCButton {
constructor() {
super();
this.addEventListener('click', this._handleClick);
}
_handleClick(event) {
const formId = this.getAttribute('form');
const form = formId ?
document.getElementById(formId) : this.closest('form');
if (form) {
switch(this.getAttribute('type')) {
case 'reset':
form.reset();
break;
default:
form.requestSubmit();
break;
}
}
}
}
window.customElements.define('mfa-button', MFAButton);
view raw mfa-button.js hosted with ❤ by GitHub

In the button’s constructor, we add a listener that handles a click and fires _handleClick.

The magic happen inside _handleClick: we get the form either by id or the closest form. If the form exists, we either reset it or submit it.

Note we use requestSubmit and not simple submit. This is in order to emulate a submit via a button and enact all the form’s goodies like validation.

Summary

In this article we saw that web components might not always play “out of the box” with the bigger apps.

In this case, we saw that a simple button stopped interacting with the hosting form. We solved it by implementing the form API and “manually” associating the button with the form.

Worth to note that’s a spec is on its way (for quite a while) to solve this. Since it isn’t here now and it does not even appear in caniuse.com, we had to find another solution for our custom form elements to be associated with a form.

In the next article I’ll explain how we can associate input elements with a form (hint: it could have been similar if not for Safari…).

In the meantime I recommend cloning the repository and checking out the form-association-demo branch. You could try to add a reset button or one of the mwc input elements like @material/text-field and see if you can make it work the same way.

Hope you learned something new 🙂

As usual, your comments are more than welcome!

Thanks a lot to Yonatan Doron from Hodash.dev for a thorough review!

Leave a Reply

Your email address will not be published. Required fields are marked *