kopia lustrzana https://github.com/shoelace-style/shoelace
support for <form> elements; remove <sl-form>
rodzic
938c7d2437
commit
33afecf7da
|
|
@ -4,6 +4,7 @@
|
|||
- [Usage](/getting-started/usage)
|
||||
- [Themes](/getting-started/themes)
|
||||
- [Customizing](/getting-started/customizing)
|
||||
- [Form Controls](/getting-started/form-controls)
|
||||
- [Localization](/getting-started/localization)
|
||||
|
||||
- Frameworks
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
### Opacity
|
||||
|
|
|
|||
|
|
@ -1,449 +0,0 @@
|
|||
# Form
|
||||
|
||||
[component-header:sl-form]
|
||||
|
||||
Forms collect data that can easily be processed and sent to a server.
|
||||
|
||||
All Shoelace components make use of a [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) to encapsulate markup, styles, and behavior. One caveat of this approach is that native `<form>` elements will not recognize Shoelace form controls.
|
||||
|
||||
This component solves that problem by serializing _both_ Shoelace form controls and native form controls when the form is submitted. The resulting form data is exposed in the `sl-submit` event as a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object in `event.detail.formData`. You can also find an array of form controls in `event.detail.formControls`.
|
||||
|
||||
Shoelace forms don't make use of `action` and `method` attributes and they don't submit the same way as native forms. To handle submission, you need to listen for the `sl-submit` event as shown in the example below and make an XHR request with the resulting form data.
|
||||
|
||||
```html preview
|
||||
<sl-form class="form-overview">
|
||||
<sl-input name="name" variant="text" label="Name"></sl-input>
|
||||
<br>
|
||||
<sl-select name="favorite" label="Select your favorite">
|
||||
<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-select>
|
||||
<br>
|
||||
<sl-checkbox name="agree" value="yes">
|
||||
I totally agree
|
||||
</sl-checkbox>
|
||||
<br><br>
|
||||
<sl-button submit>Submit</sl-button>
|
||||
</sl-form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.form-overview');
|
||||
|
||||
form.addEventListener('sl-submit', event => {
|
||||
const formData = event.detail.formData;
|
||||
let output = '';
|
||||
|
||||
// Post data to a server and wait for a JSON response
|
||||
fetch('https://jsonplaceholder.typicode.com/posts', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
console.log('Success:', result);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import {
|
||||
SlButton,
|
||||
SlCheckbox,
|
||||
SlForm,
|
||||
SlInput,
|
||||
SlMenuItem,
|
||||
SlSelect,
|
||||
} from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
function handleSubmit(event) {
|
||||
let output = '';
|
||||
|
||||
// Post data to a server and wait for a JSON response
|
||||
fetch('https://jsonplaceholder.typicode.com/posts', {
|
||||
method: 'POST',
|
||||
body: event.detail.formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
console.log('Success:', result);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
const App = () => (
|
||||
<SlForm onSlSubmit={handleSubmit}>
|
||||
<SlInput name="name" variant="text" label="Name" />
|
||||
<br />
|
||||
<SlSelect name="favorite" label="Select your favorite">
|
||||
<SlMenuItem value="birds">Birds</SlMenuItem>
|
||||
<SlMenuItem value="cats">Cats</SlMenuItem>
|
||||
<SlMenuItem value="dogs">Dogs</SlMenuItem>
|
||||
</SlSelect>
|
||||
<br />
|
||||
<SlCheckbox name="agree" value="yes">
|
||||
I totally agree
|
||||
</SlCheckbox>
|
||||
<br /><br />
|
||||
<SlButton submit>Submit</SlButton>
|
||||
</SlForm>
|
||||
);
|
||||
```
|
||||
|
||||
## Handling Submissions
|
||||
|
||||
### Using Form Data
|
||||
|
||||
On submit, a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object will be attached to `event.detail.formData`. You can use this along with [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/fetch) to pass data to the server.
|
||||
|
||||
```html preview
|
||||
<sl-form class="form-formdata">
|
||||
<sl-input name="name" variant="text" label="Name" required></sl-input>
|
||||
<sl-input name="age" variant="number" label="Age" required></sl-input>
|
||||
<br>
|
||||
<sl-button submit>Submit</sl-button>
|
||||
</sl-form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.form-formdata');
|
||||
|
||||
form.addEventListener('sl-submit', event => {
|
||||
fetch('https://jsonplaceholder.typicode.com/posts', {
|
||||
method: 'POST',
|
||||
body: event.detail.formData
|
||||
}).then(res => {
|
||||
console.log(res);
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import {
|
||||
SlButton,
|
||||
SlForm,
|
||||
SlInput
|
||||
} from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
function handleSubmit(event) {
|
||||
fetch('https://jsonplaceholder.typicode.com/posts', {
|
||||
method: 'POST',
|
||||
body: event.detail.formData
|
||||
}).then(res => {
|
||||
console.log(res);
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SlForm class="form-formdata" onSlSubmit={handleSubmit}>
|
||||
<SlInput name="name" variant="text" label="Name" required />
|
||||
<SlInput name="age" variant="number" label="Age" required />
|
||||
<br />
|
||||
<SlButton submit>Submit</SlButton>
|
||||
</SlForm>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Converting Form Data to JSON
|
||||
|
||||
It's sometimes useful to have form values in a plain object or a JSON string. You can convert the submitted `FormData` object to JSON by iterating and placing the name/value pairs in an object.
|
||||
|
||||
```js
|
||||
form.addEventListener('sl-submit', event => {
|
||||
const json = {};
|
||||
event.detail.formData.forEach((value, key) => (json[key] = value));
|
||||
|
||||
console.log(JSON.stringify(json));
|
||||
});
|
||||
```
|
||||
|
||||
## Form Control Validation
|
||||
|
||||
Client-side validation can be enabled through the browser's [Constraint Validation API](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation) for many form controls. You can enable it using props such as `required`, `pattern`, `minlength`, and `maxlength`. As the user interacts with the form control, the `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 `<sl-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.
|
||||
|
||||
```html preview
|
||||
<sl-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 variant="primary" submit>Submit</sl-button>
|
||||
</sl-form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.input-validation-required');
|
||||
form.addEventListener('sl-submit', () => alert('All fields are valid!'));
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import {
|
||||
SlButton,
|
||||
SlCheckbox,
|
||||
SlForm,
|
||||
SlInput,
|
||||
SlMenuItem,
|
||||
SlSelect,
|
||||
SlTextarea
|
||||
} from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlForm onSlSubmit={() => alert('All fields are valid!')}>
|
||||
<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 variant="primary" submit>Submit</SlButton>
|
||||
</SlForm>
|
||||
);
|
||||
```
|
||||
|
||||
### Input Patterns
|
||||
|
||||
To restrict a value to a specific [pattern](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/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.
|
||||
|
||||
```html preview
|
||||
<sl-form class="input-validation-pattern">
|
||||
<sl-input name="letters" required label="Letters" pattern="[A-Za-z]+"></sl-input>
|
||||
<br>
|
||||
<sl-button variant="primary" submit>Submit</sl-button>
|
||||
</sl-form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.input-validation-pattern');
|
||||
form.addEventListener('sl-submit', () => alert('All fields are valid!'));
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import {
|
||||
SlButton,
|
||||
SlForm,
|
||||
SlInput
|
||||
} from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlForm onSlSubmit={() => alert('All fields are valid!')}>
|
||||
<SlInput name="letters" required label="Letters" pattern="[A-Za-z]+" />
|
||||
<br />
|
||||
<SlButton variant="primary" submit>Submit</SlButton>
|
||||
</SlForm>
|
||||
);
|
||||
```
|
||||
|
||||
### Input Types
|
||||
|
||||
Some input types will automatically trigger constraints, such as `email` and `url`.
|
||||
|
||||
```html preview
|
||||
<sl-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 variant="primary" submit>Submit</sl-button>
|
||||
</sl-form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.input-validation-type');
|
||||
form.addEventListener('sl-submit', () => alert('All fields are valid!'));
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import {
|
||||
SlButton,
|
||||
SlForm,
|
||||
SlInput
|
||||
} from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlForm onSlSubmit={() => alert('All fields are valid!')}>
|
||||
<SlInput variant="email" label="Email" placeholder="you@example.com" required />
|
||||
<br />
|
||||
<SlInput variant="url" label="URL" placeholder="https://example.com/" required />
|
||||
<br />
|
||||
<SlButton variant="primary" submit>Submit</SlButton>
|
||||
</SlForm>
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Validation
|
||||
|
||||
To create a custom validation error, use the `setCustomValidity` method. The form will not be submitted when this method is called with anything other than an empty string, and its message will be shown by the browser as the validation error. To make the input valid again, call the method a second time with an empty string as the argument.
|
||||
|
||||
```html preview
|
||||
<sl-form class="input-validation-custom">
|
||||
<sl-input label="Type 'shoelace'" required></sl-input>
|
||||
<br>
|
||||
<sl-button variant="primary" submit>Submit</sl-button>
|
||||
</sl-form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.input-validation-custom');
|
||||
const input = form.querySelector('sl-input');
|
||||
|
||||
form.addEventListener('sl-submit', () => 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>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useRef, useState } from 'react';
|
||||
import {
|
||||
SlButton,
|
||||
SlForm,
|
||||
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!');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SlForm onSlSubmit={() => alert('All fields are valid!')}>
|
||||
<SlInput
|
||||
ref={input}
|
||||
label="Type 'shoelace'"
|
||||
required
|
||||
value={value}
|
||||
onSlInput={handleInput}
|
||||
/>
|
||||
<br />
|
||||
<SlButton variant="primary" submit>Submit</SlButton>
|
||||
</SlForm>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```html preview
|
||||
<sl-input class="custom-input" 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>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
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](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation). 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.
|
||||
|
||||
```html
|
||||
<sl-form novalidate>
|
||||
<sl-input class="invalid"></sl-input>
|
||||
</sl-form>
|
||||
|
||||
<style>
|
||||
sl-input.invalid {
|
||||
...
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
[component-metadata:sl-form]
|
||||
|
|
@ -16,9 +16,7 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
|
||||
?> Please refer to the section on [form control validation](/components/form?id=form-control-validation) to learn how to do client-side validation.
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
|
||||
?> Please refer to the section on [form control validation](/components/form?id=form-control-validation) to learn how to do client-side validation.
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,336 @@
|
|||
# Form Controls
|
||||
|
||||
Every Shoelace component makes use of a [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_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`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/formdata_event) event, which is [available in all modern browsers](https://caniuse.com/mdn-api_htmlformelement_formdata_event). 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.
|
||||
|
||||
## 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](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or a library such as [axios](https://github.com/axios/axios) to submit forms using JavaScript.
|
||||
|
||||
The [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) interface offers a standard way to serialize forms in the browser. You can create a `FormData` object from any `<form>` element like this.
|
||||
|
||||
```js
|
||||
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.
|
||||
|
||||
```js
|
||||
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](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation) 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.
|
||||
|
||||
```html preview
|
||||
<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="submit" variant="primary">Submit</sl-button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.input-validation-required');
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!')
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
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](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/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.
|
||||
|
||||
```html preview
|
||||
<form class="input-validation-pattern">
|
||||
<sl-input name="letters" required label="Letters" pattern="[A-Za-z]+"></sl-input>
|
||||
<br>
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.input-validation-pattern');
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!')
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
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`.
|
||||
|
||||
```html preview
|
||||
<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="submit" variant="primary">Submit</sl-button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.input-validation-type');
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!')
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
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, use the `setCustomValidity` method. The form will not be submitted when this method is called with anything other than an empty string, and its message will be shown by the browser as the validation error. To make the input valid again, call the method a second time with an empty string as the argument.
|
||||
|
||||
```html preview
|
||||
<form class="input-validation-custom">
|
||||
<sl-input label="Type 'shoelace'" required></sl-input>
|
||||
<br>
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
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>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
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 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.
|
||||
|
||||
```html preview
|
||||
<sl-input class="custom-input" 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>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
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](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation). 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.
|
||||
|
||||
```html
|
||||
<form novalidate>
|
||||
<sl-input class="invalid"></sl-input>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
sl-input.invalid {
|
||||
...
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
|
@ -8,6 +8,8 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
|||
|
||||
## Next
|
||||
|
||||
- 🚨 BREAKING: removed `<sl-form>` because all form components submit with `<form>` now ([learn more](/getting-started/form-controls))
|
||||
- 🚨 BREAKING: changed `submit` attribute to `type="submit"` on `<sl-button>`
|
||||
- 🚨 BREAKING: changed the `alt` attribute to `label` in `<sl-avatar>` for consistency with other components
|
||||
- Added `role="status"` to `<sl-spinner>`
|
||||
- Added `valueAsDate` and `valueAsNumber` properties to `<sl-input>` [#570](https://github.com/shoelace-style/shoelace/issues/570)
|
||||
|
|
|
|||
|
|
@ -251,10 +251,11 @@ This convention can be relaxed when the developer experience is greatly improved
|
|||
|
||||
### Form Controls
|
||||
|
||||
Form controls should support validation through the following conventions:
|
||||
Form controls should support submission and validation through the following conventions:
|
||||
|
||||
- All form controls must have an `invalid` property that reflects their validity
|
||||
- All form controls must use `name`, `value`, and `disabled` properties in the same manner as `HTMLInputElement`
|
||||
- All form controls must have a `setCustomValidity()` method so the user can set a custom validation message
|
||||
- All form controls must have a `reportValidity()` method that report their validity during form submission
|
||||
- All form controls must have an `invalid` property that reflects their validity
|
||||
- All form controls should mirror their native validation attributes such as `required`, `pattern`, `minlength`, `maxlength`, etc. when possible
|
||||
- All form controls must be serialized by `<sl-form>`
|
||||
- All form controls must be tested to work with the standard `<form>` element
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { html, literal } from 'lit/static-html.js';
|
|||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './button.styles';
|
||||
|
||||
|
|
@ -34,6 +35,7 @@ export default class SlButton extends LitElement {
|
|||
|
||||
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
|
||||
|
||||
private formSubmitController = new FormSubmitController(this);
|
||||
private hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||
|
||||
@state() private hasFocus = false;
|
||||
|
|
@ -63,8 +65,11 @@ export default class SlButton extends LitElement {
|
|||
/** Draws a circle button. */
|
||||
@property({ type: Boolean, reflect: true }) circle = false;
|
||||
|
||||
/** Indicates if activating the button should submit the form. Ignored when `href` is set. */
|
||||
@property({ type: Boolean, reflect: true }) submit = false;
|
||||
/**
|
||||
* The type of button. When the type is `submit`, the button will submit the surrounding form. Note that the default
|
||||
* value is `button` instead of `submit`, which is opposite of how native `<button>` elements behave.
|
||||
*/
|
||||
@property() type: 'button' | 'submit' = 'button';
|
||||
|
||||
/** An optional name for the button. Ignored when `href` is set. */
|
||||
@property() name: string;
|
||||
|
|
@ -110,6 +115,11 @@ export default class SlButton extends LitElement {
|
|||
if (this.disabled || this.loading) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.type === 'submit') {
|
||||
this.formSubmitController.submit();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +155,7 @@ export default class SlButton extends LitElement {
|
|||
'button--has-suffix': this.hasSlotController.test('suffix')
|
||||
})}
|
||||
?disabled=${ifDefined(isLink ? undefined : this.disabled)}
|
||||
type=${ifDefined(isLink ? undefined : this.submit ? 'submit' : 'button')}
|
||||
type=${this.type}
|
||||
name=${ifDefined(isLink ? undefined : this.name)}
|
||||
value=${ifDefined(isLink ? undefined : this.value)}
|
||||
href=${ifDefined(this.href)}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
|
|||
import { live } from 'lit/directives/live.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import styles from './checkbox.styles';
|
||||
|
||||
let id = 0;
|
||||
|
|
@ -31,6 +32,10 @@ export default class SlCheckbox extends LitElement {
|
|||
|
||||
@query('input[type="checkbox"]') input: HTMLInputElement;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this, {
|
||||
value: (control: SlCheckbox) => (control.checked ? control.value : undefined)
|
||||
});
|
||||
private inputId = `checkbox-${++id}`;
|
||||
private labelId = `checkbox-label-${id}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { styleMap } from 'lit/directives/style-map.js';
|
|||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { clamp } from '../../internal/math';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlInput from '../input/input';
|
||||
|
|
@ -57,14 +58,16 @@ const hasEyeDropper = 'EyeDropper' in window;
|
|||
@customElement('sl-color-picker')
|
||||
export default class SlColorPicker extends LitElement {
|
||||
static styles = styles;
|
||||
private localize = new LocalizeController(this);
|
||||
|
||||
@query('[part="input"]') input: SlInput;
|
||||
@query('[part="preview"]') previewButton: HTMLButtonElement;
|
||||
@query('.color-dropdown') dropdown: SlDropdown;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this);
|
||||
private isSafeValue = false;
|
||||
private lastValueEmitted: string;
|
||||
private localize = new LocalizeController(this);
|
||||
|
||||
@state() private inputValue = '';
|
||||
@state() private hue = 0;
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,283 +0,0 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import type SlButton from '../button/button';
|
||||
import type SlCheckbox from '../checkbox/checkbox';
|
||||
import type SlColorPicker from '../color-picker/color-picker';
|
||||
import type SlInput from '../input/input';
|
||||
import type SlRadio from '../radio/radio';
|
||||
import type SlRange from '../range/range';
|
||||
import type SlSelect from '../select/select';
|
||||
import type SlSwitch from '../switch/switch';
|
||||
import type SlTextarea from '../textarea/textarea';
|
||||
import styles from './form.styles';
|
||||
|
||||
interface FormControl {
|
||||
tag: string;
|
||||
serialize: (el: HTMLElement, formData: FormData) => void;
|
||||
click?: (event: MouseEvent) => any;
|
||||
keyDown?: (event: KeyboardEvent) => any;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status stable
|
||||
*
|
||||
* @slot - The form's content.
|
||||
*
|
||||
* @event {{ formData: FormData, formControls: [] }} sl-submit - Emitted when the form is submitted. This event will not
|
||||
* be emitted if any form control inside of it is in an invalid state, unless the form has the `novalidate` attribute.
|
||||
* Note that there is never a need to prevent this event, since it doesn't send a GET or POST request like native
|
||||
* forms. To "prevent" submission, use a conditional around the XHR request you use to submit the form's data with.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
@customElement('sl-form')
|
||||
export default class SlForm extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
@query('.form') form: HTMLElement;
|
||||
|
||||
private formControls: FormControl[];
|
||||
|
||||
/** Prevent the form from validating inputs before submitting. */
|
||||
@property({ type: Boolean, reflect: true }) novalidate = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.formControls = [
|
||||
{
|
||||
tag: 'button',
|
||||
serialize: (el: HTMLButtonElement, formData) =>
|
||||
el.name && !el.disabled ? formData.append(el.name, el.value) : null,
|
||||
click: event => {
|
||||
const target = event.target as HTMLButtonElement;
|
||||
if (target.type === 'submit') {
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'input',
|
||||
serialize: (el: HTMLInputElement, formData) => {
|
||||
if (!el.name || el.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.type === 'file') {
|
||||
[...(el.files as FileList)].map(file => formData.append(el.name, file));
|
||||
return;
|
||||
}
|
||||
|
||||
formData.append(el.name, el.value);
|
||||
},
|
||||
click: event => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (target.type === 'submit') {
|
||||
this.submit();
|
||||
}
|
||||
},
|
||||
keyDown: event => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (
|
||||
event.key === 'Enter' &&
|
||||
!event.defaultPrevented &&
|
||||
!['checkbox', 'file', 'radio'].includes(target.type)
|
||||
) {
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'select',
|
||||
serialize: (el: HTMLSelectElement, formData) => {
|
||||
if (el.name && !el.disabled) {
|
||||
if (el.multiple) {
|
||||
const selectedOptions = [...el.querySelectorAll('option:checked')];
|
||||
if (selectedOptions.length) {
|
||||
selectedOptions.map((option: HTMLOptionElement) => formData.append(el.name, option.value));
|
||||
} else {
|
||||
formData.append(el.name, '');
|
||||
}
|
||||
} else {
|
||||
formData.append(el.name, el.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'sl-button',
|
||||
serialize: (el: SlButton, formData) => (el.name && !el.disabled ? formData.append(el.name, el.value) : null),
|
||||
click: event => {
|
||||
const target = event.target as SlButton;
|
||||
if (target.submit) {
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'sl-checkbox',
|
||||
serialize: (el: SlCheckbox, formData) =>
|
||||
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
|
||||
},
|
||||
{
|
||||
tag: 'sl-color-picker',
|
||||
serialize: (el: SlColorPicker, formData) =>
|
||||
el.name && !el.disabled ? formData.append(el.name, el.value) : null
|
||||
},
|
||||
{
|
||||
tag: 'sl-input',
|
||||
serialize: (el: SlInput, formData) => (el.name && !el.disabled ? formData.append(el.name, el.value) : null),
|
||||
keyDown: event => {
|
||||
if (event.key === 'Enter' && !event.defaultPrevented) {
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'sl-radio',
|
||||
serialize: (el: SlRadio, formData) =>
|
||||
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
|
||||
},
|
||||
{
|
||||
tag: 'sl-range',
|
||||
serialize: (el: SlRange, formData) => {
|
||||
if (el.name && !el.disabled) {
|
||||
formData.append(el.name, el.value + '');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'sl-select',
|
||||
serialize: (el: SlSelect, formData) => {
|
||||
if (el.name && !el.disabled) {
|
||||
if (el.multiple) {
|
||||
const selectedOptions = [...el.value];
|
||||
if (selectedOptions.length) {
|
||||
selectedOptions.map(value => formData.append(el.name, value));
|
||||
} else {
|
||||
formData.append(el.name, '');
|
||||
}
|
||||
} else {
|
||||
formData.append(el.name, el.value + '');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'sl-switch',
|
||||
serialize: (el: SlSwitch, formData) =>
|
||||
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
|
||||
},
|
||||
{
|
||||
tag: 'sl-textarea',
|
||||
serialize: (el: SlTextarea, formData) => (el.name && !el.disabled ? formData.append(el.name, el.value) : null)
|
||||
},
|
||||
{
|
||||
tag: 'textarea',
|
||||
serialize: (el: HTMLTextAreaElement, formData) =>
|
||||
el.name && !el.disabled ? formData.append(el.name, el.value) : null
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/** Serializes all form controls elements and returns a `FormData` object. */
|
||||
getFormData() {
|
||||
const formData = new FormData();
|
||||
const formControls = this.getFormControls();
|
||||
|
||||
formControls.map(el => this.serializeElement(el, formData));
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/** Gets all form control elements (native and custom). */
|
||||
getFormControls() {
|
||||
const slot = this.form.querySelector('slot')!;
|
||||
const tags = this.formControls.map(control => control.tag);
|
||||
return slot
|
||||
.assignedElements({ flatten: true })
|
||||
.reduce(
|
||||
(all: HTMLElement[], el: HTMLElement) => all.concat(el, [...el.querySelectorAll('*')] as HTMLElement[]),
|
||||
[]
|
||||
)
|
||||
.filter((el: HTMLElement) => tags.includes(el.tagName.toLowerCase())) as HTMLElement[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the form. If all controls are valid, the `sl-submit` event will be emitted and the promise will resolve
|
||||
* with `true`. If any form control is invalid, the promise will resolve with `false` and no event will be emitted.
|
||||
*/
|
||||
submit() {
|
||||
const formData = this.getFormData();
|
||||
const formControls = this.getFormControls();
|
||||
const formControlsThatReport = formControls.filter((el: any) => typeof el.reportValidity === 'function') as any;
|
||||
|
||||
if (!this.novalidate) {
|
||||
for (const el of formControlsThatReport) {
|
||||
const isValid = el.reportValidity();
|
||||
|
||||
if (!isValid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(this, 'sl-submit', { detail: { formData, formControls } });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
handleClick(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const tag = target.tagName.toLowerCase();
|
||||
|
||||
for (const formControl of this.formControls) {
|
||||
if (formControl.tag === tag && formControl.click) {
|
||||
formControl.click(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const tag = target.tagName.toLowerCase();
|
||||
|
||||
for (const formControl of this.formControls) {
|
||||
if (formControl.tag === tag && formControl.keyDown) {
|
||||
formControl.keyDown(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serializeElement(el: HTMLElement, formData: FormData) {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
|
||||
for (const formControl of this.formControls) {
|
||||
if (formControl.tag === tag) {
|
||||
return formControl.serialize(el, formData);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div part="base" class="form" role="form" @click=${this.handleClick} @keydown=${this.handleKeyDown}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-form': SlForm;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||
import { live } from 'lit/directives/live.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { FormSubmitController, getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './input.styles';
|
||||
|
||||
|
|
@ -49,6 +49,8 @@ export default class SlInput extends LitElement {
|
|||
|
||||
@query('.input__control') input: HTMLInputElement;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this);
|
||||
private hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private inputId = `input-${++id}`;
|
||||
private helpTextId = `input-help-text-${id}`;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
|
|||
import { live } from 'lit/directives/live.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import styles from './radio.styles';
|
||||
|
||||
/**
|
||||
|
|
@ -28,6 +29,11 @@ export default class SlRadio extends LitElement {
|
|||
|
||||
@query('input[type="radio"]') input: HTMLInputElement;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this, {
|
||||
value: (control: SlRadio) => (control.checked ? control.value : undefined)
|
||||
});
|
||||
|
||||
@state() private hasFocus = false;
|
||||
|
||||
/** The radio's name attribute. */
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { emit } from '../../internal/event';
|
|||
import { live } from 'lit/directives/live.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './range.styles';
|
||||
|
||||
|
|
@ -39,6 +40,8 @@ export default class SlRange extends LitElement {
|
|||
@query('.range__control') input: HTMLInputElement;
|
||||
@query('.range__tooltip') output: HTMLOutputElement;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this);
|
||||
private hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private inputId = `input-${++id}`;
|
||||
private helpTextId = `input-help-text-${id}`;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { emit } from '../../internal/event';
|
|||
import { watch } from '../../internal/watch';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlIconButton from '../icon-button/icon-button';
|
||||
|
|
@ -65,6 +66,8 @@ export default class SlSelect extends LitElement {
|
|||
@query('.select__hidden-select') input: HTMLInputElement;
|
||||
@query('.select__menu') menu: SlMenu;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this);
|
||||
private hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private inputId = `select-${++id}`;
|
||||
private helpTextId = `select-help-text-${id}`;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
|
|||
import { live } from 'lit/directives/live.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import styles from './switch.styles';
|
||||
|
||||
let id = 0;
|
||||
|
|
@ -34,6 +35,10 @@ export default class SlSwitch extends LitElement {
|
|||
|
||||
@query('input[type="checkbox"]') input: HTMLInputElement;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this, {
|
||||
value: (control: SlSwitch) => (control.checked ? control.value : undefined)
|
||||
});
|
||||
private switchId = `switch-${++id}`;
|
||||
private labelId = `switch-label-${id}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { emit } from '../../internal/event';
|
|||
import { live } from 'lit/directives/live.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './textarea.styles';
|
||||
|
||||
|
|
@ -35,6 +36,8 @@ export default class SlTextarea extends LitElement {
|
|||
|
||||
@query('.textarea__control') input: HTMLTextAreaElement;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this);
|
||||
private hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private inputId = `textarea-${++id}`;
|
||||
private helpTextId = `textarea-help-text-${id}`;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,108 @@
|
|||
import { html, TemplateResult } from 'lit';
|
||||
import { html, ReactiveController, ReactiveControllerHost, TemplateResult } from 'lit';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import './formdata-event-polyfill';
|
||||
|
||||
export interface FormSubmitControllerOptions {
|
||||
/** A function that returns the form containing the form control. */
|
||||
form: (input: unknown) => HTMLFormElement;
|
||||
/** A function that returns the form control's name, which will be submitted with the form data. */
|
||||
name: (input: unknown) => string;
|
||||
/** A function that returns the form control's current value. */
|
||||
value: (input: unknown) => any;
|
||||
/** A function that returns the form control's current disabled state. If disabled, the value won't be submitted. */
|
||||
disabled: (input: unknown) => boolean;
|
||||
/**
|
||||
* A function that maps to the form control's reportValidity() function. When the control is invalid, this will
|
||||
* prevent submission and trigger the browser's constraint violation warning.
|
||||
*/
|
||||
reportValidity: (input: unknown) => boolean;
|
||||
}
|
||||
|
||||
export class FormSubmitController implements ReactiveController {
|
||||
host?: ReactiveControllerHost & Element;
|
||||
form?: HTMLFormElement;
|
||||
options?: FormSubmitControllerOptions;
|
||||
|
||||
constructor(host: ReactiveControllerHost & Element, options?: FormSubmitControllerOptions) {
|
||||
(this.host = host).addController(this);
|
||||
this.options = Object.assign(
|
||||
{
|
||||
form: (input: HTMLInputElement) => input.closest('form'),
|
||||
name: (input: HTMLInputElement) => input.name,
|
||||
value: (input: HTMLInputElement) => input.value,
|
||||
disabled: (input: HTMLInputElement) => input.disabled,
|
||||
reportValidity: (input: HTMLInputElement) => {
|
||||
return typeof input.reportValidity === 'function' ? input.reportValidity() : true;
|
||||
}
|
||||
},
|
||||
options
|
||||
);
|
||||
this.handleFormData = this.handleFormData.bind(this);
|
||||
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.form = this.options?.form(this.host);
|
||||
|
||||
if (this.form) {
|
||||
this.form.addEventListener('formdata', this.handleFormData);
|
||||
this.form.addEventListener('submit', this.handleFormSubmit);
|
||||
}
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
if (this.form) {
|
||||
this.form.removeEventListener('formdata', this.handleFormData);
|
||||
this.form.removeEventListener('submit', this.handleFormSubmit);
|
||||
this.form = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
handleFormData(event: FormDataEvent) {
|
||||
const disabled = this.options?.disabled(this.host);
|
||||
const name = this.options?.name(this.host);
|
||||
const value = this.options?.value(this.host);
|
||||
|
||||
if (!disabled && name && value !== undefined) {
|
||||
if (Array.isArray(value)) {
|
||||
value.map(val => event.formData.append(name, val));
|
||||
} else {
|
||||
event.formData.append(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleFormSubmit(event: Event) {
|
||||
const form = this.form;
|
||||
const disabled = this.options?.disabled(this.host);
|
||||
const reportValidity = this.options?.reportValidity;
|
||||
|
||||
if (form && !form.noValidate && !disabled && reportValidity && !reportValidity(this.host)) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
submit() {
|
||||
// Calling form.submit() seems to bypass the submit event and constraint validation. Instead, we can inject a
|
||||
// native submit button into the form, click it, then remove it to simulate a standard form submission.
|
||||
const button = document.createElement('button');
|
||||
if (this.form) {
|
||||
button.type = 'submit';
|
||||
button.style.position = 'absolute';
|
||||
button.style.width = '0';
|
||||
button.style.height = '0';
|
||||
button.style.clip = 'rect(0 0 0 0)';
|
||||
button.style.clipPath = 'inset(50%)';
|
||||
button.style.overflow = 'hidden';
|
||||
button.style.whiteSpace = 'nowrap';
|
||||
this.form.append(button);
|
||||
button.click();
|
||||
button.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const renderFormControl = (
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// Polyfills the formdata event in unsupportive browsers. This is a partial polyfill to support appending custom element
|
||||
// form data on submit. The formdata event landed in Safari until 15.1, which is slighly too new to rely on. All other
|
||||
// browsers have great support.
|
||||
//
|
||||
// https://caniuse.com/mdn-api_htmlformelement_formdata_event
|
||||
//
|
||||
// Original code derived from: https://gist.github.com/WickyNilliams/eb6a44075356ee504dd9491c5a3ab0be
|
||||
//
|
||||
// Copyright (c) 2021 Nick Williams (https://wicky.nillia.ms)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
class FormDataEventPolyfill extends Event {
|
||||
formData: FormData;
|
||||
|
||||
constructor(formData: FormData) {
|
||||
super('formdata');
|
||||
this.formData = formData;
|
||||
}
|
||||
}
|
||||
|
||||
class FormDataPolyfill extends FormData {
|
||||
private form: HTMLFormElement;
|
||||
|
||||
constructor(form: HTMLFormElement) {
|
||||
super(form);
|
||||
this.form = form;
|
||||
form.dispatchEvent(new FormDataEventPolyfill(this));
|
||||
}
|
||||
|
||||
append(name: string, value: any) {
|
||||
let input = this.form.elements[name as any] as HTMLInputElement;
|
||||
|
||||
if (!input) {
|
||||
input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = name;
|
||||
this.form.appendChild(input);
|
||||
}
|
||||
|
||||
if (this.has(name)) {
|
||||
const entries = this.getAll(name);
|
||||
const index = entries.indexOf(input.value);
|
||||
|
||||
if (index !== -1) {
|
||||
entries.splice(index, 1);
|
||||
}
|
||||
|
||||
entries.push(value);
|
||||
this.set(name, entries as any);
|
||||
} else {
|
||||
super.append(name, value);
|
||||
}
|
||||
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
function supportsFormDataEvent() {
|
||||
const form = document.createElement('form');
|
||||
let isSupported = false;
|
||||
|
||||
document.body.append(form);
|
||||
|
||||
form.addEventListener('submit', event => {
|
||||
new FormData(event.target as HTMLFormElement);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
form.addEventListener('formdata', () => (isSupported = true));
|
||||
form.dispatchEvent(new Event('submit', { cancelable: true }));
|
||||
form.remove();
|
||||
|
||||
return isSupported;
|
||||
}
|
||||
|
||||
function polyfillFormData() {
|
||||
if (!window.FormData || supportsFormDataEvent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.FormData = FormDataPolyfill;
|
||||
window.addEventListener('submit', event => {
|
||||
if (!event.defaultPrevented) {
|
||||
new FormData(event.target as HTMLFormElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
polyfillFormData();
|
||||
|
|
@ -16,7 +16,6 @@ export { default as SlDialog } from './components/dialog/dialog';
|
|||
export { default as SlDivider } from './components/divider/divider';
|
||||
export { default as SlDrawer } from './components/drawer/drawer';
|
||||
export { default as SlDropdown } from './components/dropdown/dropdown';
|
||||
export { default as SlForm } from './components/form/form';
|
||||
export { default as SlFormatBytes } from './components/format-bytes/format-bytes';
|
||||
export { default as SlFormatDate } from './components/format-date/format-date';
|
||||
export { default as SlFormatNumber } from './components/format-number/format-number';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// Serializes a form and returns a plain object. If a form control with the same name appears more than once, the
|
||||
// property will be converted to an array.
|
||||
//
|
||||
export function serialize(form: HTMLFormElement) {
|
||||
const formData = new FormData(form);
|
||||
const object: { [key: string]: any } = {};
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
if (Reflect.has(object, key)) {
|
||||
if (Array.isArray(object[key])) {
|
||||
object[key].push(value);
|
||||
} else {
|
||||
object[key] = [object[key], value];
|
||||
}
|
||||
} else {
|
||||
object[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
Ładowanie…
Reference in New Issue