diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 04eb31e1f..6996231bd 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -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 diff --git a/docs/components/checkbox.md b/docs/components/checkbox.md index a9045f7c0..d11fa427b 100644 --- a/docs/components/checkbox.md +++ b/docs/components/checkbox.md @@ -16,7 +16,7 @@ const App = () => ( ); ``` -?> This component doesn't work with standard forms. Use [``](/components/form) instead. +?> This component works with standard `
` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. ## Examples diff --git a/docs/components/color-picker.md b/docs/components/color-picker.md index a39579a88..9622875d2 100644 --- a/docs/components/color-picker.md +++ b/docs/components/color-picker.md @@ -16,6 +16,8 @@ const App = () => ( ); ``` +?> This component works with standard `` 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 diff --git a/docs/components/form.md b/docs/components/form.md deleted file mode 100644 index 9f6bc05b1..000000000 --- a/docs/components/form.md +++ /dev/null @@ -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 `` 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 - - -
- - Birds - Cats - Dogs - -
- - I totally agree - -

- Submit -
- - -``` - -```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 = () => ( - - -
- - Birds - Cats - Dogs - -
- - I totally agree - -

- Submit -
-); -``` - -## 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 - - - -
- Submit -
- - -``` - -```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 ( - - - -
- Submit -
- ); -}; -``` - -### 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 `` 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 - - -
- - Birds - Cats - Dogs - Other - -
- -
- Check me before submitting -

- Submit -
- - -``` - -```jsx react -import { - SlButton, - SlCheckbox, - SlForm, - SlInput, - SlMenuItem, - SlSelect, - SlTextarea -} from '@shoelace-style/shoelace/dist/react'; - -const App = () => ( - alert('All fields are valid!')}> - -
- - Birds - Cats - Dogs - Other - -
- -
- Check me before submitting -

- Submit -
-); -``` - -### 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 `` elements. - -```html preview - - -
- Submit -
- - -``` - -```jsx react -import { - SlButton, - SlForm, - SlInput -} from '@shoelace-style/shoelace/dist/react'; - -const App = () => ( - alert('All fields are valid!')}> - -
- Submit -
-); -``` - -### Input Types - -Some input types will automatically trigger constraints, such as `email` and `url`. - -```html preview - - -
- -
- Submit -
- - -``` - -```jsx react -import { - SlButton, - SlForm, - SlInput -} from '@shoelace-style/shoelace/dist/react'; - -const App = () => ( - alert('All fields are valid!')}> - -
- -
- Submit -
-); -``` - -### 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 - - -
- Submit -
- - -``` - -```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 ( - alert('All fields are valid!')}> - -
- Submit -
- ); -}; -``` - -### 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 - - Please enter "shoelace" to continue - - - -``` - -```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 = () => ( - <> - - Please enter "shoelace" to continue - - - - -); -``` - -### 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 - - - - - -``` - -[component-metadata:sl-form] diff --git a/docs/components/input.md b/docs/components/input.md index 36be172cd..6591acebf 100644 --- a/docs/components/input.md +++ b/docs/components/input.md @@ -16,9 +16,7 @@ const App = () => ( ); ``` -?> This component doesn't work with standard forms. Use [``](/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 `` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. ## Examples diff --git a/docs/components/radio.md b/docs/components/radio.md index 21fec821d..c30da7322 100644 --- a/docs/components/radio.md +++ b/docs/components/radio.md @@ -26,7 +26,7 @@ const App = () => ( ); ``` -?> This component doesn't work with standard forms. Use [``](/components/form) instead. +?> This component works with standard `` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. ## Examples diff --git a/docs/components/range.md b/docs/components/range.md index 04931d800..4b337d8a2 100644 --- a/docs/components/range.md +++ b/docs/components/range.md @@ -16,7 +16,7 @@ const App = () => ( ); ``` -?> This component doesn't work with standard forms. Use [``](/components/form) instead. +?> This component works with standard `` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. ## Examples diff --git a/docs/components/select.md b/docs/components/select.md index 72efca656..10770da24 100644 --- a/docs/components/select.md +++ b/docs/components/select.md @@ -32,7 +32,7 @@ const App = () => ( ); ``` -?> This component doesn't work with standard forms. Use [``](/components/form) instead. +?> This component works with standard `` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. ## Examples diff --git a/docs/components/switch.md b/docs/components/switch.md index 1570318e4..c638b4873 100644 --- a/docs/components/switch.md +++ b/docs/components/switch.md @@ -16,7 +16,7 @@ const App = () => ( ); ``` -?> This component doesn't work with standard forms. Use [``](/components/form) instead. +?> This component works with standard `` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. ## Examples diff --git a/docs/components/textarea.md b/docs/components/textarea.md index 1956dc728..3d4a30b7b 100644 --- a/docs/components/textarea.md +++ b/docs/components/textarea.md @@ -16,9 +16,7 @@ const App = () => ( ); ``` -?> This component doesn't work with standard forms. Use [``](/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 `` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. ## Examples diff --git a/docs/getting-started/form-controls.md b/docs/getting-started/form-controls.md new file mode 100644 index 000000000..3fae82c10 --- /dev/null +++ b/docs/getting-started/form-controls.md @@ -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 `` 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. ``, 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 `` 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 `` 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 + + +
+ + Birds + Cats + Dogs + Other + +
+ +
+ Check me before submitting +

+ Submit + + + +``` + +```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 ( +
+ +
+ + Birds + Cats + Dogs + Other + +
+ +
+ Check me before submitting +

+ Submit + + ); +}; +``` + +### 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 `` elements. + +```html preview +
+ +
+ Submit +
+ + +``` + +```jsx react +import { SlButton, SlInput } from '@shoelace-style/shoelace/dist/react'; + +const App = () => { + function handleSubmit(event) { + event.preventDefault(); + alert('All fields are valid!'); + } + + return ( +
+ +
+ Submit + + ); +}; +``` + +### Input Types + +Some input types will automatically trigger constraints, such as `email` and `url`. + +```html preview +
+ +
+ +
+ Submit +
+ + +``` + +```jsx react +import { SlButton, SlInput } from '@shoelace-style/shoelace/dist/react'; + +const App = () => { + function handleSubmit(event) { + event.preventDefault(); + alert('All fields are valid!'); + } + + return ( +
+ +
+ +
+ Submit + + ); +}; +``` + +### 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 +
+ +
+ Submit +
+ + +``` + +```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 ( +
+ +
+ Submit + + ); +}; +``` + +### 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 + + Please enter "shoelace" to continue + + + +``` + +```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 = () => ( + <> + + Please enter "shoelace" to continue + + + + +); +``` + +### 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 +
+ +
+ + +``` \ No newline at end of file diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 739bcdd33..1cb0c3f02 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -8,6 +8,8 @@ _During the beta period, these restrictions may be relaxed in the event of a mis ## Next +- 🚨 BREAKING: removed `` because all form components submit with `
` now ([learn more](/getting-started/form-controls)) +- 🚨 BREAKING: changed `submit` attribute to `type="submit"` on `` - 🚨 BREAKING: changed the `alt` attribute to `label` in `` for consistency with other components - Added `role="status"` to `` - Added `valueAsDate` and `valueAsNumber` properties to `` [#570](https://github.com/shoelace-style/shoelace/issues/570) diff --git a/docs/resources/contributing.md b/docs/resources/contributing.md index 85f66d748..c912113b8 100644 --- a/docs/resources/contributing.md +++ b/docs/resources/contributing.md @@ -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 `` +- All form controls must be tested to work with the standard `` element diff --git a/src/components/button/button.ts b/src/components/button/button.ts index 774f0f71b..68aba2edf 100644 --- a/src/components/button/button.ts +++ b/src/components/button/button.ts @@ -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 `