shoelace/src/internal/form-control.ts

204 wiersze
6.6 KiB
TypeScript
Czysty Zwykły widok Historia

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';
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
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
/** The size of the form control */
size: 'small' | 'medium' | 'large';
2020-12-23 20:47:13 +00:00
/** The label id, used to map the label to the input */
labelId?: string;
2020-12-23 20:47:13 +00:00
/** The label text (if the label slot isn't used) */
label?: string;
2020-12-23 20:47:13 +00:00
/** Whether or not a label slot has been provided. */
hasLabelSlot?: boolean;
2020-12-23 20:47:13 +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
/** The help text (if the help-text slot isn't used) */
helpText?: string;
2020-12-23 20:47:13 +00:00
/** Whether or not a help text slot has been provided. */
hasHelpTextSlot?: boolean;
2020-12-23 20:47:13 +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"
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"
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
};
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;
}