Skip to content

Commit

Permalink
align with newest code from backoffice
Browse files Browse the repository at this point in the history
  • Loading branch information
nielslyngsoe committed Apr 17, 2024
1 parent acef523 commit 1ff7375
Showing 1 changed file with 116 additions and 63 deletions.
179 changes: 116 additions & 63 deletions packages/uui-base/lib/mixins/FormControlMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,34 @@ import { property } from 'lit/decorators.js';

import { UUIFormControlEvent } from '../events';

type Constructor<T = {}> = new (...args: any[]) => T;
type HTMLElementConstructor<T = HTMLElement> = 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,
Expand All @@ -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,
Expand All @@ -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.
*
Expand All @@ -71,7 +70,8 @@ interface Validator {
*/
export const UUIFormControlMixin = <
ValueType = FormDataEntryValue | FormData,
T extends Constructor<LitElement> = typeof LitElement,
T extends
HTMLElementConstructor<HTMLElement> = HTMLElementConstructor<HTMLElement>,
DefaultValueType = undefined,
>(
superClass: T,
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand Down Expand Up @@ -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[]) {
Expand All @@ -183,6 +191,7 @@ export const UUIFormControlMixin = <

this.addEventListener('blur', () => {
this.pristine = false;
this.checkValidity();
});
}

Expand All @@ -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();
Expand Down Expand Up @@ -233,7 +262,7 @@ export const UUIFormControlMixin = <
flagKey: FlagTypes,
getMessageMethod: () => string,
checkMethod: () => boolean,
): Validator {
): UUIFormControlValidatorConfig {
const obj = {
flagKey: flagKey,
getMessageMethod: getMessageMethod,
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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<string> = 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));
}
}

Expand Down Expand Up @@ -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;
Expand All @@ -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<ValueType, DefaultValueType>
> &
T;
Expand Down

0 comments on commit 1ff7375

Please sign in to comment.