On this page

Form

Creating a web component that behaves like a standard <input> element will require it to work with the <form> tag. We will want to be able to submit its value, set if it is required, validate its value, reset it and generally perform just like any other input element.

To do all this requires us to work with the ElementInternals interface. This gives us the methods we need to interact with forms.

Set Value

Just like other input elements, our web component needs to be able to tell the form what value it currently has. When you have a form and you submit it, all the values of the inputs inside will be put together and sent off to the server. This normally only works with input elements, but you can create your own web component that works in the same way.

<style>
  :host {
    display: inline-block;
    font-size: x-large;
    cursor: pointer;
    user-select: none;
  }
</style>
<div id="random">????</div>

This is the inner HTML of the web component. It is used to create and show a random number. Pressing the number resets the random number to something else. Let's see how it would be used.

<form action="set-value.html">
  <label for="normal">Normal</label>
  <input id="normal" type="text" name="a">
  <br>
  <br>
  <label>Random number</label>
  <form-set-value name="b"></form-set-value>
  <br>
  <br>
  <input type="submit">
</form>

Here we have a normal text input, our web component and a submit button. What we want to happen is, when the submit button is pressed, it will take the normal input and the random number values, put them together and send them off to the server. The form's action is to just re-open the same page but with search parameters added to the URL. Take a look at it in action, before we see how it is done.

Press the ???? text to have the web component create the first random number. Enter something into the normal text input. Then press the submit button. You will see the URL for the page has changed, with some a=[test]&b=[random number] values.

To get this to work we need to do two things. The first thing is to declare a static getter function for the property formAssociated and have it return true.

// State that this web component is form associated (acts like an input)
static get formAssociated() { return true; }  

The next thing to do is to create an ElementInternals interface object using the attachInternals function.

// Create attach internals object
this._internals = this.attachInternals();  

You will notice that we are keeping an internal private copy of the internals object. This will attach the web component instance to the closest parent form element. If we do no more than this, then the value linked to the web component input will not exist. We need to use the interface to state what the value of the web component input is.

// Click event
_clickEvent(event) {
  // Create random number
  const randomNumber = Math.floor(Math.random() * 100000000);

  // Set random element with number
  this._randomElement.innerText = randomNumber.toString();

  // Set the forms value for this web component
  this._internals.setFormValue(randomNumber);
}  

This is the click event. It creates a new random number and sets the element to show it. It also uses the internals' function setFormValue to state what the web component's input value is.

Each time you press the random number, it creates a new random number, shows it, and tells the form what the new value is. When you press the submit number this new random number will be used and sent to the server.

Invalid Value

You can validate the inputs in a form. When the submit button is pressed, any inputs that are invalid will be highlighted and a message shown, and the submit process will be stopped. You can add validation states and custom messages to your web component.

In order for the message to be shown, it needs something to focus on. This is normally an input element, but if you are not using one then you need to make one of your internal elements focusable.

<style>
  :host {
    display: inline-block;
    font-size: x-large;
    cursor: pointer;
    user-select: none;
  }
</style>
<div id="random" tabindex="0">????</div>

This is the internal HTML of the web component. It is similar to the last example we saw but we have added the tabindex="0" attribute. This makes the random number element focusable. Any of the following validation messages will be anchored to this element.

When the web component is first shown it has no random number set. We want to make it required. To do this, when the web component is connected to the DOM, we need to set the validation state be valueMissing.

connectedCallback() {
  // Get random element
  this._randomElement = this.shadowRoot.getElementById('random');
  ...

  // By default the value is missing
  this._internals.setValidity(
    {
      valueMissing: true
    },
    'Random value not yet set',
    this._randomElement);
  }
}

We need to get the random element first. After this we use the ElementInternals interact function setValidity to state that the value is missing, the error message to show and which element the error message be shown next to.

Now, we also want to make sure only even numbers are allowed. Take a look at now this is done within the click event.

_clickEvent(event) {
  // Create random number
  const randomNumber = Math.floor(Math.random() * 100000000);

  // Set random element with number
  this._randomElement.innerText = randomNumber.toString();

  // Set the forms value for this web component
  this._internals.setFormValue(randomNumber);

  // If random number is odd
  if (randomNumber % 2) {
    // Set error to be shown if form submitted
    this._internals.setValidity(
    {
      typeMismatch: true
    },
    'Random value is not even',
    this._randomElement);
  } else {
    // Value is valid so set nothing wrong
    this._internals.setValidity({});
  }
}  

There are a number of steps performed in the click event. We create a random number, show it, set the form value, and then we check to see if the random number is odd or even. If the number is odd then we use the same setValidity function to state that the number is a mismatch and also set its error message. If the random number is even then we need to clear the error, just in case it was set last time.

The error messages will not be shown until the submit button is pressed. Take a look at it up and running.

The normal input has been set as required. Pressing the submit button will show the missing error message shown there first. Enter some text in the normal input then press submit, and you will see the new missing value error message.

<style>
  form-invalid-value:invalid {
    background-color: darkorange;
  }
</style>

There were some styles added, looking for the web component to be in the state of :invalid. Putting your web component into an invalid state will allow CSS to style it when the random number is missing or has an odd (not even) value. You cannot use the :required CSS selector with your web component sadly, but you maybe able to setup and use the [required] attribute CSS selector instead.

Restore Value

If you have a web page with a form of inputs, then submit the values and move on to another page, but then go back to previous page, you normally see the form and all the inputs with the same values as you entered before, they are no longer empty. The browser will try to restore the state of the form and its inputs to where they were before you left the page.

To add this type of input restore option to your web component, you need to use the formStateRestoreCallback function.

formStateRestoreCallback(state, mode) {
  // Set random element with number
  this._randomElement.innerText = state;

  // Set the forms value for this web component
  this._internals.setFormValue(state);
}  

This callback function is called by the browser when it wants to restore the web component to a previous state. It passes the state parameter that contains the value it had before and now wants to be restored to. Here we are setting the random number that is shown and setting the form's value. The type of the state parameter is always a string.

To see this in action in the example above, you need to enter some text and set a random number, then press the Submit button. You will be sent to the same page and the values will now be blank. Go back to the previous page. It is now that you will see how the previous values you saw before have been restored.

Reset Value

A form with different inputs can include a Reset button. This will clear all the input values, making them blank and ready for new data. You can allow this to happen to your web component too, using the formResetCallback function.

formResetCallback() {
  // Reset random element
  this._randomElement.innerText = '????';

  // Reset the forms value to nothing
  this._internals.setFormValue(undefined);
}  

This callback function is called by the browser when the reset button is pressed or the form's reset function is called. Here we are resetting the random number shown and setting the form value to nothing.

Enter a text value and set a random number, then press the Reset button to clear them both.

Disabled Input

A form's input can be disabled to stop the user from entering any text or interacting with them. You can disable your own web component by using the formDisabledCallback function.

<style>
  :host {
    display: inline-block;
    font-size: x-large;
    cursor: pointer;
    user-select: none;
  }
  #random[disabled] {
    color: lightgray;
    cursor: default;
  }
</style>
<div id="random">????</div>`;

This is web component's internal HTML. We are including a style that looks to see if the random element has the disabled attribute. We cannot use the CSS selector :disabled so we need to take a different approach.

formDisabledCallback(disabled) {
  // Set disabled value
  this._disabled = disabled;

  // If disabled
  if (disabled === true) {
    // Add disabled attribute
    this._randomElement.setAttribute('disabled','');
  } else {
    // Remove disabled attribute
    this._randomElement.removeAttribute('disabled');
  }
}  

This callback function is called by the browser to tell the web component that it is disabled or not. Here we set an internal private disabled variable and then either set or remove the disabled attribute from the random element. This will make the random number appear with a light gray color and stop the click button from changing the random number value.

Here we have two copies of the web component. One on its own and the other is inside a <fieldset> tag. You can disable and enable the first random number directly, by adding or removing the disabled attribute to the web component element. With the second random number, we are disabling and enabling the fieldset element, which will in turn disable and enable the random number web component inside it. You will see that all inputs inside are disable too.

Focus

Input elements can be given the focus. This happens when you select the input element but it can also happen when the user presses TAB on the keyboard. You can move from one input to the next using the TAB key, without needing to use the mouse or pressing the screen. Making your web component so that it can handle being given the focus does require some extra steps to be made. Before we go through them, let's take a look at an example.

In this example we have some standard text inputs (A & B), followed by two different web components, each with their own inputs, and ending with some other standard text inputs (C & D). Press the TAB key to see the cursor move from one input to the next. You can press SHIFT + TAB to move backwards.

The first thing we need to do is create the shadow DOM using an extra parameter called delegatesFocus with its value set to true. By default this is set to false.

class WithDelegateFocus extends HTMLElement {
  constructor() {
    // Attach shadow DOM root
    this.attachShadow({ mode: 'open', delegatesFocus: true });
    ...
  }
}
class WithoutDelegateFocus extends HTMLElement {
  constructor() {
    // Attach shadow DOM root
    this.attachShadow({ mode: 'open', delegatesFocus: false });
    ...
  }
}

Here we have both web components, each creating a shadow DOM, one with the delegatesFocus set to true and the other set to false. To see what difference this makes take another look at the above example. In the first web component, if you press the text "Shadow DOM with delegateFocus = true", it will give the input 1 the focus. But, in the second web component, if you press the text "Shadow DOM with delegateFocus = false", then no input is given the focus.

The delegatesFocus option sets up the web component so that if any part of it is clicked then it must give one of its internal elements the focus (but only if it can be focused). This can be useful if you have your own input web component and when the user selects some part of it, that the cursor would be given to some inner input element.

This sort of thing also happens if the web component has its focus function called. This will give the web component element the focus, but again, only the first one moves the focus to input 1. The second web component does nothing.

The next focus related step you need to make is related to the <label> element and its for attribute.

<label for="focus1">Label for below web component</label>
<with-delegate-focus id="focus1"></with-delegate-focus>  

The label's for attribute is used so that when the user clicks on the label text, it will give the focus to the element it is pointing to. In this case, the for is pointing to the focus1 element.

But to get this to work you need to declare a static getter function for the property formAssociated and have it return true.

// State that this web component is form associated (acts like an input)
static get formAssociated() { return true; }  

In the above example, pressing on any of the labels will move the focus to the element it is pointing to. This does not work with the second web component because it was created without setting the delegatesFocus value to true.