rename FormSubmitController; remove this.invalid

This commit is contained in:
Cory LaViska
2023-01-10 13:24:06 -05:00
parent acef0da2c1
commit e2d2f5d670
12 changed files with 122 additions and 124 deletions

View File

@@ -19,6 +19,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti
- Fixed a bug in `<sl-color-picker>` that caused selected colors to be wrong due to incorrect HSV calculations
- Fixed a bug in `<sl-radio-button>` that caused the checked button's right border to be incorrect [#1110](https://github.com/shoelace-style/shoelace/issues/1110)
- Fixed a bug in `<sl-spinner>` that caused the animation to stop working correctly in Safari [#1121](https://github.com/shoelace-style/shoelace/issues/1121)
- Refactored the `ShoelaceFormControl` interface to remove the `invalid` property, allowing a more intuitive API for controlling validation internally
- Renamed the internal `FormSubmitController` to `FormControlController` to better reflect what it's used for
## 2.0.0-beta.88

View File

@@ -2,7 +2,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { html, literal } from 'lit/static-html.js';
import { FormSubmitController } from '../../internal/form';
import { FormControlController } from '../../internal/form';
import ShoelaceElement from '../../internal/shoelace-element';
import { HasSlotController } from '../../internal/slot';
import { watch } from '../../internal/watch';
@@ -39,7 +39,7 @@ import type { CSSResultGroup } from 'lit';
export default class SlButton extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
private readonly formSubmitController = new FormSubmitController(this, {
private readonly formControlController = new FormControlController(this, {
form: input => {
// Buttons support a form attribute that points to an arbitrary form, so if this attribute it set we need to query
// the form from the same root using its id
@@ -141,7 +141,7 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
firstUpdated() {
if (this.isButton()) {
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
this.formControlController.updateValidity();
}
}
@@ -163,11 +163,11 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
}
if (this.type === 'submit') {
this.formSubmitController.submit(this);
this.formControlController.submit(this);
}
if (this.type === 'reset') {
this.formSubmitController.reset(this);
this.formControlController.reset(this);
}
}
@@ -181,10 +181,9 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
// Disabled form controls are always valid, so we need to recheck validity when the state changes
if (this.isButton()) {
this.button.disabled = this.disabled;
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
// Disabled form controls are always valid
this.formControlController.setValidity(this.disabled);
}
}
@@ -225,7 +224,7 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
setCustomValidity(message: string) {
if (this.isButton()) {
(this.button as HTMLButtonElement).setCustomValidity(message);
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
this.formControlController.updateValidity();
}
}

View File

@@ -4,7 +4,7 @@ import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { defaultValue } from '../../internal/default-value';
import { FormSubmitController } from '../../internal/form';
import { FormControlController } from '../../internal/form';
import ShoelaceElement from '../../internal/shoelace-element';
import { watch } from '../../internal/watch';
import '../icon/icon';
@@ -39,8 +39,7 @@ import type { CSSResultGroup } from 'lit';
export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
// @ts-expect-error - Controller is currently unused
private readonly formSubmitController = new FormSubmitController(this, {
private readonly formControlController = new FormControlController(this, {
value: (control: SlCheckbox) => (control.checked ? control.value || 'on' : undefined),
defaultValue: (control: SlCheckbox) => control.defaultChecked,
setValue: (control: SlCheckbox, checked: boolean) => (control.checked = checked)
@@ -49,7 +48,6 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
@query('input[type="checkbox"]') input: HTMLInputElement;
@state() private hasFocus = false;
@state() invalid = false;
@property() title = ''; // make reactive to pass through
@@ -81,7 +79,7 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
@defaultValue('checked') defaultChecked = false;
firstUpdated() {
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
private handleClick() {
@@ -106,16 +104,15 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
// Disabled form controls are always valid, so we need to recheck validity when the state changes
this.input.disabled = this.disabled;
this.invalid = !this.checkValidity();
// Disabled form controls are always valid
this.formControlController.setValidity(this.disabled);
}
@watch(['checked', 'indeterminate'], { waitUntilFirstUpdate: true })
handleStateChange() {
this.input.checked = this.checked; // force a sync update
this.input.indeterminate = this.indeterminate; // force a sync update
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
/** Simulates a click on the checkbox. */
@@ -149,7 +146,7 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
*/
setCustomValidity(message: string) {
this.input.setCustomValidity(message);
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
render() {

View File

@@ -6,7 +6,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
import { styleMap } from 'lit/directives/style-map.js';
import { defaultValue } from '../../internal/default-value';
import { drag } from '../../internal/drag';
import { FormSubmitController } from '../../internal/form';
import { FormControlController } from '../../internal/form';
import { clamp } from '../../internal/math';
import ShoelaceElement from '../../internal/shoelace-element';
import { watch } from '../../internal/watch';
@@ -88,8 +88,7 @@ declare const EyeDropper: EyeDropperConstructor;
export default class SlColorPicker extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
// @ts-expect-error - Controller is currently unused
private readonly formSubmitController = new FormSubmitController(this);
private readonly formControlController = new FormControlController(this);
private isSafeValue = false;
private lastValueEmitted: string;
private readonly localize = new LocalizeController(this);
@@ -105,7 +104,6 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
@state() private saturation = 100;
@state() private brightness = 100;
@state() private alpha = 100;
@state() invalid = false;
/**
* The current value of the color picker. The value's format will vary based the `format` attribute. To get the value
@@ -679,7 +677,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
/** Checks for validity and shows the browser's validation message if the control is invalid. */
reportValidity() {
if (!this.inline && this.input.invalid) {
if (!this.inline && !this.checkValidity()) {
// If the input is inline and invalid, show the dropdown so the browser can focus on it
this.dropdown.show();
this.addEventListener('sl-after-show', () => this.input.reportValidity(), { once: true });
@@ -692,7 +690,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
setCustomValidity(message: string) {
this.input.setCustomValidity(message);
this.invalid = this.input.invalid;
this.formControlController.updateValidity();
}
render() {

View File

@@ -4,7 +4,7 @@ import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { defaultValue } from '../../internal/default-value';
import { FormSubmitController } from '../../internal/form';
import { FormControlController } from '../../internal/form';
import ShoelaceElement from '../../internal/shoelace-element';
import { HasSlotController } from '../../internal/slot';
import { watch } from '../../internal/watch';
@@ -63,14 +63,13 @@ const isFirefox = isChromium ? false : navigator.userAgent.includes('Firefox');
export default class SlInput extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
private readonly formSubmitController = new FormSubmitController(this);
private readonly formControlController = new FormControlController(this);
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
private readonly localize = new LocalizeController(this);
@query('.input__control') input: HTMLInputElement;
@state() private hasFocus = false;
@state() invalid = false;
@property() title = ''; // make reactive to pass through
/**
@@ -220,7 +219,7 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
}
firstUpdated() {
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
private handleBlur() {
@@ -250,12 +249,12 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
private handleInput() {
this.value = this.input.value;
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
this.emit('sl-input');
}
private handleInvalid() {
this.invalid = true;
this.formControlController.setValidity(false);
}
private handleKeyDown(event: KeyboardEvent) {
@@ -272,7 +271,7 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
// See https://github.com/shoelace-style/shoelace/pull/988
//
if (!event.defaultPrevented && !event.isComposing) {
this.formSubmitController.submit();
this.formControlController.submit();
}
});
}
@@ -284,9 +283,8 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
// Disabled form controls are always valid, so we need to recheck validity when the state changes
this.input.disabled = this.disabled;
this.invalid = !this.checkValidity();
// Disabled form controls are always valid
this.formControlController.setValidity(this.disabled);
}
@watch('step', { waitUntilFirstUpdate: true })
@@ -294,13 +292,13 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
// If step changes, the value may become invalid so we need to recheck after the update. We set the new step
// imperatively so we don't have to wait for the next render to report the updated validity.
this.input.step = String(this.step);
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
@watch('value', { waitUntilFirstUpdate: true })
async handleValueChange() {
await this.updateComplete;
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
/** Sets focus on the input. */
@@ -378,7 +376,7 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
setCustomValidity(message: string) {
this.input.setCustomValidity(message);
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
render() {
@@ -459,7 +457,6 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
enterkeyhint=${ifDefined(this.enterkeyhint)}
inputmode=${ifDefined(this.inputmode)}
aria-describedby="help-text"
aria-invalid=${this.invalid ? 'true' : 'false'}
@change=${this.handleChange}
@input=${this.handleInput}
@invalid=${this.handleInvalid}

View File

@@ -1,7 +1,7 @@
import { html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { FormSubmitController } from '../../internal/form';
import { FormControlController } from '../../internal/form';
import ShoelaceElement from '../../internal/shoelace-element';
import { HasSlotController } from '../../internal/slot';
import { watch } from '../../internal/watch';
@@ -38,7 +38,7 @@ import type { CSSResultGroup } from 'lit';
export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
protected readonly formSubmitController = new FormSubmitController(this, {
protected readonly formControlController = new FormControlController(this, {
defaultValue: control => control.defaultValue
});
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
@@ -50,7 +50,6 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
@state() private errorMessage = '';
@state() private customErrorMessage = '';
@state() defaultValue = '';
@state() invalid = false;
/**
* The radio group's label. Required for proper accessibility. If you need to display HTML, use the `label` slot
@@ -76,7 +75,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
}
firstUpdated() {
this.invalid = !this.validity.valid;
this.formControlController.updateValidity();
}
private getAllRadios() {
@@ -191,7 +190,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
private updateCheckedRadio() {
const radios = this.getAllRadios();
radios.forEach(radio => (radio.checked = radio.value === this.value));
this.invalid = !this.validity.valid;
this.formControlController.setValidity(this.validity.valid);
}
@watch('value')
@@ -212,9 +211,9 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
this.errorMessage = message;
if (!message) {
this.invalid = false;
this.formControlController.setValidity(true);
} else {
this.invalid = true;
this.formControlController.setValidity(false);
this.input.setCustomValidity(message);
}
}
@@ -243,13 +242,13 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
const validity = this.validity;
this.errorMessage = this.customErrorMessage || validity.valid ? '' : this.input.validationMessage;
this.invalid = !validity.valid;
this.formControlController.setValidity(validity.valid);
if (!validity.valid) {
this.showNativeErrorMessage();
}
return !this.invalid;
return validity.valid;
}
render() {

View File

@@ -4,7 +4,7 @@ import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { defaultValue } from '../../internal/default-value';
import { FormSubmitController } from '../../internal/form';
import { FormControlController } from '../../internal/form';
import ShoelaceElement from '../../internal/shoelace-element';
import { HasSlotController } from '../../internal/slot';
import { watch } from '../../internal/watch';
@@ -46,8 +46,7 @@ import type { CSSResultGroup } from 'lit';
export default class SlRange extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
// @ts-expect-error - Controller is currently unused
private readonly formSubmitController = new FormSubmitController(this);
private readonly formControlController = new FormControlController(this);
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
private readonly localize = new LocalizeController(this);
private resizeObserver: ResizeObserver;
@@ -57,7 +56,6 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
@state() private hasFocus = false;
@state() private hasTooltip = false;
@state() invalid = false;
@property() title = ''; // make reactive to pass through
/** The name of the range, submitted as a name/value pair with form data. */
@@ -175,7 +173,7 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
@watch('value', { waitUntilFirstUpdate: true })
handleValueChange() {
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
// The value may have constraints, so we set the native control's value and sync it back to ensure it adhere's to
// min, max, and step properly
@@ -187,9 +185,8 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
// Disabled form controls are always valid, so we need to recheck validity when the state changes
this.input.disabled = this.disabled;
this.invalid = !this.checkValidity();
// Disabled form controls are always valid
this.formControlController.setValidity(this.disabled);
}
@watch('hasTooltip', { waitUntilFirstUpdate: true })
@@ -242,7 +239,7 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
setCustomValidity(message: string) {
this.input.setCustomValidity(message);
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
render() {

View File

@@ -5,7 +5,7 @@ import { scrollIntoView } from 'src/internal/scroll';
import { animateTo, stopAnimations } from '../../internal/animate';
import { defaultValue } from '../../internal/default-value';
import { waitForEvent } from '../../internal/event';
import { FormSubmitController } from '../../internal/form';
import { FormControlController } from '../../internal/form';
import ShoelaceElement from '../../internal/shoelace-element';
import { HasSlotController } from '../../internal/slot';
import { watch } from '../../internal/watch';
@@ -64,8 +64,7 @@ import type { CSSResultGroup } from 'lit';
export default class SlSelect extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
// @ts-expect-error - Controller is currently unused
private readonly formSubmitController = new FormSubmitController(this);
private readonly formControlController = new FormControlController(this);
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
private readonly localize = new LocalizeController(this);
private typeToSelectString = '';
@@ -81,7 +80,6 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
@state() displayLabel = '';
@state() currentOption: SlOption;
@state() selectedOptions: SlOption[] = [];
@state() invalid = false;
/** The name of the select, submitted as a name/value pair with form data. */
@property() name = '';
@@ -512,7 +510,9 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
}
// Update validity
this.updateComplete.then(() => (this.invalid = !this.checkValidity()));
this.updateComplete.then(() => {
this.formControlController.updateValidity();
});
}
@watch('disabled', { waitUntilFirstUpdate: true })
@@ -611,7 +611,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
setCustomValidity(message: string) {
this.valueInput.setCustomValidity(message);
this.invalid = !this.valueInput.checkValidity();
this.formControlController.updateValidity();
}
/** Sets focus on the control. */

View File

@@ -4,7 +4,7 @@ import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { defaultValue } from '../../internal/default-value';
import { FormSubmitController } from '../../internal/form';
import { FormControlController } from '../../internal/form';
import ShoelaceElement from '../../internal/shoelace-element';
import { watch } from '../../internal/watch';
import styles from './switch.styles';
@@ -37,8 +37,7 @@ import type { CSSResultGroup } from 'lit';
export default class SlSwitch extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
// @ts-expect-error - Controller is currently unused
private readonly formSubmitController = new FormSubmitController(this, {
private readonly formControlController = new FormControlController(this, {
value: (control: SlSwitch) => (control.checked ? control.value || 'on' : undefined),
defaultValue: (control: SlSwitch) => control.defaultChecked,
setValue: (control: SlSwitch, checked: boolean) => (control.checked = checked)
@@ -47,7 +46,6 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
@query('input[type="checkbox"]') input: HTMLInputElement;
@state() private hasFocus = false;
@state() invalid = false;
@property() title = ''; // make reactive to pass through
/** The name of the switch, submitted as a name/value pair with form data. */
@@ -72,7 +70,7 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
@defaultValue('checked') defaultChecked = false;
firstUpdated() {
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
private handleBlur() {
@@ -113,14 +111,13 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
@watch('checked', { waitUntilFirstUpdate: true })
handleCheckedChange() {
this.input.checked = this.checked; // force a sync update
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
// Disabled form controls are always valid, so we need to recheck validity when the state changes
this.input.disabled = this.disabled;
this.invalid = !this.checkValidity();
// Disabled form controls are always valid
this.formControlController.setValidity(true);
}
/** Simulates a click on the switch. */
@@ -151,7 +148,7 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
setCustomValidity(message: string) {
this.input.setCustomValidity(message);
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
render() {

View File

@@ -4,7 +4,7 @@ import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { defaultValue } from '../../internal/default-value';
import { FormSubmitController } from '../../internal/form';
import { FormControlController } from '../../internal/form';
import ShoelaceElement from '../../internal/shoelace-element';
import { HasSlotController } from '../../internal/slot';
import { watch } from '../../internal/watch';
@@ -37,15 +37,13 @@ import type { CSSResultGroup } from 'lit';
export default class SlTextarea extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
// @ts-expect-error - Controller is currently unused
private readonly formSubmitController = new FormSubmitController(this);
private readonly formControlController = new FormControlController(this);
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
private resizeObserver: ResizeObserver;
@query('.textarea__control') input: HTMLTextAreaElement;
@state() private hasFocus = false;
@state() invalid = false;
@property() title = ''; // make reactive to pass through
/** The name of the textarea, submitted as a name/value pair with form data. */
@@ -139,7 +137,7 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
}
firstUpdated() {
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
disconnectedCallback() {
@@ -179,9 +177,8 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
// Disabled form controls are always valid, so we need to recheck validity when the state changes
this.input.disabled = this.disabled;
this.invalid = !this.checkValidity();
// Disabled form controls are always valid
this.formControlController.setValidity(this.disabled);
}
@watch('rows', { waitUntilFirstUpdate: true })
@@ -192,7 +189,7 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
@watch('value', { waitUntilFirstUpdate: true })
async handleValueChange() {
await this.updateComplete;
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
this.setTextareaHeight();
}
@@ -267,7 +264,7 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
setCustomValidity(message: string) {
this.input.setCustomValidity(message);
this.invalid = !this.checkValidity();
this.formControlController.updateValidity();
}
render() {

View File

@@ -21,7 +21,7 @@ const userInteractedControls: WeakMap<ShoelaceFormControl, boolean> = new WeakMa
//
const reportValidityOverloads: WeakMap<HTMLFormElement, () => boolean> = new WeakMap();
export interface FormSubmitControllerOptions {
export interface FormControlControllerOptions {
/** A function that returns the form containing the form control. */
form: (input: ShoelaceFormControl) => HTMLFormElement | null;
/** A function that returns the form control's name, which will be submitted with the form data. */
@@ -41,12 +41,12 @@ export interface FormSubmitControllerOptions {
setValue: (input: ShoelaceFormControl, value: unknown) => void;
}
export class FormSubmitController implements ReactiveController {
export class FormControlController implements ReactiveController {
host: ShoelaceFormControl & ReactiveControllerHost;
form?: HTMLFormElement | null;
options: FormSubmitControllerOptions;
options: FormControlControllerOptions;
constructor(host: ReactiveControllerHost & ShoelaceFormControl, options?: Partial<FormSubmitControllerOptions>) {
constructor(host: ReactiveControllerHost & ShoelaceFormControl, options?: Partial<FormControlControllerOptions>) {
(this.host = host).addController(this);
this.options = {
form: input => input.closest('form'),
@@ -112,37 +112,12 @@ export class FormSubmitController implements ReactiveController {
}
hostUpdated() {
//
// We're mapping the following "states" to data attributes. In the future, we can use ElementInternals.states to
// create a similar mapping, but instead of [data-invalid] it will look like :--invalid.
//
// See this RFC for more details: https://github.com/shoelace-style/shoelace/issues/1011
//
const host = this.host;
const hasInteracted = Boolean(userInteractedControls.get(host));
const invalid = Boolean(host.invalid);
const required = Boolean(host.required);
if (this.form?.noValidate) {
// Form validation is disabled, remove the attributes
host.removeAttribute('data-required');
host.removeAttribute('data-optional');
host.removeAttribute('data-invalid');
host.removeAttribute('data-valid');
host.removeAttribute('data-user-invalid');
host.removeAttribute('data-user-valid');
} else {
// Form validation is enabled, set the attributes
host.toggleAttribute('data-required', required);
host.toggleAttribute('data-optional', !required);
host.toggleAttribute('data-invalid', invalid);
host.toggleAttribute('data-valid', !invalid);
host.toggleAttribute('data-user-invalid', invalid && hasInteracted);
host.toggleAttribute('data-user-valid', !invalid && hasInteracted);
if (this.host.hasUpdated) {
this.setValidity(this.host.checkValidity());
}
}
handleFormData(event: FormDataEvent) {
private handleFormData(event: FormDataEvent) {
const disabled = this.options.disabled(this.host);
const name = this.options.name(this.host);
const value = this.options.value(this.host);
@@ -162,7 +137,7 @@ export class FormSubmitController implements ReactiveController {
}
}
handleFormSubmit(event: Event) {
private handleFormSubmit(event: Event) {
const disabled = this.options.disabled(this.host);
const reportValidity = this.options.reportValidity;
@@ -179,17 +154,17 @@ export class FormSubmitController implements ReactiveController {
}
}
handleFormReset() {
private handleFormReset() {
this.options.setValue(this.host, this.options.defaultValue(this.host));
this.setUserInteracted(this.host, false);
}
async handleUserInput() {
private async handleUserInput() {
await this.host.updateComplete;
this.setUserInteracted(this.host, true);
}
reportFormValidity() {
private reportFormValidity() {
//
// Shoelace form controls work hard to act like regular form controls. They support the Constraint Validation API
// and its associated methods such as setCustomValidity() and reportValidity(). However, the HTMLFormElement also
@@ -219,12 +194,12 @@ export class FormSubmitController implements ReactiveController {
return true;
}
setUserInteracted(el: ShoelaceFormControl, hasInteracted: boolean) {
private setUserInteracted(el: ShoelaceFormControl, hasInteracted: boolean) {
userInteractedControls.set(el, hasInteracted);
el.requestUpdate();
}
doAction(type: 'submit' | 'reset', invoker?: HTMLInputElement | SlButton) {
private doAction(type: 'submit' | 'reset', invoker?: HTMLInputElement | SlButton) {
if (this.form) {
const button = document.createElement('button');
button.type = type;
@@ -264,4 +239,47 @@ export class FormSubmitController implements ReactiveController {
// native submit button into the form, "click" it, then remove it to simulate a standard form submission.
this.doAction('submit', invoker);
}
/**
* Synchronously sets the form control's validity. Call this when you know the future validity but need to update
* the host element immediately, i.e. before Lit updates the component in the next update.
*/
setValidity(isValid: boolean) {
const host = this.host;
const hasInteracted = Boolean(userInteractedControls.get(host));
const required = Boolean(host.required);
//
// We're mapping the following "states" to data attributes. In the future, we can use ElementInternals.states to
// create a similar mapping, but instead of [data-invalid] it will look like :--invalid.
//
// See this RFC for more details: https://github.com/shoelace-style/shoelace/issues/1011
//
if (this.form?.noValidate) {
// Form validation is disabled, remove the attributes
host.removeAttribute('data-required');
host.removeAttribute('data-optional');
host.removeAttribute('data-invalid');
host.removeAttribute('data-valid');
host.removeAttribute('data-user-invalid');
host.removeAttribute('data-user-valid');
} else {
// Form validation is enabled, set the attributes
host.toggleAttribute('data-required', required);
host.toggleAttribute('data-optional', !required);
host.toggleAttribute('data-invalid', !isValid);
host.toggleAttribute('data-valid', isValid);
host.toggleAttribute('data-user-invalid', !isValid && hasInteracted);
host.toggleAttribute('data-user-valid', isValid && hasInteracted);
}
}
/**
* Updates the form control's validity based on the current value of `host.checkValidity()`. Call this when anything
* that affects constraint validation changes so the component receives the correct validity states.
*/
updateValidity() {
const host = this.host;
this.setValidity(host.checkValidity());
}
}

View File

@@ -39,9 +39,6 @@ export interface ShoelaceFormControl extends ShoelaceElement {
minlength?: number;
maxlength?: number;
// Proprietary validation properties (non-attributes)
invalid: boolean;
// Validation methods
checkValidity: () => boolean;
reportValidity: () => boolean;