feat(form-core): allow enums as outcome of a Validator
This commit is contained in:
parent
e457ce73bb
commit
7016a150dc
5 changed files with 341 additions and 183 deletions
5
.changeset/proud-geese-suffer.md
Normal file
5
.changeset/proud-geese-suffer.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@lion/form-core': minor
|
||||
---
|
||||
|
||||
Validation: allow enums as outcome of a Validator
|
||||
|
|
@ -17,6 +17,10 @@ import { FormControlMixin } from '../FormControlMixin.js';
|
|||
/**
|
||||
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidateMixin} ValidateMixin
|
||||
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidationType} ValidationType
|
||||
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidateHost} ValidateHost
|
||||
* @typedef {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} ValidateHostConstructor
|
||||
* @typedef {{validator:Validator; outcome:boolean|string}} ValidationResultEntry
|
||||
* @typedef {{[type:string]: {[validatorName:string]:boolean|string}}} ValidationStates
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -159,7 +163,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
*/
|
||||
this.showsFeedbackFor = [];
|
||||
|
||||
// TODO: [v1] make this fully private (preifix __)?
|
||||
// TODO: [v1] make this fully private (prefix __)?
|
||||
/**
|
||||
* A temporary storage to transition from hasFeedbackFor to showsFeedbackFor
|
||||
* @type {ValidationType[]}
|
||||
|
|
@ -171,7 +175,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
/**
|
||||
* The outcome of a validation 'round'. Keyed by ValidationType and Validator name
|
||||
* @readOnly
|
||||
* @type {Object.<string, Object.<string, boolean>>}
|
||||
* @type {ValidationStates}
|
||||
*/
|
||||
this.validationStates = {};
|
||||
|
||||
|
|
@ -206,44 +210,45 @@ export const ValidateMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* The amount of feedback messages that will visible in LionValidationFeedback
|
||||
* @configurable
|
||||
* @protected
|
||||
*/
|
||||
this._visibleMessagesAmount = 1;
|
||||
|
||||
/**
|
||||
* @type {Validator[]}
|
||||
* @type {ValidationResultEntry[]}
|
||||
* @private
|
||||
*/
|
||||
this.__syncValidationResult = [];
|
||||
|
||||
/**
|
||||
* @type {Validator[]}
|
||||
* @type {ValidationResultEntry[]}
|
||||
* @private
|
||||
*/
|
||||
this.__asyncValidationResult = [];
|
||||
|
||||
/**
|
||||
* Aggregated result from sync Validators, async Validators and ResultValidators
|
||||
* @type {Validator[]}
|
||||
* @type {ValidationResultEntry[]}
|
||||
* @private
|
||||
*/
|
||||
this.__validationResult = [];
|
||||
|
||||
/**
|
||||
* @type {Validator[]}
|
||||
* @type {ValidationResultEntry[]}
|
||||
* @private
|
||||
*/
|
||||
this.__prevValidationResult = [];
|
||||
|
||||
/**
|
||||
* @type {Validator[]}
|
||||
* @type {ValidationResultEntry[]}
|
||||
* @private
|
||||
*/
|
||||
this.__prevShownValidationResult = [];
|
||||
|
||||
/**
|
||||
* The updated children validity affects the validity of the parent. Helper to recompute
|
||||
* validatity of parent FormGroup
|
||||
* validity of parent FormGroup
|
||||
* @private
|
||||
*/
|
||||
this.__childModelValueChanged = false;
|
||||
|
|
@ -384,6 +389,14 @@ export const ValidateMixinImplementation = superclass =>
|
|||
* @desc step a1-3 + b (as explained in `validate()`)
|
||||
*/
|
||||
async __executeValidators() {
|
||||
/**
|
||||
* Allows Application Developer to wait for (async) validation
|
||||
* @example
|
||||
* ```js
|
||||
* await el.validateComplete;
|
||||
* ```
|
||||
* @type {Promise<boolean>}
|
||||
*/
|
||||
this.validateComplete = new Promise(resolve => {
|
||||
this.__validateCompleteResolve = resolve;
|
||||
});
|
||||
|
|
@ -410,7 +423,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
const isEmpty = this.__isEmpty(value);
|
||||
if (isEmpty) {
|
||||
if (requiredValidator) {
|
||||
this.__syncValidationResult = [requiredValidator];
|
||||
this.__syncValidationResult = [{ validator: requiredValidator, outcome: true }];
|
||||
}
|
||||
this.__finishValidation({ source: 'sync' });
|
||||
return;
|
||||
|
|
@ -451,9 +464,12 @@ export const ValidateMixinImplementation = superclass =>
|
|||
*/
|
||||
__executeSyncValidators(syncValidators, value, { hasAsync }) {
|
||||
if (syncValidators.length) {
|
||||
this.__syncValidationResult = syncValidators.filter(v =>
|
||||
v.execute(value, v.param, { node: this }),
|
||||
);
|
||||
this.__syncValidationResult = syncValidators
|
||||
.map(v => ({
|
||||
validator: v,
|
||||
outcome: /** @type {boolean|string} */ (v.execute(value, v.param, { node: this })),
|
||||
}))
|
||||
.filter(v => Boolean(v.outcome));
|
||||
}
|
||||
this.__finishValidation({ source: 'sync', hasAsync });
|
||||
}
|
||||
|
|
@ -468,10 +484,15 @@ export const ValidateMixinImplementation = superclass =>
|
|||
if (asyncValidators.length) {
|
||||
this.isPending = true;
|
||||
const resultPromises = asyncValidators.map(v => v.execute(value, v.param, { node: this }));
|
||||
const booleanResults = await Promise.all(resultPromises);
|
||||
this.__asyncValidationResult = booleanResults
|
||||
.map((r, i) => asyncValidators[i]) // Create an array of Validators
|
||||
.filter((v, i) => booleanResults[i]); // Only leave the ones returning true
|
||||
const asyncExecutionResults = await Promise.all(resultPromises);
|
||||
|
||||
this.__asyncValidationResult = asyncExecutionResults
|
||||
.map((r, i) => ({
|
||||
validator: asyncValidators[i],
|
||||
outcome: /** @type {boolean|string} */ (asyncExecutionResults[i]),
|
||||
}))
|
||||
.filter(v => Boolean(v.outcome));
|
||||
|
||||
this.__finishValidation({ source: 'async' });
|
||||
this.isPending = false;
|
||||
}
|
||||
|
|
@ -479,7 +500,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* step b (as explained in `validate()`), called by __finishValidation
|
||||
* @param {Validator[]} regularValidationResult result of steps 1-3
|
||||
* @param {{validator:Validator; outcome: boolean|string;}[]} regularValidationResult result of steps 1-3
|
||||
* @private
|
||||
*/
|
||||
__executeResultValidators(regularValidationResult) {
|
||||
|
|
@ -490,13 +511,21 @@ export const ValidateMixinImplementation = superclass =>
|
|||
})
|
||||
);
|
||||
|
||||
return resultValidators.filter(v =>
|
||||
// Map everything to Validator[] for backwards compatibility
|
||||
return resultValidators
|
||||
.map(v => ({
|
||||
validator: v,
|
||||
outcome: /** @type {boolean|string} */ (
|
||||
v.executeOnResults({
|
||||
regularValidationResult,
|
||||
prevValidationResult: this.__prevValidationResult,
|
||||
prevShownValidationResult: this.__prevShownValidationResult,
|
||||
}),
|
||||
);
|
||||
regularValidationResult: regularValidationResult.map(entry => entry.validator),
|
||||
prevValidationResult: this.__prevValidationResult.map(entry => entry.validator),
|
||||
prevShownValidationResult: this.__prevShownValidationResult.map(
|
||||
entry => entry.validator,
|
||||
),
|
||||
})
|
||||
),
|
||||
}))
|
||||
.filter(v => Boolean(v.outcome));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -512,35 +541,32 @@ export const ValidateMixinImplementation = superclass =>
|
|||
const resultOutCome = this.__executeResultValidators(syncAndAsyncOutcome);
|
||||
|
||||
this.__validationResult = [...resultOutCome, ...syncAndAsyncOutcome];
|
||||
// this._storeResultsOnInstance(this.__validationResult);
|
||||
|
||||
const ctor =
|
||||
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
|
||||
this.constructor
|
||||
);
|
||||
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||
|
||||
/** @type {Object.<string, Object.<string, boolean>>} */
|
||||
/** @type {ValidationStates} */
|
||||
const validationStates = ctor.validationTypes.reduce(
|
||||
(acc, type) => ({ ...acc, [type]: {} }),
|
||||
{},
|
||||
);
|
||||
this.__validationResult.forEach(v => {
|
||||
if (!validationStates[v.type]) {
|
||||
validationStates[v.type] = {};
|
||||
this.__validationResult.forEach(({ validator, outcome }) => {
|
||||
if (!validationStates[validator.type]) {
|
||||
validationStates[validator.type] = {};
|
||||
}
|
||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
||||
validationStates[v.type][vCtor.validatorName] = true;
|
||||
const vCtor = /** @type {typeof Validator} */ (validator.constructor);
|
||||
validationStates[validator.type][vCtor.validatorName] = outcome;
|
||||
});
|
||||
this.validationStates = validationStates;
|
||||
|
||||
this.hasFeedbackFor = [...new Set(this.__validationResult.map(v => v.type))];
|
||||
this.hasFeedbackFor = [
|
||||
...new Set(this.__validationResult.map(({ validator }) => validator.type)),
|
||||
];
|
||||
|
||||
/** private event that should be listened to by LionFieldSet */
|
||||
this.dispatchEvent(new Event('validate-performed', { bubbles: true }));
|
||||
if (source === 'async' || !hasAsync) {
|
||||
if (this.__validateCompleteResolve) {
|
||||
// @ts-ignore [allow-private]
|
||||
this.__validateCompleteResolve();
|
||||
this.__validateCompleteResolve(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -587,10 +613,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
console.error(errorMessage, this);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const ctor =
|
||||
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
|
||||
this.constructor
|
||||
);
|
||||
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||
if (ctor.validationTypes.indexOf(v.type) === -1) {
|
||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
||||
// throws in constructor are not visible to end user so we do both
|
||||
|
|
@ -640,14 +663,14 @@ export const ValidateMixinImplementation = superclass =>
|
|||
*/
|
||||
|
||||
/**
|
||||
* @param {Validator[]} validators list of objects having a .getMessage method
|
||||
* @param {ValidationResultEntry[]} validationResults list of objects having a .getMessage method
|
||||
* @return {Promise.<FeedbackMessage[]>}
|
||||
* @private
|
||||
*/
|
||||
async __getFeedbackMessages(validators) {
|
||||
async __getFeedbackMessages(validationResults) {
|
||||
let fieldName = await this.fieldName;
|
||||
return Promise.all(
|
||||
validators.map(async validator => {
|
||||
validationResults.map(async ({ validator, outcome }) => {
|
||||
if (validator.config.fieldName) {
|
||||
fieldName = await validator.config.fieldName;
|
||||
}
|
||||
|
|
@ -656,6 +679,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
modelValue: this.modelValue,
|
||||
formControl: this,
|
||||
fieldName,
|
||||
outcome,
|
||||
});
|
||||
return { message, type: validator.type, validator };
|
||||
}),
|
||||
|
|
@ -690,10 +714,19 @@ export const ValidateMixinImplementation = superclass =>
|
|||
if (this.showsFeedbackFor.length > 0) {
|
||||
this.__feedbackQueue.add(async () => {
|
||||
/** @type {Validator[]} */
|
||||
this.__prioritizedResult = this._prioritizeAndFilterFeedback({
|
||||
validationResult: this.__validationResult,
|
||||
const prioritizedValidators = this._prioritizeAndFilterFeedback({
|
||||
validationResult: this.__validationResult.map(entry => entry.validator),
|
||||
});
|
||||
|
||||
this.__prioritizedResult = prioritizedValidators
|
||||
.map(v => {
|
||||
const found = /** @type {ValidationResultEntry} */ (
|
||||
this.__validationResult.find(r => v === r.validator)
|
||||
);
|
||||
return found;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (this.__prioritizedResult.length > 0) {
|
||||
this.__prevShownValidationResult = this.__prioritizedResult;
|
||||
}
|
||||
|
|
@ -732,12 +765,12 @@ export const ValidateMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Allows the end user to specify when a feedback message should be shown
|
||||
* Allows the Application Developer to specify when a feedback message should be shown
|
||||
* @example
|
||||
* ```js
|
||||
* feedbackCondition(type, meta, defaultCondition) {
|
||||
* if (type === 'info') {
|
||||
* return return;
|
||||
* return true;
|
||||
* } else if (type === 'prefilledOnly') {
|
||||
* return meta.prefilled;
|
||||
* }
|
||||
|
|
@ -775,7 +808,9 @@ export const ValidateMixinImplementation = superclass =>
|
|||
);
|
||||
}
|
||||
|
||||
/** @param {import('@lion/core').PropertyValues} changedProperties */
|
||||
/**
|
||||
* @param {import('@lion/core').PropertyValues} changedProperties
|
||||
*/
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
|
|
@ -783,10 +818,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
changedProperties.has('shouldShowFeedbackFor') ||
|
||||
changedProperties.has('hasFeedbackFor')
|
||||
) {
|
||||
const ctor =
|
||||
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
|
||||
this.constructor
|
||||
);
|
||||
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
|
||||
this.showsFeedbackFor = /** @type {string[]} */ (
|
||||
ctor.validationTypes
|
||||
|
|
@ -822,10 +854,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
* @protected
|
||||
*/
|
||||
_updateShouldShowFeedbackFor() {
|
||||
const ctor =
|
||||
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
|
||||
this.constructor
|
||||
);
|
||||
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||
|
||||
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
|
||||
const newShouldShowFeedbackFor = /** @type {string[]} */ (
|
||||
|
|
@ -848,18 +877,15 @@ export const ValidateMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Orders all active validators in this.__validationResult. Can
|
||||
* also filter out occurrences (based on interaction states)
|
||||
* Orders all active validators in this.__validationResult.
|
||||
* Can also filter out occurrences (based on interaction states)
|
||||
* @overridable
|
||||
* @param {{ validationResult: Validator[] }} opts
|
||||
* @return {Validator[]} ordered list of Validators with feedback messages visible to the end user
|
||||
* @protected
|
||||
*/
|
||||
_prioritizeAndFilterFeedback({ validationResult }) {
|
||||
const ctor =
|
||||
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
|
||||
this.constructor
|
||||
);
|
||||
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||
const types = ctor.validationTypes;
|
||||
// Sort all validators based on the type provided.
|
||||
const res = validationResult
|
||||
|
|
|
|||
|
|
@ -1,44 +1,65 @@
|
|||
/**
|
||||
* @typedef {object} MessageData
|
||||
* @property {*} [MessageData.modelValue]
|
||||
* @property {string} [MessageData.fieldName]
|
||||
* @property {HTMLElement} [MessageData.formControl]
|
||||
* @property {string} [MessageData.type]
|
||||
* @property {Object.<string,?>} [MessageData.config]
|
||||
* @property {string} [MessageData.name]
|
||||
* @typedef {import('./types').FeedbackMessageData} FeedbackMessageData
|
||||
* @typedef {import('./types').ValidatorParam} ValidatorParam
|
||||
* @typedef {import('./types').ValidatorConfig} ValidatorConfig
|
||||
* @typedef {import('./types').ValidatorOutcome} ValidatorOutcome
|
||||
* @typedef {import('./types').ValidatorName} ValidatorName
|
||||
* @typedef {import('./types').ValidationType} ValidationType
|
||||
* @typedef {import('../FormControlMixin').FormControlHost} FormControlHost
|
||||
*/
|
||||
|
||||
export class Validator {
|
||||
// TODO: support attribute validators like <my-el my-validator=${dynamicParam}></my-el> =>
|
||||
// register in a ValidateService that is read by Validator and adds these attrs in properties
|
||||
// object.
|
||||
// They would become like configurable
|
||||
// [global attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes)
|
||||
// for FormControls.
|
||||
|
||||
export class Validator extends EventTarget {
|
||||
/**
|
||||
*
|
||||
* @param {?} [param]
|
||||
* @param {Object.<string,?>} [config]
|
||||
* @param {ValidatorParam} [param]
|
||||
* @param {ValidatorConfig} [config]
|
||||
*/
|
||||
constructor(param, config) {
|
||||
this.__fakeExtendsEventTarget();
|
||||
super();
|
||||
|
||||
/** @type {?} */
|
||||
/** @type {ValidatorParam} */
|
||||
this.__param = param;
|
||||
|
||||
/** @type {Object.<string,?>} */
|
||||
/** @type {ValidatorConfig} */
|
||||
this.__config = config || {};
|
||||
this.type = (config && config.type) || 'error'; // Default type supported by ValidateMixin
|
||||
}
|
||||
|
||||
static get validatorName() {
|
||||
return '';
|
||||
}
|
||||
|
||||
static get async() {
|
||||
return false;
|
||||
/** @type {ValidationType} */
|
||||
this.type = config?.type || 'error'; // Default type supported by ValidateMixin
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc The function that returns a Boolean
|
||||
* @param {?} [modelValue]
|
||||
* @param {?} [param]
|
||||
* @param {{}} [config]
|
||||
* @returns {Boolean|Promise<Boolean>}
|
||||
* The name under which validation results get registered. For convience and predictability, this
|
||||
* should always be the same as the constructor name (since it will be obfuscated in js builds,
|
||||
* we need to provide it separately).
|
||||
* @type {ValidatorName}
|
||||
*/
|
||||
static validatorName = '';
|
||||
|
||||
/**
|
||||
* Whether the validator is asynchronous or not. When true., this means execute function returns
|
||||
* a Promise. This can be handy for:
|
||||
* - server side calls
|
||||
* - validations that are dependent on lazy loaded resources (they can be async until the dependency
|
||||
* is loaded)
|
||||
* @type {boolean}
|
||||
*/
|
||||
static async = false;
|
||||
|
||||
/**
|
||||
* The function that returns a validity outcome. When we need to shpw feedback,
|
||||
* it should return true, otherwise false. So when an error\info|warning|success message
|
||||
* needs to be shown, return true. For async Validators, the function canretun a Promise.
|
||||
* It's also possible to return an enum. Let's say that a phone number can have multiple
|
||||
* states: 'invalid-country-code' | 'too-long' | 'too-short'
|
||||
* Those states can be retrieved in the getMessage
|
||||
* @param {any} modelValue
|
||||
* @param {ValidatorParam} [param]
|
||||
* @param {ValidatorConfig} [config]
|
||||
* @returns {ValidatorOutcome|Promise<ValidatorOutcome>}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
||||
execute(modelValue, param, config) {
|
||||
|
|
@ -51,32 +72,85 @@ export class Validator {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first argument of the constructor, for instance 3 in `new MinLength(3)`. Will
|
||||
* be stored on Validator instance and passed to `execute` function
|
||||
* @example
|
||||
* ```js
|
||||
* // Store reference to Validator instance
|
||||
* const myValidatorInstance = new MyValidator(1);
|
||||
* // Use this instance initially on a FormControl (that uses ValidateMixin)
|
||||
* render(html`<validatable-element .validators="${[myValidatorInstance]}"></validatable-element>`, document.body);
|
||||
* // Based on some event, we need to change the param
|
||||
* myValidatorInstance.param = 2;
|
||||
* ```
|
||||
* @property {ValidatorParam}
|
||||
*/
|
||||
set param(p) {
|
||||
this.__param = p;
|
||||
if (this.dispatchEvent) {
|
||||
/**
|
||||
* This event is listened for by ValidateMixin. Whenever the validation parameter has
|
||||
* changed, the FormControl will revalidate itself
|
||||
*/
|
||||
this.dispatchEvent(new Event('param-changed'));
|
||||
}
|
||||
}
|
||||
|
||||
get param() {
|
||||
return this.__param;
|
||||
}
|
||||
|
||||
/**
|
||||
* The second argument of the constructor, for instance
|
||||
* `new MinLength(3, {getFeedMessage: async () => 'too long'})`.
|
||||
* Will be stored on Validator instance and passed to `execute` function.
|
||||
* @example
|
||||
* ```js
|
||||
* // Store reference to Validator instance
|
||||
* const myValidatorInstance = new MyValidator(1, {getMessage() => 'x'});
|
||||
* // Use this instance initially on a FormControl (that uses ValidateMixin)
|
||||
* render(html`<validatable-element .validators="${[myValidatorInstance]}"></validatable-element>`, document.body);
|
||||
* // Based on some event, we need to change the param
|
||||
* myValidatorInstance.config = {getMessage() => 'y'};
|
||||
* ```
|
||||
* @property {ValidatorConfig}
|
||||
*/
|
||||
set config(c) {
|
||||
this.__config = c;
|
||||
if (this.dispatchEvent) {
|
||||
/**
|
||||
* This event is listened for by ValidateMixin. Whenever the validation config has
|
||||
* changed, the FormControl will revalidate itself
|
||||
*/
|
||||
this.dispatchEvent(new Event('config-changed'));
|
||||
}
|
||||
}
|
||||
|
||||
get config() {
|
||||
return this.__config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @overridable
|
||||
* @param {MessageData} [data]
|
||||
* @returns {Promise<string|Node>}
|
||||
* This is a protected method that usually should not be overridden. It is called by ValidateMixin
|
||||
* and it gathers data to be passed to getMessage functions found:
|
||||
* - `this.config.getMessage`, locally provided by consumers of the Validator (overrides global getMessage)
|
||||
* - `MyValidator.getMessage`, globally provided by creators or consumers of the Validator
|
||||
*
|
||||
* Confusion can arise because of similarities with former mentioned methods. In that regard, a
|
||||
* better name for this function would have been _pepareDataAndCallHighestPrioGetMessage.
|
||||
* @example
|
||||
* ```js
|
||||
* class MyValidator extends Validator {
|
||||
* // ...
|
||||
* // 1. globally defined
|
||||
* static async getMessage() {
|
||||
* return 'lowest prio, defined globally by Validator author'
|
||||
* }
|
||||
* }
|
||||
* // 2. globally overridden
|
||||
* MyValidator.getMessage = async() => 'overrides already configured message';
|
||||
* // 3. locally overridden
|
||||
* new MyValidator(myParam, { getMessage: async() => 'locally defined, always wins' });
|
||||
* ```
|
||||
* @param {Partial<FeedbackMessageData>} [data]
|
||||
* @returns {Promise<string|Element>}
|
||||
* @protected
|
||||
*/
|
||||
async _getMessage(data) {
|
||||
|
|
@ -101,9 +175,20 @@ export class Validator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Called inside Validator.prototype._getMessage (see explanation).
|
||||
* @example
|
||||
* ```js
|
||||
* class MyValidator extends Validator {
|
||||
* static async getMessage() {
|
||||
* return 'lowest prio, defined globally by Validator author'
|
||||
* }
|
||||
* }
|
||||
* // globally overridden
|
||||
* MyValidator.getMessage = async() => 'overrides already configured message';
|
||||
* ```
|
||||
* @overridable
|
||||
* @param {MessageData} [data]
|
||||
* @returns {Promise<string|Node>}
|
||||
* @param {Partial<FeedbackMessageData>} [data]
|
||||
* @returns {Promise<string|Element>}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
static async getMessage(data) {
|
||||
|
|
@ -111,12 +196,38 @@ export class Validator {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} formControl
|
||||
* Validators are allowed to have knowledge about FormControls.
|
||||
* In some cases (in case of the Required Validator) we wanted to enhance accessibility by
|
||||
* adding [aria-required]. Also, it would be possible to write an advanced MinLength
|
||||
* Validator that adds a .preprocessor that restricts from typing too many characters
|
||||
* (like the native [minlength] validator).
|
||||
* Will be called when Validator is added to FormControl.validators.
|
||||
* @example
|
||||
* ```js
|
||||
* onFormControlConnect(formControl) {
|
||||
* if(formControl.inputNode) {
|
||||
* inputNode.setAttribute('aria-required', 'true');
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* ```
|
||||
* @configurable
|
||||
* @param {FormControlHost} formControl
|
||||
*/
|
||||
onFormControlConnect(formControl) {} // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} formControl
|
||||
* Also see `onFormControlConnect`.
|
||||
* Will be called when Validator is removed from FormControl.validators.
|
||||
* @example
|
||||
* ```js
|
||||
* onFormControlDisconnect(formControl) {
|
||||
* if(formControl.inputNode) {
|
||||
* inputNode.removeAttribute('aria-required');
|
||||
* }
|
||||
* }
|
||||
* @configurable
|
||||
* @param {FormControlHost} formControl
|
||||
*/
|
||||
onFormControlDisconnect(formControl) {} // eslint-disable-line
|
||||
|
||||
|
|
@ -130,41 +241,6 @@ export class Validator {
|
|||
* - Or, when a webworker was started, its process could be aborted and then restarted.
|
||||
*/
|
||||
abortExecution() {} // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__fakeExtendsEventTarget() {
|
||||
const delegate = document.createDocumentFragment();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {EventListener} listener
|
||||
* @param {Object} [opts]
|
||||
*/
|
||||
const delegatedAddEventListener = (type, listener, opts) =>
|
||||
delegate.addEventListener(type, listener, opts);
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {EventListener} listener
|
||||
* @param {Object} [opts]
|
||||
*/
|
||||
const delegatedRemoveEventListener = (type, listener, opts) =>
|
||||
delegate.removeEventListener(type, listener, opts);
|
||||
|
||||
/**
|
||||
* @param {Event|CustomEvent} event
|
||||
*/
|
||||
const delegatedDispatchEvent = event => delegate.dispatchEvent(event);
|
||||
|
||||
this.addEventListener = delegatedAddEventListener;
|
||||
|
||||
this.removeEventListener = delegatedRemoveEventListener;
|
||||
|
||||
this.dispatchEvent = delegatedDispatchEvent;
|
||||
}
|
||||
}
|
||||
|
||||
// For simplicity, a default validator only handles one state:
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
*/
|
||||
|
||||
describe('Validation initiation', () => {
|
||||
it('throws and console.errors if adding not Validator instances to the validators array', async () => {
|
||||
it('throws and console.errors if adding non Validator instances to the validators array', async () => {
|
||||
// we throw and console error as constructor throw are not visible to the end user
|
||||
const stub = sinon.stub(console, 'error');
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`<${tag}></${tag}>`));
|
||||
|
|
@ -93,7 +93,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
stub.restore();
|
||||
});
|
||||
|
||||
it('throws and console error if adding a not supported Validator type', async () => {
|
||||
it('throws a console error if adding a non supported Validator type', async () => {
|
||||
// we throw and console error to improve DX
|
||||
const stub = sinon.stub(console, 'error');
|
||||
const errorMessage = `This component does not support the validator type "major error" used in "MajorValidator". You may change your validators type or add it to the components "static get validationTypes() {}".`;
|
||||
|
|
@ -720,7 +720,10 @@ export function runValidateMixinSuite(customConfig) {
|
|||
|
||||
// @ts-ignore [allow-private] in test
|
||||
const totalValidationResult = el.__validationResult;
|
||||
expect(totalValidationResult).to.eql([resultV, validator]);
|
||||
expect(totalValidationResult).to.eql([
|
||||
{ validator: resultV, outcome: true },
|
||||
{ validator, outcome: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1049,7 +1052,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
await fixture(html`
|
||||
<${tag}
|
||||
.modelValue=${'123'}
|
||||
.validators=${[new MinLength(3, { message: 'foo' })]}>
|
||||
.validators=${[new MinLength(3, { getMessage: async () => 'foo' })]}>
|
||||
<input slot="input">
|
||||
</${tag}>`)
|
||||
);
|
||||
|
|
@ -1352,14 +1355,14 @@ export function runValidateMixinSuite(customConfig) {
|
|||
};
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
/**
|
||||
* @param {string} type
|
||||
*/
|
||||
_showFeedbackConditionFor(type) {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
// @ts-ignore
|
||||
return ['A', 'B'].includes(this.modelValue);
|
||||
default:
|
||||
// @ts-ignore
|
||||
return ['B', 'C'].includes(this.modelValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -1384,12 +1387,10 @@ export function runValidateMixinSuite(customConfig) {
|
|||
['D', []],
|
||||
]) {
|
||||
el.modelValue = modelValue;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await el.updateComplete;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await el.feedbackComplete;
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-expect-error [allow-protected]
|
||||
const resultOrder = el._feedbackNode.feedbackData.map(v => v.type);
|
||||
expect(resultOrder).to.deep.equal(expected);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ import sinon from 'sinon';
|
|||
import { DefaultSuccess, MinLength, Required, ValidateMixin, Validator } from '../index.js';
|
||||
import { AlwaysInvalid } from '../test-helpers/index.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../src/validate/types').FeedbackMessageData} FeedbackMessageData
|
||||
*/
|
||||
|
||||
export function runValidateMixinFeedbackPart() {
|
||||
describe('Validity Feedback', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -303,7 +307,7 @@ export function runValidateMixinFeedbackPart() {
|
|||
await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
|
||||
.validators=${[new MinLength(3, { getMessage: async () => 'custom via config' })]}
|
||||
>${lightDom}</${tag}>
|
||||
`)
|
||||
);
|
||||
|
|
@ -397,7 +401,7 @@ export function runValidateMixinFeedbackPart() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Meta data', () => {
|
||||
describe('FeedbackMessageData', () => {
|
||||
it('".getMessage()" gets a reference to formControl, params, modelValue and type', async () => {
|
||||
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||
static get validationTypes() {
|
||||
|
|
@ -431,6 +435,7 @@ export function runValidateMixinFeedbackPart() {
|
|||
fieldName: '',
|
||||
type: 'x',
|
||||
name: 'MinLength',
|
||||
outcome: true,
|
||||
});
|
||||
|
||||
const instanceMessageSpy = sinon.spy();
|
||||
|
|
@ -457,6 +462,7 @@ export function runValidateMixinFeedbackPart() {
|
|||
fieldName: '',
|
||||
type: 'error',
|
||||
name: 'MinLength',
|
||||
outcome: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -485,7 +491,7 @@ export function runValidateMixinFeedbackPart() {
|
|||
fieldName: 'myField',
|
||||
type: 'error',
|
||||
name: 'MinLength',
|
||||
});
|
||||
outcome: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -520,6 +526,50 @@ export function runValidateMixinFeedbackPart() {
|
|||
fieldName: 'myFieldViaCfg',
|
||||
type: 'error',
|
||||
name: 'MinLength',
|
||||
outcome: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('".getMessage()" gets .outcome, which can be "true" or an enum', async () => {
|
||||
class EnumOutComeValidator extends Validator {
|
||||
static validatorName = 'EnumOutCome';
|
||||
|
||||
execute() {
|
||||
return 'a-string-instead-of-bool';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FeedbackMessageData} meta
|
||||
* @returns
|
||||
*/
|
||||
static async getMessage({ outcome }) {
|
||||
const results = {
|
||||
'a-string-instead-of-bool': 'Msg based on enum output',
|
||||
};
|
||||
return results[/** @type {string} */ (outcome)];
|
||||
}
|
||||
}
|
||||
|
||||
const enumOutComeValidator = new EnumOutComeValidator();
|
||||
const spy = sinon.spy(
|
||||
/** @type {typeof EnumOutComeValidator} */ (enumOutComeValidator.constructor),
|
||||
'getMessage',
|
||||
);
|
||||
|
||||
const el = /** @type {ValidateElement} */ (
|
||||
await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.validators=${[enumOutComeValidator]}
|
||||
.modelValue=${'cat'}
|
||||
>${lightDom}</${tag}>
|
||||
`)
|
||||
);
|
||||
await el.updateComplete;
|
||||
await el.feedbackComplete;
|
||||
|
||||
const getMessageArs = spy.args[0][0];
|
||||
expect(getMessageArs.outcome).to.equal('a-string-instead-of-bool');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue