From 1ff73752476288b8fdddc4fe4a3060a07e5fa569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 17 Apr 2024 15:36:29 +0200 Subject: [PATCH] align with newest code from backoffice --- .../uui-base/lib/mixins/FormControlMixin.ts | 179 ++++++++++++------ 1 file changed, 116 insertions(+), 63 deletions(-) diff --git a/packages/uui-base/lib/mixins/FormControlMixin.ts b/packages/uui-base/lib/mixins/FormControlMixin.ts index fb3e42e3d..516730d10 100644 --- a/packages/uui-base/lib/mixins/FormControlMixin.ts +++ b/packages/uui-base/lib/mixins/FormControlMixin.ts @@ -3,12 +3,34 @@ import { property } from 'lit/decorators.js'; import { UUIFormControlEvent } from '../events'; -type Constructor = new (...args: any[]) => T; +type HTMLElementConstructor = new (...args: any[]) => T; type NativeFormControlElement = HTMLInputElement; // Eventually use a specific interface or list multiple options like appending these types: ... | HTMLTextAreaElement | HTMLSelectElement -// TODO: make it possible to define FormDataEntryValue type. -// TODO: Prefix with UUI +/* FlagTypes type options originate from: + * https://developer.mozilla.org/en-US/docs/Web/API/ValidityState + * */ +type FlagTypes = + | 'badInput' + | 'customError' + | 'patternMismatch' + | 'rangeOverflow' + | 'rangeUnderflow' + | 'stepMismatch' + | 'tooLong' + | 'tooShort' + | 'typeMismatch' + | 'valueMissing' + | 'badInput' + | 'valid'; + +// Acceptable as an internal interface/type, BUT if exposed externally this should be turned into a public class in a separate file. +interface UUIFormControlValidatorConfig { + flagKey: FlagTypes; + getMessageMethod: () => string; + checkMethod: () => boolean; +} + export declare abstract class UUIFormControlMixinInterface< ValueType, DefaultValueType, @@ -25,7 +47,8 @@ export declare abstract class UUIFormControlMixinInterface< get validity(): ValidityState; public setCustomValidity(error: string): void; public submit(): void; - protected abstract getFormElement(): HTMLElement | undefined; + protected abstract getFormElement(): HTMLElement | undefined | null; // allows for null as it makes it simpler to just implement a querySelector as that might return null. [NL] + focusFirstInvalidElement(): void; protected addValidator: ( flagKey: FlagTypes, getMessageMethod: () => string, @@ -39,30 +62,6 @@ export declare abstract class UUIFormControlMixinInterface< errorMessage: string; } -/* FlagTypes type options originate from: - * https://developer.mozilla.org/en-US/docs/Web/API/ValidityState - * */ -type FlagTypes = - | 'badInput' - | 'customError' - | 'patternMismatch' - | 'rangeOverflow' - | 'rangeUnderflow' - | 'stepMismatch' - | 'tooLong' - | 'tooShort' - | 'typeMismatch' - | 'valueMissing' - | 'badInput' - | 'valid'; - -// Acceptable as an internal interface/type, BUT if exposed externally this should be turned into a public class in a separate file. -interface Validator { - flagKey: FlagTypes; - getMessageMethod: () => string; - checkMethod: () => boolean; -} - /** * The mixin allows a custom element to participate in HTML forms. * @@ -71,7 +70,8 @@ interface Validator { */ export const UUIFormControlMixin = < ValueType = FormDataEntryValue | FormData, - T extends Constructor = typeof LitElement, + T extends + HTMLElementConstructor = HTMLElementConstructor, DefaultValueType = undefined, >( superClass: T, @@ -111,7 +111,6 @@ export const UUIFormControlMixin = < this.#value = newValue; if ( 'ElementInternals' in window && - //@ts-ignore 'setFormValue' in window.ElementInternals.prototype ) { this._internals.setFormValue((this.#value as any) ?? null); @@ -120,16 +119,25 @@ export const UUIFormControlMixin = < } // Validation - private _validityState: any = {}; + #validity: any = {}; /** * Determines wether the form control has been touched or interacted with, this determines wether the validation-status of this form control should be made visible. * @type {boolean} * @attr - * @default false + * @default true */ @property({ type: Boolean, reflect: true }) - pristine: boolean = true; + public set pristine(value: boolean) { + if (this._pristine !== value) { + this._pristine = value; + this.#dispatchValidationState(); + } + } + public get pristine(): boolean { + return this._pristine; + } + private _pristine: boolean = true; /** * Apply validation rule for requiring a value of this form control. @@ -161,9 +169,9 @@ export const UUIFormControlMixin = < errorMessage = 'This field is invalid'; #value: ValueType | DefaultValueType = defaultValue; - _internals: ElementInternals; + protected _internals: ElementInternals; #form: HTMLFormElement | null = null; - #validators: Validator[] = []; + #validators: UUIFormControlValidatorConfig[] = []; #formCtrlElements: NativeFormControlElement[] = []; constructor(...args: any[]) { @@ -183,6 +191,7 @@ export const UUIFormControlMixin = < this.addEventListener('blur', () => { this.pristine = false; + this.checkValidity(); }); } @@ -204,6 +213,26 @@ export const UUIFormControlMixin = < */ protected abstract getFormElement(): HTMLElement | undefined; + /** + * Focus first element that is invalid. + * @method focusFirstInvalidElement + * @returns {HTMLElement | undefined} + */ + focusFirstInvalidElement() { + const firstInvalid = this.#formCtrlElements.find( + el => el.validity.valid === false, + ); + if (firstInvalid) { + if ('focusFirstInvalidElement' in firstInvalid) { + (firstInvalid as any).focusFirstInvalidElement(); + } else { + firstInvalid.focus(); + } + } else { + this.focus(); + } + } + disconnectedCallback(): void { super.disconnectedCallback(); this.#removeFormListeners(); @@ -233,7 +262,7 @@ export const UUIFormControlMixin = < flagKey: FlagTypes, getMessageMethod: () => string, checkMethod: () => boolean, - ): Validator { + ): UUIFormControlValidatorConfig { const obj = { flagKey: flagKey, getMessageMethod: getMessageMethod, @@ -243,7 +272,7 @@ export const UUIFormControlMixin = < return obj; } - protected removeValidator(validator: Validator) { + protected removeValidator(validator: UUIFormControlValidatorConfig) { const index = this.#validators.indexOf(validator); if (index !== -1) { this.#validators.splice(index, 1); @@ -257,9 +286,21 @@ export const UUIFormControlMixin = < */ protected addFormControlElement(element: NativeFormControlElement) { this.#formCtrlElements.push(element); + element.addEventListener(UUIFormControlEvent.INVALID, () => { + this._runValidators(); + }); + element.addEventListener(UUIFormControlEvent.VALID, () => { + this._runValidators(); + }); + // If we are in validationMode/'touched'/not-pristine then we need to validate this newly added control. [NL] + if (this._pristine === false) { + element.checkValidity(); + // I think we could just execute validators for the new control, but now lets just run al of it again. [NL] + this._runValidators(); + } } - private _customValidityObject?: Validator; + private _customValidityObject?: UUIFormControlValidatorConfig; /** * @method setCustomValidity @@ -291,46 +332,55 @@ export const UUIFormControlMixin = < * Such are mainly properties that are not declared as a Lit state and or Lit property. */ protected _runValidators() { - this._validityState = {}; + this.#validity = {}; + const messages: Set = new Set(); + let innerFormControlEl: NativeFormControlElement | undefined = undefined; - // Loop through inner native form controls to adapt their validityState. + // Loop through inner native form controls to adapt their validityState. [NL] this.#formCtrlElements.forEach(formCtrlEl => { - for (const key in formCtrlEl.validity) { - if (key !== 'valid' && (formCtrlEl.validity as any)[key]) { - (this as any)._validityState[key] = true; - this._internals.setValidity( - (this as any)._validityState, - formCtrlEl.validationMessage, - formCtrlEl, - ); + let key: keyof ValidityState; + for (key in formCtrlEl.validity) { + if (key !== 'valid' && formCtrlEl.validity[key]) { + this.#validity[key] = true; + messages.add(formCtrlEl.validationMessage); + innerFormControlEl ??= formCtrlEl; } } }); - // Loop through custom validators, currently its intentional to have them overwritten native validity. but might need to be reconsidered (This current way enables to overwrite with custom messages) + // Loop through custom validators, currently its intentional to have them overwritten native validity. but might need to be reconsidered (This current way enables to overwrite with custom messages) [NL] this.#validators.forEach(validator => { if (validator.checkMethod()) { - this._validityState[validator.flagKey] = true; - this._internals.setValidity( - this._validityState, - validator.getMessageMethod(), - this.getFormElement(), - ); + this.#validity[validator.flagKey] = true; + messages.add(validator.getMessageMethod()); } }); - const hasError = Object.values(this._validityState).includes(true); + const hasError = Object.values(this.#validity).includes(true); // https://developer.mozilla.org/en-US/docs/Web/API/ValidityState#valid - this._validityState.valid = !hasError; + this.#validity.valid = !hasError; + + // Transfer the new validityState to the ElementInternals. [NL] + this._internals.setValidity( + this.#validity, + // Turn messages into an array and join them with a comma. [NL]: + [...messages].join(', '), + innerFormControlEl ?? this.getFormElement() ?? undefined, + ); - if (hasError) { + this.#dispatchValidationState(); + } + + #dispatchValidationState() { + // Do not fire validation events unless we are not pristine/'untouched'/not-in-validation-mode. [NL] + if (this._pristine === true) return; + if (this.#validity.valid) { + this.dispatchEvent(new UUIFormControlEvent(UUIFormControlEvent.VALID)); + } else { this.dispatchEvent( new UUIFormControlEvent(UUIFormControlEvent.INVALID), ); - } else { - this._internals.setValidity({}); - this.dispatchEvent(new UUIFormControlEvent(UUIFormControlEvent.VALID)); } } @@ -371,6 +421,9 @@ export const UUIFormControlMixin = < } public checkValidity() { + this.pristine = false; + this._runValidators(); + for (const key in this.#formCtrlElements) { if (this.#formCtrlElements[key].checkValidity() === false) { return false; @@ -382,14 +435,14 @@ export const UUIFormControlMixin = < // https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/validity public get validity(): ValidityState { - return this._validityState; + return this.#validity; } get validationMessage() { return this._internals?.validationMessage; } } - return UUIFormControlMixinClass as unknown as Constructor< + return UUIFormControlMixinClass as unknown as HTMLElementConstructor< UUIFormControlMixinInterface > & T;