2022-01-11 14:18:20 +00:00
|
|
|
import './formdata-event-polyfill';
|
2022-03-24 12:01:09 +00:00
|
|
|
import type SlButton from '../components/button/button';
|
2022-03-08 22:34:17 +00:00
|
|
|
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
2022-01-11 14:18:20 +00:00
|
|
|
|
|
|
|
export interface FormSubmitControllerOptions {
|
|
|
|
/** A function that returns the form containing the form control. */
|
2022-01-16 05:47:14 +00:00
|
|
|
form: (input: unknown) => HTMLFormElement | null;
|
2022-01-11 14:18:20 +00:00
|
|
|
/** 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. */
|
2022-01-16 05:47:14 +00:00
|
|
|
value: (input: unknown) => unknown | unknown[];
|
2022-01-11 14:18:20 +00:00
|
|
|
/** 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;
|
2022-01-16 05:47:14 +00:00
|
|
|
options: FormSubmitControllerOptions;
|
2022-01-11 14:18:20 +00:00
|
|
|
|
2022-01-16 05:47:14 +00:00
|
|
|
constructor(host: ReactiveControllerHost & Element, options?: Partial<FormSubmitControllerOptions>) {
|
2022-01-11 14:18:20 +00:00
|
|
|
(this.host = host).addController(this);
|
2022-01-16 05:47:14 +00:00
|
|
|
this.options = {
|
|
|
|
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;
|
2022-01-11 14:18:20 +00:00
|
|
|
},
|
2022-01-16 05:47:14 +00:00
|
|
|
...options
|
|
|
|
};
|
2022-01-11 14:18:20 +00:00
|
|
|
this.handleFormData = this.handleFormData.bind(this);
|
|
|
|
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
hostConnected() {
|
2022-04-11 14:36:16 +00:00
|
|
|
// We use the capture phase and listen on the document to intercept the event as soon as possible. Otherwise, the
|
|
|
|
// user may attach listeners that run before we have a chance to do validation.
|
|
|
|
document.addEventListener('formdata', this.handleFormData, { capture: true });
|
|
|
|
document.addEventListener('submit', this.handleFormSubmit, { capture: true });
|
2022-01-11 14:18:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
hostDisconnected() {
|
2022-04-11 14:36:16 +00:00
|
|
|
document.removeEventListener('formdata', this.handleFormData, { capture: true });
|
|
|
|
document.removeEventListener('submit', this.handleFormSubmit, { capture: true });
|
2022-01-11 14:18:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handleFormData(event: FormDataEvent) {
|
2022-01-16 05:47:14 +00:00
|
|
|
const disabled = this.options.disabled(this.host);
|
|
|
|
const name = this.options.name(this.host);
|
|
|
|
const value = this.options.value(this.host);
|
2022-01-11 14:18:20 +00:00
|
|
|
|
2022-01-16 05:47:14 +00:00
|
|
|
if (!disabled && typeof name === 'string' && typeof value !== 'undefined') {
|
2022-01-11 14:18:20 +00:00
|
|
|
if (Array.isArray(value)) {
|
2022-01-16 05:47:14 +00:00
|
|
|
(value as unknown[]).forEach(val => {
|
|
|
|
event.formData.append(name, (val as string | number | boolean).toString());
|
|
|
|
});
|
2022-01-11 14:18:20 +00:00
|
|
|
} else {
|
2022-01-16 05:47:14 +00:00
|
|
|
event.formData.append(name, (value as string | number | boolean).toString());
|
2022-01-11 14:18:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleFormSubmit(event: Event) {
|
2022-04-11 14:36:16 +00:00
|
|
|
const form = this.options.form(this.host);
|
2022-01-16 05:47:14 +00:00
|
|
|
const disabled = this.options.disabled(this.host);
|
|
|
|
const reportValidity = this.options.reportValidity;
|
|
|
|
|
2022-04-11 14:36:16 +00:00
|
|
|
if (event.target === form && !disabled && !form?.noValidate && !reportValidity(this.host)) {
|
2022-01-11 14:18:20 +00:00
|
|
|
event.preventDefault();
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-08 14:14:59 +00:00
|
|
|
/** Submits the form, triggering validation and form data injection. */
|
2022-02-28 14:59:32 +00:00
|
|
|
submit(submitter?: HTMLInputElement | SlButton) {
|
2022-04-11 14:36:16 +00:00
|
|
|
const form = this.options.form(this.host);
|
|
|
|
|
|
|
|
if (form) {
|
|
|
|
// Calling form.submit() bypasses the submit event and constraint validation. To prevent this, we can inject a
|
|
|
|
// native submit button into the form, "click" it, then remove it to simulate a standard form submission.
|
2022-02-28 14:59:32 +00:00
|
|
|
const button = document.createElement('button');
|
2022-01-11 14:18:20 +00:00
|
|
|
button.type = 'submit';
|
|
|
|
button.style.position = 'absolute';
|
|
|
|
button.style.width = '0';
|
|
|
|
button.style.height = '0';
|
|
|
|
button.style.clipPath = 'inset(50%)';
|
|
|
|
button.style.overflow = 'hidden';
|
|
|
|
button.style.whiteSpace = 'nowrap';
|
2022-02-28 14:59:32 +00:00
|
|
|
|
2022-03-11 16:28:34 +00:00
|
|
|
// Pass form attributes through to the temporary button
|
2022-02-28 14:59:32 +00:00
|
|
|
if (submitter) {
|
2022-03-11 16:28:34 +00:00
|
|
|
['formaction', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => {
|
|
|
|
if (submitter.hasAttribute(attr)) {
|
|
|
|
button.setAttribute(attr, submitter.getAttribute(attr)!);
|
|
|
|
}
|
|
|
|
});
|
2022-02-28 14:59:32 +00:00
|
|
|
}
|
|
|
|
|
2022-04-11 14:36:16 +00:00
|
|
|
form.append(button);
|
2022-01-11 14:18:20 +00:00
|
|
|
button.click();
|
|
|
|
button.remove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|