kopia lustrzana https://github.com/shoelace-style/shoelace
283 wiersze
8.4 KiB
TypeScript
283 wiersze
8.4 KiB
TypeScript
import { LitElement, customElement, html, property, query, unsafeCSS } from 'lit-element';
|
|
import { event, EventEmitter } from '../../internal/event';
|
|
import styles from 'sass:./form.scss';
|
|
import {
|
|
SlButton,
|
|
SlCheckbox,
|
|
SlColorPicker,
|
|
SlInput,
|
|
SlRadio,
|
|
SlRange,
|
|
SlSelect,
|
|
SlSwitch,
|
|
SlTextarea
|
|
} from '../../shoelace';
|
|
|
|
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.
|
|
*
|
|
* @part base - The component's base wrapper.
|
|
*/
|
|
@customElement('sl-form')
|
|
export class SlForm extends LitElement {
|
|
static styles = unsafeCSS(styles);
|
|
|
|
@query('.form') form: HTMLElement;
|
|
|
|
private formControls: FormControl[];
|
|
|
|
/** Prevent the form from validating inputs before submitting. */
|
|
@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();
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.slSubmit.emit({ 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>
|
|
`;
|
|
}
|
|
}
|