13 KiB
Form Controls
Every Shoelace component makes use of a shadow DOM to encapsulate markup, styles, and behavior. One caveat of this approach is that native <form>
elements do not recognize form controls located inside a shadow root.
Shoelace solves this problem by using the formdata
event, which is available in all modern browsers. This means, when a form is submitted, Shoelace form controls will automatically append their values to the FormData
object that's used to submit the form. In most cases, things will "just work." However, if you're using a form serialization library, it might need to be adapted to recognize Shoelace form controls.
?> If you're using an older browser that doesn't support the formdata
event, a lightweight polyfill will be automatically applied to ensure forms submit as expected.
A Note About Event Handling
Shoelace uses event listeners to intercept the form's formdata
and submit
events. This allows it to inject data and trigger validation as necessary. If you're also attaching an event listener to the form, you must attach it after Shoelace form controls are connected to the DOM, otherwise your logic will run before Shoelace has a chance to inject form data and validate form controls.
Form Serialization
Serialization is just a fancy word for collecting form data. If you're relying on standard form submissions, e.g. <form action="...">
, you can probably skip this section. However, most modern apps use the Fetch API or a library such as axios to submit forms using JavaScript.
The FormData
interface offers a standard way to serialize forms in the browser. You can create a FormData
object from any <form>
element like this.
const form = document.querySelector('form');
const data = new FormData(form);
// All form control data is available in a FormData object
However, some folks find FormData
tricky to work with or they need to pass a JSON payload to their server. To accommodate this, Shoelace offers a serialization utility that gathers form data and returns a simple JavaScript object instead.
import { serialize } from '@shoelace-style/shoelace/dist/utilities/form.js';
const form = document.querySelector('form');
const data = serialize(form);
// All form control data is available in a plain object
This results in an object with name/value pairs that map to each form control. If more than one form control shares the same name, the values will be passed as an array, e.g. { name: ['value1', 'value2'] }
.
Form Control Validation
Client-side validation can be enabled through the browser's Constraint Validation API for Shoelace form controls. You can activate it using attributes such as required
, pattern
, minlength
, and maxlength
. Shoelace implements many of the same attributes as native form controls, but check each form control's documentation for a list of all supported properties.
As the user interacts with a form control, its invalid
attribute will reflect its validity based on its current value and the constraints that have been defined. When a form control is invalid, the containing form will not be submitted. Instead, the browser will show the user a relevant error message. If you don't want to use client-side validation, you can suppress this behavior by adding novalidate
to the surrounding <form>
element.
All form controls support validation, but not all validation props are available for every component. Refer to a component's documentation to see which validation props it supports.
!> Client-side validation can be used to improve the UX of forms, but it is not a replacement for server-side validation. You should always validate and sanitize user input on the server!
Required Fields
To make a field required, use the required
prop. The form will not be submitted if a required form control is empty.
<form class="input-validation-required">
<sl-input name="name" label="Name" required></sl-input>
<br />
<sl-select label="Favorite Animal" clearable required>
<sl-menu-item value="birds">Birds</sl-menu-item>
<sl-menu-item value="cats">Cats</sl-menu-item>
<sl-menu-item value="dogs">Dogs</sl-menu-item>
<sl-menu-item value="other">Other</sl-menu-item>
</sl-select>
<br />
<sl-textarea name="comment" label="Comment" required></sl-textarea>
<br />
<sl-checkbox required>Check me before submitting</sl-checkbox>
<br /><br />
<sl-button type="reset" variant="default">Reset</sl-button>
<sl-button type="submit" variant="primary">Submit</sl-button>
</form>
<script type="module">
const form = document.querySelector('.input-validation-required');
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
</script>
import { SlButton, SlCheckbox, SlInput, SlMenuItem, SlSelect, SlTextarea } from '@shoelace-style/shoelace/dist/react';
const App = () => {
function handleSubmit(event) {
event.preventDefault();
alert('All fields are valid!');
}
return (
<form onSubmit={handleSubmit}>
<SlInput name="name" label="Name" required />
<br />
<SlSelect label="Favorite Animal" clearable required>
<SlMenuItem value="birds">Birds</SlMenuItem>
<SlMenuItem value="cats">Cats</SlMenuItem>
<SlMenuItem value="dogs">Dogs</SlMenuItem>
<SlMenuItem value="other">Other</SlMenuItem>
</SlSelect>
<br />
<SlTextarea name="comment" label="Comment" required></SlTextarea>
<br />
<SlCheckbox required>Check me before submitting</SlCheckbox>
<br />
<br />
<SlButton type="submit" variant="primary">
Submit
</SlButton>
</form>
);
};
Input Patterns
To restrict a value to a specific pattern, use the pattern
attribute. This example only allows the letters A-Z, so the form will not submit if a number or symbol is entered. This only works with <sl-input>
elements.
<form class="input-validation-pattern">
<sl-input name="letters" required label="Letters" pattern="[A-Za-z]+"></sl-input>
<br />
<sl-button type="reset" variant="default">Reset</sl-button>
<sl-button type="submit" variant="primary">Submit</sl-button>
</form>
<script type="module">
const form = document.querySelector('.input-validation-pattern');
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
</script>
import { SlButton, SlInput } from '@shoelace-style/shoelace/dist/react';
const App = () => {
function handleSubmit(event) {
event.preventDefault();
alert('All fields are valid!');
}
return (
<form onSubmit={handleSubmit}>
<SlInput name="letters" required label="Letters" pattern="[A-Za-z]+" />
<br />
<SlButton type="submit" variant="primary">
Submit
</SlButton>
</form>
);
};
Input Types
Some input types will automatically trigger constraints, such as email
and url
.
<form class="input-validation-type">
<sl-input variant="email" label="Email" placeholder="you@example.com" required></sl-input>
<br />
<sl-input variant="url" label="URL" placeholder="https://example.com/" required></sl-input>
<br />
<sl-button type="reset" variant="default">Reset</sl-button>
<sl-button type="submit" variant="primary">Submit</sl-button>
</form>
<script type="module">
const form = document.querySelector('.input-validation-type');
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
</script>
import { SlButton, SlInput } from '@shoelace-style/shoelace/dist/react';
const App = () => {
function handleSubmit(event) {
event.preventDefault();
alert('All fields are valid!');
}
return (
<form onSubmit={handleSubmit}>
<SlInput variant="email" label="Email" placeholder="you@example.com" required />
<br />
<SlInput variant="url" label="URL" placeholder="https://example.com/" required />
<br />
<SlButton type="submit" variant="primary">
Submit
</SlButton>
</form>
);
};
Custom Validation
To create a custom validation error, pass a non-empty string to the setCustomValidity()
method. This will override any existing validation constraints. The form will not be submitted when a custom validity is set and the browser will show a validation error when the containing form is submitted. To make the input valid again, call setCustomValidity()
again with an empty string.
<form class="input-validation-custom">
<sl-input label="Type 'shoelace'" required></sl-input>
<br />
<sl-button type="reset" variant="default">Reset</sl-button>
<sl-button type="submit" variant="primary">Submit</sl-button>
</form>
<script type="module">
const form = document.querySelector('.input-validation-custom');
const input = form.querySelector('sl-input');
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
input.addEventListener('sl-input', () => {
if (input.value === 'shoelace') {
input.setCustomValidity('');
} else {
input.setCustomValidity("Hey, you're supposed to type 'shoelace' before submitting this!");
}
});
</script>
import { useRef, useState } from 'react';
import { SlButton, SlInput } from '@shoelace-style/shoelace/dist/react';
const App = () => {
const input = useRef(null);
const [value, setValue] = useState('');
function handleInput(event) {
setValue(event.target.value);
if (event.target.value === 'shoelace') {
input.current.setCustomValidity('');
} else {
input.current.setCustomValidity("Hey, you're supposed to type 'shoelace' before submitting this!");
}
}
function handleSubmit(event) {
event.preventDefault();
alert('All fields are valid!');
}
return (
<form onSubmit={handleSubmit}>
<SlInput ref={input} label="Type 'shoelace'" required value={value} onSlInput={handleInput} />
<br />
<SlButton type="submit" variant="primary">
Submit
</SlButton>
</form>
);
};
?> Custom validation can be applied to any form control that supports the setCustomValidity()
method. It is not limited to inputs and textareas.
Custom Validation Styles
The invalid
attribute reflects the form control's validity, so you can style invalid fields using the [invalid]
selector. The example below demonstrates how you can give erroneous fields a different appearance. Type something other than "shoelace" to demonstrate this.
<sl-input class="custom-input" label="Type Something" required pattern="shoelace">
<small slot="help-text">Please enter "shoelace" to continue</small>
</sl-input>
<style>
.custom-input[invalid]:not([disabled])::part(label),
.custom-input[invalid]:not([disabled])::part(help-text) {
color: var(--sl-color-danger-600);
}
.custom-input[invalid]:not([disabled])::part(base) {
border-color: var(--sl-color-danger-500);
}
.custom-input[invalid]:focus-within::part(base) {
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-danger-500);
}
</style>
import { SlInput } from '@shoelace-style/shoelace/dist/react';
const css = `
.custom-input[invalid]:not([disabled])::part(label),
.custom-input[invalid]:not([disabled])::part(help-text) {
color: var(--sl-color-danger-600);
}
.custom-input[invalid]:not([disabled])::part(base) {
border-color: var(--sl-color-danger-500);
}
.custom-input[invalid]:focus-within::part(base) {
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-danger-500);
}
`;
const App = () => (
<>
<SlInput className="custom-input" required pattern="shoelace">
<small slot="help-text">Please enter "shoelace" to continue</small>
</SlInput>
<style>{css}</style>
</>
);
Third-party Validation
To opt out of the browser's built-in validation and use your own, add the novalidate
attribute to the form. This will ignore all constraints and prevent the browser from showing its own warnings when form controls are invalid.
Remember that the invalid
attribute on form controls reflects validity as defined by the Constraint Validation API. You can set it initially, but the invalid
attribute will update as the user interacts with the form control. As such, you should not rely on it to set invalid styles using a custom validation library.
Instead, toggle a class and target it in your stylesheet as shown below.
<form novalidate>
<sl-input class="invalid"></sl-input>
</form>
<style>
sl-input.invalid {
...;
}
</style>