shoelace/src/components/form/form.ts

283 wiersze
8.4 KiB
TypeScript
Czysty Zwykły widok Historia

2021-03-06 17:01:39 +00:00
import { LitElement, customElement, html, property, query, unsafeCSS } from 'lit-element';
import { event, EventEmitter } from '../../internal/event';
2021-02-26 14:09:13 +00:00
import styles from 'sass:./form.scss';
import {
SlButton,
SlCheckbox,
SlColorPicker,
SlInput,
SlRadio,
SlRange,
SlSelect,
SlSwitch,
SlTextarea
} from '../../shoelace';
2020-07-15 21:30:37 +00:00
interface FormControl {
tag: string;
serialize: (el: HTMLElement, formData: FormData) => void;
click?: (event: MouseEvent) => any;
keyDown?: (event: KeyboardEvent) => any;
}
/**
2020-07-17 10:09:10 +00:00
* @since 2.0
2020-12-09 13:17:49 +00:00
* @status stable
2020-07-15 21:30:37 +00:00
*
* @slot - The form's content.
*
* @part base - The component's base wrapper.
*/
2021-03-06 17:01:39 +00:00
@customElement('sl-form')
export class SlForm extends LitElement {
static styles = unsafeCSS(styles);
@query('.form') form: HTMLElement;
2020-07-15 21:30:37 +00:00
2021-02-26 14:09:13 +00:00
private formControls: FormControl[];
2020-07-15 21:30:37 +00:00
2020-08-29 14:39:18 +00:00
/** Prevent the form from validating inputs before submitting. */
2021-03-06 17:01:39 +00:00
@property({ type: Boolean, reflect: true }) novalidate = false;
/**
* @emit 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 doen'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. Event details will contain:
* `{ formData: FormData; formControls: HTMLElement[] }`
*/
@event('sl-submit') slSubmit: EventEmitter<{ formData: FormData; formControls: HTMLElement[] }>;
connectedCallback() {
super.connectedCallback();
2020-07-15 21:30:37 +00:00
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') {
2021-02-26 14:09:13 +00:00
[...(el.files as FileList)].map(file => formData.append(el.name, file));
2020-07-15 21:30:37 +00:00
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;
2020-12-02 22:17:34 +00:00
if (
event.key === 'Enter' &&
!event.defaultPrevented &&
!['checkbox', 'file', 'radio'].includes(target.type)
) {
2020-07-15 21:30:37 +00:00
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',
2021-02-26 14:09:13 +00:00
serialize: (el: SlButton, formData) => (el.name && !el.disabled ? formData.append(el.name, el.value) : null),
2020-07-15 21:30:37 +00:00
click: event => {
2021-02-26 14:09:13 +00:00
const target = event.target as SlButton;
2020-07-15 21:30:37 +00:00
if (target.submit) {
this.submit();
}
}
},
{
tag: 'sl-checkbox',
2021-02-26 14:09:13 +00:00
serialize: (el: SlCheckbox, formData) =>
2020-07-15 21:30:37 +00:00
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
},
2020-09-04 12:57:34 +00:00
{
tag: 'sl-color-picker',
2021-02-26 14:09:13 +00:00
serialize: (el: SlColorPicker, formData) =>
2020-09-04 12:57:34 +00:00
el.name && !el.disabled ? formData.append(el.name, el.value) : null
},
2020-07-15 21:30:37 +00:00
{
tag: 'sl-input',
2021-02-26 14:09:13 +00:00
serialize: (el: SlInput, formData) => (el.name && !el.disabled ? formData.append(el.name, el.value) : null),
2020-07-15 21:30:37 +00:00
keyDown: event => {
2020-12-02 22:17:34 +00:00
if (event.key === 'Enter' && !event.defaultPrevented) {
2020-07-15 21:30:37 +00:00
this.submit();
}
}
},
{
tag: 'sl-radio',
2021-02-26 14:09:13 +00:00
serialize: (el: SlRadio, formData) =>
2020-07-15 21:30:37 +00:00
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
},
{
tag: 'sl-range',
2021-02-26 14:09:13 +00:00
serialize: (el: SlRange, formData) => {
2020-07-15 21:30:37 +00:00
if (el.name && !el.disabled) {
formData.append(el.name, el.value + '');
}
}
},
{
tag: 'sl-select',
2021-02-26 14:09:13 +00:00
serialize: (el: SlSelect, formData) => {
2020-07-15 21:30:37 +00:00
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',
2021-02-26 14:09:13 +00:00
serialize: (el: SlSwitch, formData) =>
2020-07-15 21:30:37 +00:00
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
},
{
tag: 'sl-textarea',
2021-02-26 14:09:13 +00:00
serialize: (el: SlTextarea, formData) => (el.name && !el.disabled ? formData.append(el.name, el.value) : null)
2020-07-15 21:30:37 +00:00
},
{
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. */
2021-02-26 14:09:13 +00:00
getFormData() {
2020-07-15 21:30:37 +00:00
const formData = new FormData();
2021-02-26 14:09:13 +00:00
const formControls = this.getFormControls();
2020-07-15 21:30:37 +00:00
formControls.map(el => this.serializeElement(el, formData));
return formData;
}
/** Gets all form control elements (native and custom). */
2021-02-26 14:09:13 +00:00
getFormControls() {
const slot = this.form.querySelector('slot')!;
2020-07-15 21:30:37 +00:00
const tags = this.formControls.map(control => control.tag);
return slot
.assignedElements({ flatten: true })
2021-02-26 14:09:13 +00:00
.reduce(
(all: HTMLElement[], el: HTMLElement) => all.concat(el, [...el.querySelectorAll('*')] as HTMLElement[]),
[]
)
.filter((el: HTMLElement) => tags.includes(el.tagName.toLowerCase())) as HTMLElement[];
2020-07-15 21:30:37 +00:00
}
2020-08-28 20:14:39 +00:00
/**
2020-10-15 18:33:30 +00:00
* 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.
2020-08-28 20:14:39 +00:00
*/
2021-02-26 14:09:13 +00:00
submit() {
const formData = this.getFormData();
const formControls = this.getFormControls();
2020-08-28 20:14:39 +00:00
const formControlsThatReport = formControls.filter((el: any) => typeof el.reportValidity === 'function') as any;
2020-08-29 14:39:18 +00:00
if (!this.novalidate) {
for (const el of formControlsThatReport) {
2021-02-26 14:09:13 +00:00
const isValid = el.reportValidity();
2020-08-28 20:14:39 +00:00
2020-08-29 14:39:18 +00:00
if (!isValid) {
return false;
}
2020-08-28 20:14:39 +00:00
}
}
2020-07-15 21:30:37 +00:00
2021-03-06 17:01:39 +00:00
this.slSubmit.emit({ detail: { formData, formControls } });
2020-08-28 20:14:39 +00:00
return true;
2020-07-15 21:30:37 +00:00
}
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() {
2021-02-26 14:09:13 +00:00
return html`
2021-03-06 17:01:39 +00:00
<div part="base" class="form" role="form" @click=${this.handleClick} @keydown=${this.handleKeyDown}>
<slot></slot>
2020-07-15 21:30:37 +00:00
</div>
2021-02-26 14:09:13 +00:00
`;
2020-07-15 21:30:37 +00:00
}
}