Files
webawesome/src/components/form/form.ts

287 lines
8.4 KiB
TypeScript
Raw Normal View History

2021-02-26 09:09:13 -05:00
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import styles from 'sass:./form.scss';
import {
SlButton,
SlCheckbox,
SlColorPicker,
SlInput,
SlRadio,
SlRange,
SlSelect,
SlSwitch,
SlTextarea
} from '../../shoelace';
2020-07-15 17:30:37 -04:00
interface FormControl {
tag: string;
serialize: (el: HTMLElement, formData: FormData) => void;
click?: (event: MouseEvent) => any;
keyDown?: (event: KeyboardEvent) => any;
}
/**
2020-07-17 06:09:10 -04:00
* @since 2.0
2020-12-09 08:17:49 -05:00
* @status stable
2020-07-15 17:30:37 -04:00
*
* @slot - The form's content.
*
* @part base - The component's base wrapper.
2021-02-26 09:09:13 -05:00
*
* @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[] }`
2020-07-15 17:30:37 -04:00
*/
2021-02-26 09:09:13 -05:00
export default class SlForm extends Shoemaker {
static tag = 'sl-form';
static props = ['novalidate'];
static styles = styles;
2020-07-15 17:30:37 -04:00
2021-02-26 09:09:13 -05:00
private form: HTMLElement;
private formControls: FormControl[];
2020-07-15 17:30:37 -04:00
2020-08-29 10:39:18 -04:00
/** Prevent the form from validating inputs before submitting. */
2021-02-26 09:09:13 -05:00
novalidate = false;
2021-02-26 09:09:13 -05:00
onConnect() {
2020-07-15 17:30:37 -04: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 09:09:13 -05:00
[...(el.files as FileList)].map(file => formData.append(el.name, file));
2020-07-15 17:30:37 -04: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 17:17:34 -05:00
if (
event.key === 'Enter' &&
!event.defaultPrevented &&
!['checkbox', 'file', 'radio'].includes(target.type)
) {
2020-07-15 17:30:37 -04: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 09:09:13 -05:00
serialize: (el: SlButton, formData) => (el.name && !el.disabled ? formData.append(el.name, el.value) : null),
2020-07-15 17:30:37 -04:00
click: event => {
2021-02-26 09:09:13 -05:00
const target = event.target as SlButton;
2020-07-15 17:30:37 -04:00
if (target.submit) {
this.submit();
}
}
},
{
tag: 'sl-checkbox',
2021-02-26 09:09:13 -05:00
serialize: (el: SlCheckbox, formData) =>
2020-07-15 17:30:37 -04:00
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
},
2020-09-04 08:57:34 -04:00
{
tag: 'sl-color-picker',
2021-02-26 09:09:13 -05:00
serialize: (el: SlColorPicker, formData) =>
2020-09-04 08:57:34 -04:00
el.name && !el.disabled ? formData.append(el.name, el.value) : null
},
2020-07-15 17:30:37 -04:00
{
tag: 'sl-input',
2021-02-26 09:09:13 -05:00
serialize: (el: SlInput, formData) => (el.name && !el.disabled ? formData.append(el.name, el.value) : null),
2020-07-15 17:30:37 -04:00
keyDown: event => {
2020-12-02 17:17:34 -05:00
if (event.key === 'Enter' && !event.defaultPrevented) {
2020-07-15 17:30:37 -04:00
this.submit();
}
}
},
{
tag: 'sl-radio',
2021-02-26 09:09:13 -05:00
serialize: (el: SlRadio, formData) =>
2020-07-15 17:30:37 -04:00
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
},
{
tag: 'sl-range',
2021-02-26 09:09:13 -05:00
serialize: (el: SlRange, formData) => {
2020-07-15 17:30:37 -04:00
if (el.name && !el.disabled) {
formData.append(el.name, el.value + '');
}
}
},
{
tag: 'sl-select',
2021-02-26 09:09:13 -05:00
serialize: (el: SlSelect, formData) => {
2020-07-15 17:30:37 -04: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 09:09:13 -05:00
serialize: (el: SlSwitch, formData) =>
2020-07-15 17:30:37 -04:00
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
},
{
tag: 'sl-textarea',
2021-02-26 09:09:13 -05:00
serialize: (el: SlTextarea, formData) => (el.name && !el.disabled ? formData.append(el.name, el.value) : null)
2020-07-15 17:30:37 -04:00
},
{
tag: 'textarea',
serialize: (el: HTMLTextAreaElement, formData) =>
el.name && !el.disabled ? formData.append(el.name, el.value) : null
}
];
this.handleClick = this.handleClick.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
/** Serializes all form controls elements and returns a `FormData` object. */
2021-02-26 09:09:13 -05:00
getFormData() {
2020-07-15 17:30:37 -04:00
const formData = new FormData();
2021-02-26 09:09:13 -05:00
const formControls = this.getFormControls();
2020-07-15 17:30:37 -04:00
formControls.map(el => this.serializeElement(el, formData));
return formData;
}
/** Gets all form control elements (native and custom). */
2021-02-26 09:09:13 -05:00
getFormControls() {
const slot = this.form.querySelector('slot')!;
2020-07-15 17:30:37 -04:00
const tags = this.formControls.map(control => control.tag);
return slot
.assignedElements({ flatten: true })
2021-02-26 09:09:13 -05: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 17:30:37 -04:00
}
2020-08-28 16:14:39 -04:00
/**
2020-10-15 14:33:30 -04: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 16:14:39 -04:00
*/
2021-02-26 09:09:13 -05:00
submit() {
const formData = this.getFormData();
const formControls = this.getFormControls();
2020-08-28 16:14:39 -04:00
const formControlsThatReport = formControls.filter((el: any) => typeof el.reportValidity === 'function') as any;
2020-08-29 10:39:18 -04:00
if (!this.novalidate) {
for (const el of formControlsThatReport) {
2021-02-26 09:09:13 -05:00
const isValid = el.reportValidity();
2020-08-28 16:14:39 -04:00
2020-08-29 10:39:18 -04:00
if (!isValid) {
return false;
}
2020-08-28 16:14:39 -04:00
}
}
2020-07-15 17:30:37 -04:00
2021-02-26 09:09:13 -05:00
this.emit('sl-submit', { detail: { formData, formControls } });
2020-08-28 16:14:39 -04:00
return true;
2020-07-15 17:30:37 -04: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 09:09:13 -05:00
return html`
2020-07-15 17:30:37 -04:00
<div
2021-02-26 09:09:13 -05:00
ref=${(el: HTMLElement) => (this.form = el)}
2020-07-15 17:30:37 -04:00
part="base"
class="form"
role="form"
2021-02-26 09:09:13 -05:00
onclick=${this.handleClick.bind(this)}
onkeydown=${this.handleKeyDown.bind(this)}
2020-07-15 17:30:37 -04:00
>
<slot />
</div>
2021-02-26 09:09:13 -05:00
`;
2020-07-15 17:30:37 -04:00
}
}