2022-01-11 14:18:20 +00:00
|
|
|
import { html, ReactiveController, ReactiveControllerHost, TemplateResult } from 'lit';
|
2021-09-29 12:40:26 +00:00
|
|
|
import { classMap } from 'lit/directives/class-map.js';
|
|
|
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
2022-01-11 14:18:20 +00:00
|
|
|
import './formdata-event-polyfill';
|
|
|
|
|
|
|
|
export interface FormSubmitControllerOptions {
|
|
|
|
/** A function that returns the form containing the form control. */
|
|
|
|
form: (input: unknown) => HTMLFormElement;
|
|
|
|
/** 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. */
|
|
|
|
value: (input: unknown) => any;
|
|
|
|
/** 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;
|
|
|
|
form?: HTMLFormElement;
|
|
|
|
options?: FormSubmitControllerOptions;
|
|
|
|
|
|
|
|
constructor(host: ReactiveControllerHost & Element, options?: FormSubmitControllerOptions) {
|
|
|
|
(this.host = host).addController(this);
|
|
|
|
this.options = Object.assign(
|
|
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
options
|
|
|
|
);
|
|
|
|
this.handleFormData = this.handleFormData.bind(this);
|
|
|
|
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
hostConnected() {
|
|
|
|
this.form = this.options?.form(this.host);
|
|
|
|
|
|
|
|
if (this.form) {
|
|
|
|
this.form.addEventListener('formdata', this.handleFormData);
|
|
|
|
this.form.addEventListener('submit', this.handleFormSubmit);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hostDisconnected() {
|
|
|
|
if (this.form) {
|
|
|
|
this.form.removeEventListener('formdata', this.handleFormData);
|
|
|
|
this.form.removeEventListener('submit', this.handleFormSubmit);
|
|
|
|
this.form = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleFormData(event: FormDataEvent) {
|
|
|
|
const disabled = this.options?.disabled(this.host);
|
|
|
|
const name = this.options?.name(this.host);
|
|
|
|
const value = this.options?.value(this.host);
|
|
|
|
|
|
|
|
if (!disabled && name && value !== undefined) {
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
value.map(val => event.formData.append(name, val));
|
|
|
|
} else {
|
|
|
|
event.formData.append(name, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleFormSubmit(event: Event) {
|
|
|
|
const form = this.form;
|
|
|
|
const disabled = this.options?.disabled(this.host);
|
|
|
|
const reportValidity = this.options?.reportValidity;
|
|
|
|
|
|
|
|
if (form && !form.noValidate && !disabled && reportValidity && !reportValidity(this.host)) {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
submit() {
|
|
|
|
// Calling form.submit() seems to bypass the submit event and constraint validation. Instead, we can inject a
|
|
|
|
// native submit button into the form, click it, then remove it to simulate a standard form submission.
|
|
|
|
const button = document.createElement('button');
|
|
|
|
if (this.form) {
|
|
|
|
button.type = 'submit';
|
|
|
|
button.style.position = 'absolute';
|
|
|
|
button.style.width = '0';
|
|
|
|
button.style.height = '0';
|
|
|
|
button.style.clip = 'rect(0 0 0 0)';
|
|
|
|
button.style.clipPath = 'inset(50%)';
|
|
|
|
button.style.overflow = 'hidden';
|
|
|
|
button.style.whiteSpace = 'nowrap';
|
|
|
|
this.form.append(button);
|
|
|
|
button.click();
|
|
|
|
button.remove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-12-23 20:47:13 +00:00
|
|
|
|
2021-04-05 21:13:17 +00:00
|
|
|
export const renderFormControl = (
|
|
|
|
props: {
|
|
|
|
/** The input id, used to map the input to the label */
|
|
|
|
inputId: string;
|
2020-12-23 20:47:13 +00:00
|
|
|
|
2021-04-05 21:13:17 +00:00
|
|
|
/** The size of the form control */
|
|
|
|
size: 'small' | 'medium' | 'large';
|
2020-12-23 20:47:13 +00:00
|
|
|
|
2021-04-05 21:13:17 +00:00
|
|
|
/** The label id, used to map the label to the input */
|
|
|
|
labelId?: string;
|
2020-12-23 20:47:13 +00:00
|
|
|
|
2021-04-05 21:13:17 +00:00
|
|
|
/** The label text (if the label slot isn't used) */
|
|
|
|
label?: string;
|
2020-12-23 20:47:13 +00:00
|
|
|
|
2021-04-05 21:13:17 +00:00
|
|
|
/** Whether or not a label slot has been provided. */
|
|
|
|
hasLabelSlot?: boolean;
|
2020-12-23 20:47:13 +00:00
|
|
|
|
2021-04-05 21:13:17 +00:00
|
|
|
/** The help text id, used to map the input to the help text */
|
|
|
|
helpTextId?: string;
|
2020-12-23 20:47:13 +00:00
|
|
|
|
2021-04-05 21:13:17 +00:00
|
|
|
/** The help text (if the help-text slot isn't used) */
|
|
|
|
helpText?: string;
|
2020-12-23 20:47:13 +00:00
|
|
|
|
2021-04-05 21:13:17 +00:00
|
|
|
/** Whether or not a help text slot has been provided. */
|
|
|
|
hasHelpTextSlot?: boolean;
|
2020-12-23 20:47:13 +00:00
|
|
|
|
2021-04-05 21:13:17 +00:00
|
|
|
/** A function that gets called when the label is clicked. */
|
|
|
|
onLabelClick?: (event: MouseEvent) => void;
|
|
|
|
},
|
|
|
|
input: TemplateResult
|
|
|
|
) => {
|
2021-03-06 17:01:39 +00:00
|
|
|
const hasLabel = props.label ? true : !!props.hasLabelSlot;
|
|
|
|
const hasHelpText = props.helpText ? true : !!props.hasHelpTextSlot;
|
2020-12-23 20:47:13 +00:00
|
|
|
|
2021-02-26 14:09:13 +00:00
|
|
|
return html`
|
2020-12-23 20:47:13 +00:00
|
|
|
<div
|
|
|
|
part="form-control"
|
2021-02-26 14:09:13 +00:00
|
|
|
class=${classMap({
|
2020-12-23 20:47:13 +00:00
|
|
|
'form-control': true,
|
|
|
|
'form-control--small': props.size === 'small',
|
|
|
|
'form-control--medium': props.size === 'medium',
|
|
|
|
'form-control--large': props.size === 'large',
|
|
|
|
'form-control--has-label': hasLabel,
|
|
|
|
'form-control--has-help-text': hasHelpText
|
2021-02-26 14:09:13 +00:00
|
|
|
})}
|
2020-12-23 20:47:13 +00:00
|
|
|
>
|
|
|
|
<label
|
|
|
|
part="label"
|
2021-04-05 21:13:17 +00:00
|
|
|
id=${ifDefined(props.labelId)}
|
2020-12-23 20:47:13 +00:00
|
|
|
class="form-control__label"
|
2021-02-26 14:09:13 +00:00
|
|
|
for=${props.inputId}
|
|
|
|
aria-hidden=${hasLabel ? 'false' : 'true'}
|
2021-03-06 17:01:39 +00:00
|
|
|
@click=${(event: MouseEvent) => (props.onLabelClick ? props.onLabelClick(event) : null)}
|
2020-12-23 20:47:13 +00:00
|
|
|
>
|
2021-02-26 14:09:13 +00:00
|
|
|
<slot name="label">${props.label}</slot>
|
2020-12-23 20:47:13 +00:00
|
|
|
</label>
|
|
|
|
|
2021-02-26 14:09:13 +00:00
|
|
|
<div class="form-control__input">${html`${input}`}</div>
|
2020-12-23 20:47:13 +00:00
|
|
|
|
|
|
|
<div
|
|
|
|
part="help-text"
|
2021-04-05 21:13:17 +00:00
|
|
|
id=${ifDefined(props.helpTextId)}
|
2020-12-23 20:47:13 +00:00
|
|
|
class="form-control__help-text"
|
2021-02-26 14:09:13 +00:00
|
|
|
aria-hidden=${hasHelpText ? 'false' : 'true'}
|
2020-12-23 20:47:13 +00:00
|
|
|
>
|
2021-02-26 14:09:13 +00:00
|
|
|
<slot name="help-text">${props.helpText}</slot>
|
2020-12-23 20:47:13 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
2021-02-26 14:09:13 +00:00
|
|
|
`;
|
2020-12-23 20:47:13 +00:00
|
|
|
};
|
2021-04-05 21:13:17 +00:00
|
|
|
|
|
|
|
export function getLabelledBy(props: {
|
|
|
|
/** The label id, used to map the label to the input */
|
|
|
|
labelId: string;
|
|
|
|
|
|
|
|
/** The label text (if the label slot isn't used) */
|
|
|
|
label: string;
|
|
|
|
|
|
|
|
/** Whether or not a label slot has been provided. */
|
|
|
|
hasLabelSlot: boolean;
|
|
|
|
|
|
|
|
/** The help text id, used to map the input to the help text */
|
|
|
|
helpTextId: string;
|
|
|
|
|
|
|
|
/** The help text (if the help-text slot isn't used) */
|
|
|
|
helpText: string;
|
|
|
|
|
|
|
|
/** Whether or not a help text slot has been provided. */
|
|
|
|
hasHelpTextSlot: boolean;
|
|
|
|
}) {
|
|
|
|
const labelledBy = [
|
|
|
|
props.label || props.hasLabelSlot ? props.labelId : '',
|
|
|
|
props.helpText || props.hasHelpTextSlot ? props.helpTextId : ''
|
|
|
|
].filter(val => val);
|
|
|
|
|
|
|
|
return labelledBy.join(' ') || undefined;
|
|
|
|
}
|