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').ValidateMixin} ValidateMixin
|
||||||
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidationType} ValidationType
|
* @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 = [];
|
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
|
* A temporary storage to transition from hasFeedbackFor to showsFeedbackFor
|
||||||
* @type {ValidationType[]}
|
* @type {ValidationType[]}
|
||||||
|
|
@ -171,7 +175,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
/**
|
/**
|
||||||
* The outcome of a validation 'round'. Keyed by ValidationType and Validator name
|
* The outcome of a validation 'round'. Keyed by ValidationType and Validator name
|
||||||
* @readOnly
|
* @readOnly
|
||||||
* @type {Object.<string, Object.<string, boolean>>}
|
* @type {ValidationStates}
|
||||||
*/
|
*/
|
||||||
this.validationStates = {};
|
this.validationStates = {};
|
||||||
|
|
||||||
|
|
@ -206,44 +210,45 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The amount of feedback messages that will visible in LionValidationFeedback
|
* The amount of feedback messages that will visible in LionValidationFeedback
|
||||||
|
* @configurable
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
this._visibleMessagesAmount = 1;
|
this._visibleMessagesAmount = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Validator[]}
|
* @type {ValidationResultEntry[]}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.__syncValidationResult = [];
|
this.__syncValidationResult = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Validator[]}
|
* @type {ValidationResultEntry[]}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.__asyncValidationResult = [];
|
this.__asyncValidationResult = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aggregated result from sync Validators, async Validators and ResultValidators
|
* Aggregated result from sync Validators, async Validators and ResultValidators
|
||||||
* @type {Validator[]}
|
* @type {ValidationResultEntry[]}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.__validationResult = [];
|
this.__validationResult = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Validator[]}
|
* @type {ValidationResultEntry[]}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.__prevValidationResult = [];
|
this.__prevValidationResult = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Validator[]}
|
* @type {ValidationResultEntry[]}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.__prevShownValidationResult = [];
|
this.__prevShownValidationResult = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The updated children validity affects the validity of the parent. Helper to recompute
|
* The updated children validity affects the validity of the parent. Helper to recompute
|
||||||
* validatity of parent FormGroup
|
* validity of parent FormGroup
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.__childModelValueChanged = false;
|
this.__childModelValueChanged = false;
|
||||||
|
|
@ -337,7 +342,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
* Triggered by:
|
* Triggered by:
|
||||||
* - modelValue change
|
* - modelValue change
|
||||||
* - change in the 'validators' array
|
* - change in the 'validators' array
|
||||||
* - change in the config of an individual Validator
|
* - change in the config of an individual Validator
|
||||||
*
|
*
|
||||||
* Three situations are handled:
|
* Three situations are handled:
|
||||||
* - a1) the FormControl is empty: further execution is halted. When the Required Validator
|
* - a1) the FormControl is empty: further execution is halted. When the Required Validator
|
||||||
|
|
@ -384,6 +389,14 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
* @desc step a1-3 + b (as explained in `validate()`)
|
* @desc step a1-3 + b (as explained in `validate()`)
|
||||||
*/
|
*/
|
||||||
async __executeValidators() {
|
async __executeValidators() {
|
||||||
|
/**
|
||||||
|
* Allows Application Developer to wait for (async) validation
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* await el.validateComplete;
|
||||||
|
* ```
|
||||||
|
* @type {Promise<boolean>}
|
||||||
|
*/
|
||||||
this.validateComplete = new Promise(resolve => {
|
this.validateComplete = new Promise(resolve => {
|
||||||
this.__validateCompleteResolve = resolve;
|
this.__validateCompleteResolve = resolve;
|
||||||
});
|
});
|
||||||
|
|
@ -410,7 +423,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
const isEmpty = this.__isEmpty(value);
|
const isEmpty = this.__isEmpty(value);
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
if (requiredValidator) {
|
if (requiredValidator) {
|
||||||
this.__syncValidationResult = [requiredValidator];
|
this.__syncValidationResult = [{ validator: requiredValidator, outcome: true }];
|
||||||
}
|
}
|
||||||
this.__finishValidation({ source: 'sync' });
|
this.__finishValidation({ source: 'sync' });
|
||||||
return;
|
return;
|
||||||
|
|
@ -451,9 +464,12 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
*/
|
*/
|
||||||
__executeSyncValidators(syncValidators, value, { hasAsync }) {
|
__executeSyncValidators(syncValidators, value, { hasAsync }) {
|
||||||
if (syncValidators.length) {
|
if (syncValidators.length) {
|
||||||
this.__syncValidationResult = syncValidators.filter(v =>
|
this.__syncValidationResult = syncValidators
|
||||||
v.execute(value, v.param, { node: this }),
|
.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 });
|
this.__finishValidation({ source: 'sync', hasAsync });
|
||||||
}
|
}
|
||||||
|
|
@ -468,10 +484,15 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
if (asyncValidators.length) {
|
if (asyncValidators.length) {
|
||||||
this.isPending = true;
|
this.isPending = true;
|
||||||
const resultPromises = asyncValidators.map(v => v.execute(value, v.param, { node: this }));
|
const resultPromises = asyncValidators.map(v => v.execute(value, v.param, { node: this }));
|
||||||
const booleanResults = await Promise.all(resultPromises);
|
const asyncExecutionResults = await Promise.all(resultPromises);
|
||||||
this.__asyncValidationResult = booleanResults
|
|
||||||
.map((r, i) => asyncValidators[i]) // Create an array of Validators
|
this.__asyncValidationResult = asyncExecutionResults
|
||||||
.filter((v, i) => booleanResults[i]); // Only leave the ones returning true
|
.map((r, i) => ({
|
||||||
|
validator: asyncValidators[i],
|
||||||
|
outcome: /** @type {boolean|string} */ (asyncExecutionResults[i]),
|
||||||
|
}))
|
||||||
|
.filter(v => Boolean(v.outcome));
|
||||||
|
|
||||||
this.__finishValidation({ source: 'async' });
|
this.__finishValidation({ source: 'async' });
|
||||||
this.isPending = false;
|
this.isPending = false;
|
||||||
}
|
}
|
||||||
|
|
@ -479,7 +500,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* step b (as explained in `validate()`), called by __finishValidation
|
* 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
|
* @private
|
||||||
*/
|
*/
|
||||||
__executeResultValidators(regularValidationResult) {
|
__executeResultValidators(regularValidationResult) {
|
||||||
|
|
@ -490,13 +511,21 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return resultValidators.filter(v =>
|
// Map everything to Validator[] for backwards compatibility
|
||||||
v.executeOnResults({
|
return resultValidators
|
||||||
regularValidationResult,
|
.map(v => ({
|
||||||
prevValidationResult: this.__prevValidationResult,
|
validator: v,
|
||||||
prevShownValidationResult: this.__prevShownValidationResult,
|
outcome: /** @type {boolean|string} */ (
|
||||||
}),
|
v.executeOnResults({
|
||||||
);
|
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);
|
const resultOutCome = this.__executeResultValidators(syncAndAsyncOutcome);
|
||||||
|
|
||||||
this.__validationResult = [...resultOutCome, ...syncAndAsyncOutcome];
|
this.__validationResult = [...resultOutCome, ...syncAndAsyncOutcome];
|
||||||
// this._storeResultsOnInstance(this.__validationResult);
|
|
||||||
|
|
||||||
const ctor =
|
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||||
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
|
|
||||||
this.constructor
|
|
||||||
);
|
|
||||||
|
|
||||||
/** @type {Object.<string, Object.<string, boolean>>} */
|
/** @type {ValidationStates} */
|
||||||
const validationStates = ctor.validationTypes.reduce(
|
const validationStates = ctor.validationTypes.reduce(
|
||||||
(acc, type) => ({ ...acc, [type]: {} }),
|
(acc, type) => ({ ...acc, [type]: {} }),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
this.__validationResult.forEach(v => {
|
this.__validationResult.forEach(({ validator, outcome }) => {
|
||||||
if (!validationStates[v.type]) {
|
if (!validationStates[validator.type]) {
|
||||||
validationStates[v.type] = {};
|
validationStates[validator.type] = {};
|
||||||
}
|
}
|
||||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
const vCtor = /** @type {typeof Validator} */ (validator.constructor);
|
||||||
validationStates[v.type][vCtor.validatorName] = true;
|
validationStates[validator.type][vCtor.validatorName] = outcome;
|
||||||
});
|
});
|
||||||
this.validationStates = validationStates;
|
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 */
|
/** private event that should be listened to by LionFieldSet */
|
||||||
this.dispatchEvent(new Event('validate-performed', { bubbles: true }));
|
this.dispatchEvent(new Event('validate-performed', { bubbles: true }));
|
||||||
if (source === 'async' || !hasAsync) {
|
if (source === 'async' || !hasAsync) {
|
||||||
if (this.__validateCompleteResolve) {
|
if (this.__validateCompleteResolve) {
|
||||||
// @ts-ignore [allow-private]
|
this.__validateCompleteResolve(true);
|
||||||
this.__validateCompleteResolve();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -587,10 +613,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
console.error(errorMessage, this);
|
console.error(errorMessage, this);
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
const ctor =
|
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||||
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
|
|
||||||
this.constructor
|
|
||||||
);
|
|
||||||
if (ctor.validationTypes.indexOf(v.type) === -1) {
|
if (ctor.validationTypes.indexOf(v.type) === -1) {
|
||||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
||||||
// throws in constructor are not visible to end user so we do both
|
// 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[]>}
|
* @return {Promise.<FeedbackMessage[]>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async __getFeedbackMessages(validators) {
|
async __getFeedbackMessages(validationResults) {
|
||||||
let fieldName = await this.fieldName;
|
let fieldName = await this.fieldName;
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
validators.map(async validator => {
|
validationResults.map(async ({ validator, outcome }) => {
|
||||||
if (validator.config.fieldName) {
|
if (validator.config.fieldName) {
|
||||||
fieldName = await validator.config.fieldName;
|
fieldName = await validator.config.fieldName;
|
||||||
}
|
}
|
||||||
|
|
@ -656,6 +679,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
modelValue: this.modelValue,
|
modelValue: this.modelValue,
|
||||||
formControl: this,
|
formControl: this,
|
||||||
fieldName,
|
fieldName,
|
||||||
|
outcome,
|
||||||
});
|
});
|
||||||
return { message, type: validator.type, validator };
|
return { message, type: validator.type, validator };
|
||||||
}),
|
}),
|
||||||
|
|
@ -690,10 +714,19 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
if (this.showsFeedbackFor.length > 0) {
|
if (this.showsFeedbackFor.length > 0) {
|
||||||
this.__feedbackQueue.add(async () => {
|
this.__feedbackQueue.add(async () => {
|
||||||
/** @type {Validator[]} */
|
/** @type {Validator[]} */
|
||||||
this.__prioritizedResult = this._prioritizeAndFilterFeedback({
|
const prioritizedValidators = this._prioritizeAndFilterFeedback({
|
||||||
validationResult: this.__validationResult,
|
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) {
|
if (this.__prioritizedResult.length > 0) {
|
||||||
this.__prevShownValidationResult = this.__prioritizedResult;
|
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
|
* @example
|
||||||
* ```js
|
* ```js
|
||||||
* feedbackCondition(type, meta, defaultCondition) {
|
* feedbackCondition(type, meta, defaultCondition) {
|
||||||
* if (type === 'info') {
|
* if (type === 'info') {
|
||||||
* return return;
|
* return true;
|
||||||
* } else if (type === 'prefilledOnly') {
|
* } else if (type === 'prefilledOnly') {
|
||||||
* return meta.prefilled;
|
* 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) {
|
updated(changedProperties) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
|
@ -783,10 +818,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
changedProperties.has('shouldShowFeedbackFor') ||
|
changedProperties.has('shouldShowFeedbackFor') ||
|
||||||
changedProperties.has('hasFeedbackFor')
|
changedProperties.has('hasFeedbackFor')
|
||||||
) {
|
) {
|
||||||
const ctor =
|
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||||
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
|
|
||||||
this.constructor
|
|
||||||
);
|
|
||||||
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
|
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
|
||||||
this.showsFeedbackFor = /** @type {string[]} */ (
|
this.showsFeedbackFor = /** @type {string[]} */ (
|
||||||
ctor.validationTypes
|
ctor.validationTypes
|
||||||
|
|
@ -822,10 +854,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_updateShouldShowFeedbackFor() {
|
_updateShouldShowFeedbackFor() {
|
||||||
const ctor =
|
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||||
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
|
|
||||||
this.constructor
|
|
||||||
);
|
|
||||||
|
|
||||||
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
|
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
|
||||||
const newShouldShowFeedbackFor = /** @type {string[]} */ (
|
const newShouldShowFeedbackFor = /** @type {string[]} */ (
|
||||||
|
|
@ -848,18 +877,15 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Orders all active validators in this.__validationResult. Can
|
* Orders all active validators in this.__validationResult.
|
||||||
* also filter out occurrences (based on interaction states)
|
* Can also filter out occurrences (based on interaction states)
|
||||||
* @overridable
|
* @overridable
|
||||||
* @param {{ validationResult: Validator[] }} opts
|
* @param {{ validationResult: Validator[] }} opts
|
||||||
* @return {Validator[]} ordered list of Validators with feedback messages visible to the end user
|
* @return {Validator[]} ordered list of Validators with feedback messages visible to the end user
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_prioritizeAndFilterFeedback({ validationResult }) {
|
_prioritizeAndFilterFeedback({ validationResult }) {
|
||||||
const ctor =
|
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||||
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
|
|
||||||
this.constructor
|
|
||||||
);
|
|
||||||
const types = ctor.validationTypes;
|
const types = ctor.validationTypes;
|
||||||
// Sort all validators based on the type provided.
|
// Sort all validators based on the type provided.
|
||||||
const res = validationResult
|
const res = validationResult
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,65 @@
|
||||||
/**
|
/**
|
||||||
* @typedef {object} MessageData
|
* @typedef {import('./types').FeedbackMessageData} FeedbackMessageData
|
||||||
* @property {*} [MessageData.modelValue]
|
* @typedef {import('./types').ValidatorParam} ValidatorParam
|
||||||
* @property {string} [MessageData.fieldName]
|
* @typedef {import('./types').ValidatorConfig} ValidatorConfig
|
||||||
* @property {HTMLElement} [MessageData.formControl]
|
* @typedef {import('./types').ValidatorOutcome} ValidatorOutcome
|
||||||
* @property {string} [MessageData.type]
|
* @typedef {import('./types').ValidatorName} ValidatorName
|
||||||
* @property {Object.<string,?>} [MessageData.config]
|
* @typedef {import('./types').ValidationType} ValidationType
|
||||||
* @property {string} [MessageData.name]
|
* @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 {ValidatorParam} [param]
|
||||||
* @param {?} [param]
|
* @param {ValidatorConfig} [config]
|
||||||
* @param {Object.<string,?>} [config]
|
|
||||||
*/
|
*/
|
||||||
constructor(param, config) {
|
constructor(param, config) {
|
||||||
this.__fakeExtendsEventTarget();
|
super();
|
||||||
|
|
||||||
/** @type {?} */
|
/** @type {ValidatorParam} */
|
||||||
this.__param = param;
|
this.__param = param;
|
||||||
|
/** @type {ValidatorConfig} */
|
||||||
/** @type {Object.<string,?>} */
|
|
||||||
this.__config = config || {};
|
this.__config = config || {};
|
||||||
this.type = (config && config.type) || 'error'; // Default type supported by ValidateMixin
|
/** @type {ValidationType} */
|
||||||
}
|
this.type = config?.type || 'error'; // Default type supported by ValidateMixin
|
||||||
|
|
||||||
static get validatorName() {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get async() {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc The function that returns a Boolean
|
* The name under which validation results get registered. For convience and predictability, this
|
||||||
* @param {?} [modelValue]
|
* should always be the same as the constructor name (since it will be obfuscated in js builds,
|
||||||
* @param {?} [param]
|
* we need to provide it separately).
|
||||||
* @param {{}} [config]
|
* @type {ValidatorName}
|
||||||
* @returns {Boolean|Promise<Boolean>}
|
*/
|
||||||
|
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
|
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
||||||
execute(modelValue, param, config) {
|
execute(modelValue, param, config) {
|
||||||
|
|
@ -51,22 +72,55 @@ export class Validator {
|
||||||
return true;
|
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) {
|
set param(p) {
|
||||||
this.__param = p;
|
this.__param = p;
|
||||||
if (this.dispatchEvent) {
|
/**
|
||||||
this.dispatchEvent(new Event('param-changed'));
|
* 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() {
|
get param() {
|
||||||
return this.__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) {
|
set config(c) {
|
||||||
this.__config = c;
|
this.__config = c;
|
||||||
if (this.dispatchEvent) {
|
/**
|
||||||
this.dispatchEvent(new Event('config-changed'));
|
* 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() {
|
get config() {
|
||||||
|
|
@ -74,9 +128,29 @@ export class Validator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @overridable
|
* This is a protected method that usually should not be overridden. It is called by ValidateMixin
|
||||||
* @param {MessageData} [data]
|
* and it gathers data to be passed to getMessage functions found:
|
||||||
* @returns {Promise<string|Node>}
|
* - `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
|
* @protected
|
||||||
*/
|
*/
|
||||||
async _getMessage(data) {
|
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
|
* @overridable
|
||||||
* @param {MessageData} [data]
|
* @param {Partial<FeedbackMessageData>} [data]
|
||||||
* @returns {Promise<string|Node>}
|
* @returns {Promise<string|Element>}
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
static async getMessage(data) {
|
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
|
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
|
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.
|
* - Or, when a webworker was started, its process could be aborted and then restarted.
|
||||||
*/
|
*/
|
||||||
abortExecution() {} // eslint-disable-line
|
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:
|
// For simplicity, a default validator only handles one state:
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
describe('Validation initiation', () => {
|
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
|
// we throw and console error as constructor throw are not visible to the end user
|
||||||
const stub = sinon.stub(console, 'error');
|
const stub = sinon.stub(console, 'error');
|
||||||
const el = /** @type {ValidateElement} */ (await fixture(html`<${tag}></${tag}>`));
|
const el = /** @type {ValidateElement} */ (await fixture(html`<${tag}></${tag}>`));
|
||||||
|
|
@ -93,7 +93,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
stub.restore();
|
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
|
// we throw and console error to improve DX
|
||||||
const stub = sinon.stub(console, 'error');
|
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() {}".`;
|
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
|
// @ts-ignore [allow-private] in test
|
||||||
const totalValidationResult = el.__validationResult;
|
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`
|
await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.modelValue=${'123'}
|
.modelValue=${'123'}
|
||||||
.validators=${[new MinLength(3, { message: 'foo' })]}>
|
.validators=${[new MinLength(3, { getMessage: async () => 'foo' })]}>
|
||||||
<input slot="input">
|
<input slot="input">
|
||||||
</${tag}>`)
|
</${tag}>`)
|
||||||
);
|
);
|
||||||
|
|
@ -1352,14 +1355,14 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
/**
|
||||||
|
* @param {string} type
|
||||||
|
*/
|
||||||
_showFeedbackConditionFor(type) {
|
_showFeedbackConditionFor(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'error':
|
case 'error':
|
||||||
// @ts-ignore
|
|
||||||
return ['A', 'B'].includes(this.modelValue);
|
return ['A', 'B'].includes(this.modelValue);
|
||||||
default:
|
default:
|
||||||
// @ts-ignore
|
|
||||||
return ['B', 'C'].includes(this.modelValue);
|
return ['B', 'C'].includes(this.modelValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1384,12 +1387,10 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
['D', []],
|
['D', []],
|
||||||
]) {
|
]) {
|
||||||
el.modelValue = modelValue;
|
el.modelValue = modelValue;
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-expect-error [allow-protected]
|
||||||
const resultOrder = el._feedbackNode.feedbackData.map(v => v.type);
|
const resultOrder = el._feedbackNode.feedbackData.map(v => v.type);
|
||||||
expect(resultOrder).to.deep.equal(expected);
|
expect(resultOrder).to.deep.equal(expected);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ import sinon from 'sinon';
|
||||||
import { DefaultSuccess, MinLength, Required, ValidateMixin, Validator } from '../index.js';
|
import { DefaultSuccess, MinLength, Required, ValidateMixin, Validator } from '../index.js';
|
||||||
import { AlwaysInvalid } from '../test-helpers/index.js';
|
import { AlwaysInvalid } from '../test-helpers/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../src/validate/types').FeedbackMessageData} FeedbackMessageData
|
||||||
|
*/
|
||||||
|
|
||||||
export function runValidateMixinFeedbackPart() {
|
export function runValidateMixinFeedbackPart() {
|
||||||
describe('Validity Feedback', () => {
|
describe('Validity Feedback', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -303,7 +307,7 @@ export function runValidateMixinFeedbackPart() {
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
|
.validators=${[new MinLength(3, { getMessage: async () => 'custom via config' })]}
|
||||||
>${lightDom}</${tag}>
|
>${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 () => {
|
it('".getMessage()" gets a reference to formControl, params, modelValue and type', async () => {
|
||||||
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||||
static get validationTypes() {
|
static get validationTypes() {
|
||||||
|
|
@ -431,6 +435,7 @@ export function runValidateMixinFeedbackPart() {
|
||||||
fieldName: '',
|
fieldName: '',
|
||||||
type: 'x',
|
type: 'x',
|
||||||
name: 'MinLength',
|
name: 'MinLength',
|
||||||
|
outcome: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const instanceMessageSpy = sinon.spy();
|
const instanceMessageSpy = sinon.spy();
|
||||||
|
|
@ -457,6 +462,7 @@ export function runValidateMixinFeedbackPart() {
|
||||||
fieldName: '',
|
fieldName: '',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
name: 'MinLength',
|
name: 'MinLength',
|
||||||
|
outcome: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -485,41 +491,85 @@ export function runValidateMixinFeedbackPart() {
|
||||||
fieldName: 'myField',
|
fieldName: 'myField',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
name: 'MinLength',
|
name: 'MinLength',
|
||||||
|
outcome: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('".getMessage()" gets .fieldName defined on Validator config', async () => {
|
it('".getMessage()" gets .fieldName defined on Validator config', async () => {
|
||||||
const constructorValidator = new MinLength(4, {
|
const constructorValidator = new MinLength(4, {
|
||||||
fieldName: new Promise(resolve => resolve('myFieldViaCfg')),
|
fieldName: new Promise(resolve => resolve('myFieldViaCfg')),
|
||||||
|
});
|
||||||
|
const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor);
|
||||||
|
const spy = sinon.spy(ctorValidator, 'getMessage');
|
||||||
|
|
||||||
|
const el = /** @type {ValidateElement} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.submitted=${true}
|
||||||
|
.validators=${[constructorValidator]}
|
||||||
|
.modelValue=${'cat'}
|
||||||
|
.fieldName=${new Promise(resolve => resolve('myField'))}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
|
||||||
|
// ignore fieldName Promise as it will always be unique
|
||||||
|
const compare = spy.args[0][0];
|
||||||
|
delete compare?.config?.fieldName;
|
||||||
|
expect(compare).to.eql({
|
||||||
|
config: {},
|
||||||
|
params: 4,
|
||||||
|
modelValue: 'cat',
|
||||||
|
formControl: el,
|
||||||
|
fieldName: 'myFieldViaCfg',
|
||||||
|
type: 'error',
|
||||||
|
name: 'MinLength',
|
||||||
|
outcome: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor);
|
|
||||||
const spy = sinon.spy(ctorValidator, 'getMessage');
|
|
||||||
|
|
||||||
const el = /** @type {ValidateElement} */ (
|
it('".getMessage()" gets .outcome, which can be "true" or an enum', async () => {
|
||||||
await fixture(html`
|
class EnumOutComeValidator extends Validator {
|
||||||
<${tag}
|
static validatorName = 'EnumOutCome';
|
||||||
.submitted=${true}
|
|
||||||
.validators=${[constructorValidator]}
|
|
||||||
.modelValue=${'cat'}
|
|
||||||
.fieldName=${new Promise(resolve => resolve('myField'))}
|
|
||||||
>${lightDom}</${tag}>
|
|
||||||
`)
|
|
||||||
);
|
|
||||||
await el.updateComplete;
|
|
||||||
await el.feedbackComplete;
|
|
||||||
|
|
||||||
// ignore fieldName Promise as it will always be unique
|
execute() {
|
||||||
const compare = spy.args[0][0];
|
return 'a-string-instead-of-bool';
|
||||||
delete compare?.config?.fieldName;
|
}
|
||||||
expect(compare).to.eql({
|
|
||||||
config: {},
|
/**
|
||||||
params: 4,
|
* @param {FeedbackMessageData} meta
|
||||||
modelValue: 'cat',
|
* @returns
|
||||||
formControl: el,
|
*/
|
||||||
fieldName: 'myFieldViaCfg',
|
static async getMessage({ outcome }) {
|
||||||
type: 'error',
|
const results = {
|
||||||
name: 'MinLength',
|
'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