feat: allow Required validator on Fieldset and Form; "static executesOnEmpty" flag on Validators
This commit is contained in:
parent
55d6c7588d
commit
4cc72b1251
6 changed files with 262 additions and 165 deletions
5
.changeset/sharp-plants-shave.md
Normal file
5
.changeset/sharp-plants-shave.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: allow Required validator on Fieldset and Form; `static executesOnEmpty` flag on Validators
|
||||||
|
|
@ -584,6 +584,18 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
this.__unlinkParentMessages(el);
|
this.__unlinkParentMessages(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override FormControlMixin
|
||||||
|
*/
|
||||||
|
_isEmpty() {
|
||||||
|
for (const el of this.formElements) {
|
||||||
|
if (!el._isEmpty?.()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FormGroupMixin = dedupeMixin(FormGroupMixinImplementation);
|
export const FormGroupMixin = dedupeMixin(FormGroupMixinImplementation);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { AsyncQueue } from '../utils/AsyncQueue.js';
|
||||||
import { pascalCase } from '../utils/pascalCase.js';
|
import { pascalCase } from '../utils/pascalCase.js';
|
||||||
import { SyncUpdatableMixin } from '../utils/SyncUpdatableMixin.js';
|
import { SyncUpdatableMixin } from '../utils/SyncUpdatableMixin.js';
|
||||||
import { LionValidationFeedback } from './LionValidationFeedback.js';
|
import { LionValidationFeedback } from './LionValidationFeedback.js';
|
||||||
import { ResultValidator } from './ResultValidator.js';
|
import { ResultValidator as MetaValidator } from './ResultValidator.js';
|
||||||
import { Unparseable } from './Unparseable.js';
|
import { Unparseable } from './Unparseable.js';
|
||||||
import { Validator } from './Validator.js';
|
import { Validator } from './Validator.js';
|
||||||
import { Required } from './validators/Required.js';
|
import { Required } from './validators/Required.js';
|
||||||
|
|
@ -20,6 +20,7 @@ import { FormControlMixin } from '../FormControlMixin.js';
|
||||||
* @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidateMixin} ValidateMixin
|
* @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidateMixin} ValidateMixin
|
||||||
* @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidationType} ValidationType
|
* @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidationType} ValidationType
|
||||||
* @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidateHost} ValidateHost
|
* @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidateHost} ValidateHost
|
||||||
|
* @typedef {import('../../types/validate/index.js').ValidatorOutcome} ValidatorOutcome
|
||||||
* @typedef {typeof import('../../types/validate/ValidateMixinTypes.js').ValidateHost} ValidateHostConstructor
|
* @typedef {typeof import('../../types/validate/ValidateMixinTypes.js').ValidateHost} ValidateHostConstructor
|
||||||
* @typedef {{validator:Validator; outcome:boolean|string}} ValidationResultEntry
|
* @typedef {{validator:Validator; outcome:boolean|string}} ValidationResultEntry
|
||||||
* @typedef {{[type:string]: {[validatorName:string]:boolean|string}}} ValidationStates
|
* @typedef {{[type:string]: {[validatorName:string]:boolean|string}}} ValidationStates
|
||||||
|
|
@ -33,6 +34,15 @@ function arrayDiff(array1 = [], array2 = []) {
|
||||||
return array1.filter(x => !array2.includes(x)).concat(array2.filter(x => !array1.includes(x)));
|
return array1.filter(x => !array2.includes(x)).concat(array2.filter(x => !array1.includes(x)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the modelValue can't be created by FormatMixin.parser, still allow all validators
|
||||||
|
* to give valuable feedback to the user based on the current viewValue.
|
||||||
|
* @param {any} modelValue
|
||||||
|
*/
|
||||||
|
function getValueForValidators(modelValue) {
|
||||||
|
return modelValue instanceof Unparseable ? modelValue.viewValue : modelValue;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles all validation, based on modelValue changes. It has no knowledge about dom and
|
* Handles all validation, based on modelValue changes. It has no knowledge about dom and
|
||||||
* UI. All error visibility, dom interaction and accessibility are handled in FeedbackMixin.
|
* UI. All error visibility, dom interaction and accessibility are handled in FeedbackMixin.
|
||||||
|
|
@ -46,17 +56,13 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
SyncUpdatableMixin(DisabledMixin(SlotMixin(ScopedElementsMixin(superclass)))),
|
SyncUpdatableMixin(DisabledMixin(SlotMixin(ScopedElementsMixin(superclass)))),
|
||||||
) {
|
) {
|
||||||
static get scopedElements() {
|
static get scopedElements() {
|
||||||
const scopedElementsCtor =
|
|
||||||
/** @type {typeof import('@open-wc/scoped-elements/types.js').ScopedElementsHost} */ (
|
|
||||||
super.constructor
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
...scopedElementsCtor.scopedElements,
|
// @ts-ignore
|
||||||
|
...super.scopedElements,
|
||||||
'lion-validation-feedback': LionValidationFeedback,
|
'lion-validation-feedback': LionValidationFeedback,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {any} */
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
validators: { attribute: false },
|
validators: { attribute: false },
|
||||||
|
|
@ -93,8 +99,8 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @overridable
|
|
||||||
* Adds "._feedbackNode" as described below
|
* Adds "._feedbackNode" as described below
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
get slots() {
|
get slots() {
|
||||||
/**
|
/**
|
||||||
|
|
@ -229,7 +235,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
this.__asyncValidationResult = [];
|
this.__asyncValidationResult = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aggregated result from sync Validators, async Validators and ResultValidators
|
* Aggregated result from sync Validators, async Validators and MetaValidators
|
||||||
* @type {ValidationResultEntry[]}
|
* @type {ValidationResultEntry[]}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
@ -242,6 +248,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
this.__prevValidationResult = [];
|
this.__prevValidationResult = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* The shown validation result depends on the visibility of the feedback messages
|
||||||
* @type {ValidationResultEntry[]}
|
* @type {ValidationResultEntry[]}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
@ -279,7 +286,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
*/
|
*/
|
||||||
firstUpdated(changedProperties) {
|
firstUpdated(changedProperties) {
|
||||||
super.firstUpdated(changedProperties);
|
super.firstUpdated(changedProperties);
|
||||||
this.__validateInitialized = true;
|
this.__isValidateInitialized = true;
|
||||||
this.validate();
|
this.validate();
|
||||||
if (this._repropagationRole !== 'child') {
|
if (this._repropagationRole !== 'child') {
|
||||||
this.addEventListener('model-value-changed', () => {
|
this.addEventListener('model-value-changed', () => {
|
||||||
|
|
@ -360,8 +367,8 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
* Executions are scheduled and awaited and the 'async results' are merged with the
|
* Executions are scheduled and awaited and the 'async results' are merged with the
|
||||||
* 'sync results'.
|
* 'sync results'.
|
||||||
*
|
*
|
||||||
* - b) there are ResultValidators. After steps a1, a2, or a3 are finished, the holistic
|
* - b) there are MetaValidators. After steps a1, a2, or a3 are finished, the holistic
|
||||||
* ResultValidators (evaluating the total result of the 'regular' (a1, a2 and a3) validators)
|
* MetaValidators (evaluating the total result of the 'regular' (a1, a2 and a3) validators)
|
||||||
* will be run...
|
* will be run...
|
||||||
*
|
*
|
||||||
* Situations a2 and a3 are not mutually exclusive and can be triggered within one `validate()`
|
* Situations a2 and a3 are not mutually exclusive and can be triggered within one `validate()`
|
||||||
|
|
@ -369,14 +376,27 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
*
|
*
|
||||||
* @param {{ clearCurrentResult?: boolean }} opts
|
* @param {{ clearCurrentResult?: boolean }} opts
|
||||||
*/
|
*/
|
||||||
async validate({ clearCurrentResult } = {}) {
|
async validate({ clearCurrentResult = false } = {}) {
|
||||||
|
/**
|
||||||
|
* Allows Application Developer to wait for (async) validation
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* await el.validateComplete;
|
||||||
|
* ```
|
||||||
|
* @type {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
this.validateComplete = new Promise(resolve => {
|
||||||
|
this.__validateCompleteResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
this.__clearValidationResults();
|
this.__clearValidationResults();
|
||||||
this.__finishValidation({ source: 'sync', hasAsync: true });
|
this.__finishValidationPass();
|
||||||
this._updateFeedbackComponent();
|
this._updateFeedbackComponent();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.__validateInitialized) {
|
// We don't validate before firstUpdated has run
|
||||||
|
if (!this.__isValidateInitialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -394,131 +414,144 @@ 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() {
|
||||||
/**
|
const value = getValueForValidators(this.modelValue);
|
||||||
* Allows Application Developer to wait for (async) validation
|
|
||||||
* @example
|
|
||||||
* ```js
|
|
||||||
* await el.validateComplete;
|
|
||||||
* ```
|
|
||||||
* @type {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
this.validateComplete = new Promise(resolve => {
|
|
||||||
this.__validateCompleteResolve = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
// When the modelValue can't be created by FormatMixin.parser, still allow all validators
|
|
||||||
// to give valuable feedback to the user based on the current viewValue.
|
|
||||||
const value =
|
|
||||||
this.modelValue instanceof Unparseable ? this.modelValue.viewValue : this.modelValue;
|
|
||||||
|
|
||||||
/** @type {Validator | undefined} */
|
|
||||||
const requiredValidator = this._allValidators.find(v => v instanceof Required);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Handle the 'exceptional' Required validator:
|
* Handle the 'mutually exclusive' Required validator:
|
||||||
* - the validatity is dependent on the formControl type and therefore determined
|
*
|
||||||
* by the formControl.__isEmpty method. Basically, the Required Validator is a means
|
* About the Required Validator:
|
||||||
* to trigger formControl.__isEmpty.
|
* - the validity is dependent on the formControls' modelValue and therefore determined
|
||||||
* - when __isEmpty returns true, the input was empty. This means we need to stop
|
* by the formControl._isEmpty method. Basically, the Required Validator is a means
|
||||||
|
* to trigger formControl._isEmpty().
|
||||||
|
*
|
||||||
|
* About the empty state:
|
||||||
|
* - when _isEmpty returns true, the modelValue is considered empty. This means we need to stop
|
||||||
* validation here, because all other Validators' execute functions assume the
|
* validation here, because all other Validators' execute functions assume the
|
||||||
* value is not empty (there would be nothing to validate).
|
* value is not empty (there would be nothing to validate).
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
// TODO: Try to remove this when we have a single lion form core package, because then we can
|
|
||||||
// depend on FormControlMixin directly, and _isEmpty will always be an existing method on the prototype then
|
|
||||||
const isEmpty = this.__isEmpty(value);
|
const isEmpty = this.__isEmpty(value);
|
||||||
|
|
||||||
|
this.__syncValidationResult = [];
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
|
const requiredValidator = this._allValidators.find(v => v instanceof Required);
|
||||||
if (requiredValidator) {
|
if (requiredValidator) {
|
||||||
this.__syncValidationResult = [{ validator: requiredValidator, outcome: true }];
|
this.__syncValidationResult = [{ validator: requiredValidator, outcome: true }];
|
||||||
}
|
}
|
||||||
this.__finishValidation({ source: 'sync' });
|
// TODO: get rid of _isFormOrFieldset in a breaking future update.
|
||||||
return;
|
// For now, We should keep forms and fieldsets backwards compatible...
|
||||||
|
const validatorsThatShouldRunOnEmpty = this._allValidators.filter(
|
||||||
|
v => v.constructor.executesOnEmpty,
|
||||||
|
);
|
||||||
|
const shouldHaltValidationOnEmpty =
|
||||||
|
!validatorsThatShouldRunOnEmpty.length && !this._isFormOrFieldset;
|
||||||
|
|
||||||
|
if (shouldHaltValidationOnEmpty) {
|
||||||
|
this.__finishValidationPass({
|
||||||
|
syncValidationResult: this.__syncValidationResult,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate Validators in sync and async
|
const metaValidators = /** @type {MetaValidator[]} */ [];
|
||||||
const /** @type {Validator[]} */ filteredValidators = this._allValidators.filter(
|
const syncValidators = /** @type {Validator[]} */ [];
|
||||||
v => !(v instanceof ResultValidator) && !(v instanceof Required),
|
const asyncValidators = /** @type {Validator[]} */ [];
|
||||||
);
|
|
||||||
const /** @type {Validator[]} */ syncValidators = filteredValidators.filter(v => {
|
for (const v of this._allValidators) {
|
||||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
if (v instanceof MetaValidator) {
|
||||||
return !vCtor.async;
|
metaValidators.push(v);
|
||||||
});
|
} else if (v instanceof Required) {
|
||||||
const /** @type {Validator[]} */ asyncValidators = filteredValidators.filter(v => {
|
// syncValidators.unshift(v);
|
||||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
// Required validator was already handled
|
||||||
return vCtor.async;
|
} else if (/** @type {typeof Validator} */ (v.constructor).async) {
|
||||||
});
|
asyncValidators.push(v);
|
||||||
|
} else {
|
||||||
|
syncValidators.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAsync = Boolean(asyncValidators.length);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2. Synchronous validators
|
* 2. Synchronous validators
|
||||||
*/
|
*/
|
||||||
this.__executeSyncValidators(syncValidators, value, {
|
|
||||||
hasAsync: Boolean(asyncValidators.length),
|
this.__syncValidationResult = [
|
||||||
|
// This could eventually contain the Required Validator
|
||||||
|
...this.__syncValidationResult,
|
||||||
|
...this.__executeSyncValidators(syncValidators, value),
|
||||||
|
];
|
||||||
|
// Finish the first (synchronous) pass
|
||||||
|
this.__finishValidationPass({
|
||||||
|
syncValidationResult: this.__syncValidationResult,
|
||||||
|
metaValidators,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 3. Asynchronous validators
|
* 3. Asynchronous validators
|
||||||
*/
|
*/
|
||||||
await this.__executeAsyncValidators(asyncValidators, value);
|
if (hasAsync) {
|
||||||
|
// Add a hint in the ui that we're waiting for async validation
|
||||||
|
this.isPending = true;
|
||||||
|
this.__asyncValidationResult = await this.__executeAsyncValidators(asyncValidators, value);
|
||||||
|
this.isPending = false;
|
||||||
|
// Now finish the second (asynchronous) pass (including both sync and async results)
|
||||||
|
this.__finishValidationPass({
|
||||||
|
syncValidationResult: this.__syncValidationResult,
|
||||||
|
asyncValidationResult: this.__asyncValidationResult,
|
||||||
|
metaValidators,
|
||||||
|
});
|
||||||
|
this.__validateCompleteResolve?.(true);
|
||||||
|
} else {
|
||||||
|
this.__validateCompleteResolve?.(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* step a2 (as explained in `validate()`): calls `__finishValidation`
|
* step a2 (as explained in `validate()`): calls `__finishValidationPass`
|
||||||
* @param {Validator[]} syncValidators
|
* @param {Validator[]} syncValidators
|
||||||
* @param {unknown} value
|
* @param {unknown} value
|
||||||
* @param {{ hasAsync: boolean }} opts
|
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
__executeSyncValidators(syncValidators, value, { hasAsync }) {
|
__executeSyncValidators(syncValidators, value) {
|
||||||
if (syncValidators.length) {
|
return syncValidators
|
||||||
this.__syncValidationResult = syncValidators
|
.map(v => ({
|
||||||
.map(v => ({
|
validator: v,
|
||||||
validator: v,
|
// TODO: fix this type - ts things this is not a FormControlHost?
|
||||||
// TODO: fix this type - ts things this is not a FormControlHost?
|
// @ts-ignore
|
||||||
// @ts-ignore
|
outcome: /** @type {boolean|string} */ (v.execute(value, v.param, { node: this })),
|
||||||
outcome: /** @type {boolean|string} */ (v.execute(value, v.param, { node: this })),
|
}))
|
||||||
}))
|
.filter(v => Boolean(v.outcome));
|
||||||
.filter(v => Boolean(v.outcome));
|
|
||||||
}
|
|
||||||
this.__finishValidation({ source: 'sync', hasAsync });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* step a3 (as explained in `validate()`), calls __finishValidation
|
* step a3 (as explained in `validate()`), calls __finishValidationPass
|
||||||
* @param {Validator[]} asyncValidators all Validators except required and ResultValidators
|
* @param {Validator[]} asyncValidators all Validators except required and MetaValidators
|
||||||
* @param {?} value
|
* @param {?} value
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async __executeAsyncValidators(asyncValidators, value) {
|
async __executeAsyncValidators(asyncValidators, value) {
|
||||||
if (asyncValidators.length) {
|
const outcomePromises = asyncValidators.map(v => v.execute(value, v.param, { node: this }));
|
||||||
this.isPending = true;
|
const asyncExecutionResults = await Promise.all(outcomePromises);
|
||||||
const resultPromises = asyncValidators.map(v => v.execute(value, v.param, { node: this }));
|
|
||||||
const asyncExecutionResults = await Promise.all(resultPromises);
|
|
||||||
|
|
||||||
this.__asyncValidationResult = asyncExecutionResults
|
return asyncExecutionResults
|
||||||
.map((r, i) => ({
|
.map((r, i) => ({
|
||||||
validator: asyncValidators[i],
|
validator: asyncValidators[i],
|
||||||
outcome: /** @type {boolean|string} */ (asyncExecutionResults[i]),
|
outcome: /** @type {boolean|string} */ (asyncExecutionResults[i]),
|
||||||
}))
|
}))
|
||||||
.filter(v => Boolean(v.outcome));
|
.filter(v => Boolean(v.outcome));
|
||||||
|
|
||||||
this.__finishValidation({ source: 'async' });
|
|
||||||
this.isPending = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* step b (as explained in `validate()`), called by __finishValidation
|
* step b (as explained in `validate()`), called by __finishValidationPass
|
||||||
* @param {{validator: Validator;outcome: boolean | string;}[]} regularValidationResult result of steps 1-3
|
* @param {{validator: Validator;outcome: boolean | string;}[]} regularValidationResult result of steps 1-3
|
||||||
|
* @param {MetaValidator[]} metaValidators
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
__executeResultValidators(regularValidationResult) {
|
__executeMetaValidators(regularValidationResult, metaValidators) {
|
||||||
const resultValidators = /** @type {ResultValidator[]} */ (
|
if (!metaValidators.length) {
|
||||||
this._allValidators.filter(v => {
|
|
||||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
|
||||||
return !vCtor.async && v instanceof ResultValidator;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!resultValidators.length) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -529,7 +562,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map everything to Validator[] for backwards compatibility
|
// Map everything to Validator[] for backwards compatibility
|
||||||
return resultValidators
|
return metaValidators
|
||||||
.map(v => ({
|
.map(v => ({
|
||||||
validator: v,
|
validator: v,
|
||||||
outcome: /** @type {boolean|string} */ (
|
outcome: /** @type {boolean|string} */ (
|
||||||
|
|
@ -546,19 +579,35 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* A 'pass' is a single run of the validation process, which will be triggered in these cases:
|
||||||
|
* - on clear or disable
|
||||||
|
* - on sync validation
|
||||||
|
* - on async validation (can depend on server response)
|
||||||
|
*
|
||||||
|
* This method inishes a pass by adding the properties to the instance:
|
||||||
|
* - validationStates
|
||||||
|
* - hasFeedbackFor
|
||||||
|
*
|
||||||
|
* It sends a private event validate-performed, which is received by parent Formgroups.
|
||||||
|
*
|
||||||
* @param {object} options
|
* @param {object} options
|
||||||
* @param {'sync'|'async'} options.source
|
* @param {ValidationResultEntry[]} [options.syncValidationResult]
|
||||||
* @param {boolean} [options.hasAsync] whether async validators are configured in this run.
|
* @param {ValidationResultEntry[]} [options.asyncValidationResult]
|
||||||
|
* @param {MetaValidator[]} [options.metaValidators] MetaValidators to be executed
|
||||||
* @private
|
* @private
|
||||||
* If not, we have nothing left to wait for.
|
* If not, we have nothing left to wait for.
|
||||||
*/
|
*/
|
||||||
__finishValidation({ source, hasAsync }) {
|
__finishValidationPass({
|
||||||
const syncAndAsyncOutcome = [...this.__syncValidationResult, ...this.__asyncValidationResult];
|
syncValidationResult = [],
|
||||||
// if we have any ResultValidators left, now is the time to run them...
|
asyncValidationResult = [],
|
||||||
const resultOutCome = /** @type {ValidationResultEntry[]} */ (
|
metaValidators = [],
|
||||||
this.__executeResultValidators(syncAndAsyncOutcome)
|
} = {}) {
|
||||||
|
const syncAndAsyncOutcome = [...syncValidationResult, ...asyncValidationResult];
|
||||||
|
// if we have any MetaValidators left, now is the time to run them...
|
||||||
|
const metaOutCome = /** @type {ValidationResultEntry[]} */ (
|
||||||
|
this.__executeMetaValidators(syncAndAsyncOutcome, metaValidators)
|
||||||
);
|
);
|
||||||
this.__validationResult = [...resultOutCome, ...syncAndAsyncOutcome];
|
this.__validationResult = [...metaOutCome, ...syncAndAsyncOutcome];
|
||||||
|
|
||||||
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||||
|
|
||||||
|
|
@ -567,13 +616,13 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
(acc, type) => ({ ...acc, [type]: {} }),
|
(acc, type) => ({ ...acc, [type]: {} }),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
this.__validationResult.forEach(({ validator, outcome }) => {
|
for (const { validator, outcome } of this.__validationResult) {
|
||||||
if (!validationStates[validator.type]) {
|
if (!validationStates[validator.type]) {
|
||||||
validationStates[validator.type] = {};
|
validationStates[validator.type] = {};
|
||||||
}
|
}
|
||||||
const vCtor = /** @type {typeof Validator} */ (validator.constructor);
|
const vCtor = /** @type {typeof Validator} */ (validator.constructor);
|
||||||
validationStates[validator.type][vCtor.validatorName] = outcome;
|
validationStates[validator.type][vCtor.validatorName] = outcome;
|
||||||
});
|
}
|
||||||
this.validationStates = validationStates;
|
this.validationStates = validationStates;
|
||||||
|
|
||||||
this.hasFeedbackFor = [
|
this.hasFeedbackFor = [
|
||||||
|
|
@ -581,11 +630,6 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
];
|
];
|
||||||
/** 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 (this.__validateCompleteResolve) {
|
|
||||||
this.__validateCompleteResolve(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -611,51 +655,49 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
*/
|
*/
|
||||||
__setupValidators() {
|
__setupValidators() {
|
||||||
const events = ['param-changed', 'config-changed'];
|
const events = ['param-changed', 'config-changed'];
|
||||||
if (this.__prevValidators) {
|
|
||||||
this.__prevValidators.forEach(v => {
|
for (const validatorToCleanup of this.__prevValidators || []) {
|
||||||
events.forEach(e => {
|
for (const eventToCleanup of events) {
|
||||||
if (v.removeEventListener) {
|
validatorToCleanup.removeEventListener?.(eventToCleanup, this._onValidatorUpdated);
|
||||||
v.removeEventListener(e, this._onValidatorUpdated);
|
}
|
||||||
}
|
validatorToCleanup.onFormControlDisconnect(
|
||||||
});
|
/** @type {import('../../types/FormControlMixinTypes.js').FormControlHost} */ (
|
||||||
v.onFormControlDisconnect(
|
/** @type {unknown} */ (this)
|
||||||
/** @type {import('../../types/FormControlMixinTypes.js').FormControlHost} */ (
|
),
|
||||||
/** @type {unknown} */ (this)
|
);
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this._allValidators.forEach(v => {
|
|
||||||
if (!(v instanceof Validator)) {
|
for (const validatorToSetup of this._allValidators) {
|
||||||
|
if (!(validatorToSetup instanceof Validator)) {
|
||||||
// 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
|
||||||
const errorType = Array.isArray(v) ? 'array' : typeof v;
|
const errorType = Array.isArray(validatorToSetup) ? 'array' : typeof validatorToSetup;
|
||||||
const errorMessage = `Validators array only accepts class instances of Validator. Type "${errorType}" found. This may be caused by having multiple installations of @lion/form-core.`;
|
const errorMessage = `Validators array only accepts class instances of Validator. Type "${errorType}" found. This may be caused by having multiple installations of "@lion/ui/form-core.js".`;
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(errorMessage, this);
|
console.error(errorMessage, this);
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
|
||||||
if (ctor.validationTypes.indexOf(v.type) === -1) {
|
const vCtor = /** @type {typeof Validator} */ (validatorToSetup.constructor);
|
||||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
if (ctor.validationTypes.indexOf(validatorToSetup.type) === -1) {
|
||||||
// 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
|
||||||
const errorMessage = `This component does not support the validator type "${v.type}" used in "${vCtor.validatorName}". 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 "${validatorToSetup.type}" used in "${vCtor.validatorName}". You may change your validators type or add it to the components "static get validationTypes() {}".`;
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(errorMessage, this);
|
console.error(errorMessage, this);
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
/** Updated the code to fix issue #1607 to sync the calendar date with validators params
|
|
||||||
* Here _onValidatorUpdated is responsible for responding to the event
|
/**
|
||||||
|
* Updated the code to fix issue #1607 to sync the calendar date with validators params
|
||||||
|
* Here _onValidatorUpdated is responsible for responding to the event
|
||||||
*/
|
*/
|
||||||
events.forEach(eventName => {
|
for (const eventToSetup of events) {
|
||||||
if (v.addEventListener) {
|
validatorToSetup.addEventListener?.(eventToSetup, e => {
|
||||||
v.addEventListener(eventName, e => {
|
// @ts-ignore for making validator param dynamic
|
||||||
// @ts-ignore for making validator param dynamic
|
this._onValidatorUpdated(e, { validator: validatorToSetup });
|
||||||
this._onValidatorUpdated(e, { validator: v });
|
});
|
||||||
});
|
}
|
||||||
}
|
validatorToSetup.onFormControlConnect(this);
|
||||||
});
|
}
|
||||||
v.onFormControlConnect(this);
|
|
||||||
});
|
|
||||||
this.__prevValidators = this._allValidators;
|
this.__prevValidators = this._allValidators;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -759,7 +801,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
|
const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
|
||||||
_feedbackNode.feedbackData = messageMap.length ? messageMap : [];
|
_feedbackNode.feedbackData = messageMap || [];
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.__feedbackQueue.add(async () => {
|
this.__feedbackQueue.add(async () => {
|
||||||
|
|
@ -827,12 +869,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_hasFeedbackVisibleFor(type) {
|
_hasFeedbackVisibleFor(type) {
|
||||||
return (
|
return this.hasFeedbackFor?.includes(type) && this.shouldShowFeedbackFor?.includes(type);
|
||||||
this.hasFeedbackFor &&
|
|
||||||
this.hasFeedbackFor.includes(type) &&
|
|
||||||
this.shouldShowFeedbackFor &&
|
|
||||||
this.shouldShowFeedbackFor.includes(type)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
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}>`));
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
'Validators array only accepts class instances of Validator. Type "array" found. This may be caused by having multiple installations of @lion/form-core.';
|
'Validators array only accepts class instances of Validator. Type "array" found. This may be caused by having multiple installations of "@lion/ui/form-core.js".';
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// @ts-expect-error putting the wrong value on purpose
|
// @ts-expect-error putting the wrong value on purpose
|
||||||
el.validators = [[new Required()]];
|
el.validators = [[new Required()]];
|
||||||
|
|
@ -85,7 +85,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
expect(stub.args[0][0]).to.equal(errorMessage);
|
expect(stub.args[0][0]).to.equal(errorMessage);
|
||||||
|
|
||||||
const errorMessage2 =
|
const errorMessage2 =
|
||||||
'Validators array only accepts class instances of Validator. Type "string" found. This may be caused by having multiple installations of @lion/form-core.';
|
'Validators array only accepts class instances of Validator. Type "string" found. This may be caused by having multiple installations of "@lion/ui/form-core.js".';
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// @ts-expect-error because we purposely put a wrong type
|
// @ts-expect-error because we purposely put a wrong type
|
||||||
el.validators = ['required'];
|
el.validators = ['required'];
|
||||||
|
|
@ -322,7 +322,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
// @ts-ignore [allow-private] in test
|
// @ts-ignore [allow-private] in test
|
||||||
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
||||||
// @ts-ignore [allow-private] in test
|
// @ts-ignore [allow-private] in test
|
||||||
const resultSpy2 = sinon.spy(el, '__executeResultValidators');
|
const resultSpy2 = sinon.spy(el, '__executeMetaValidators');
|
||||||
|
|
||||||
el.modelValue = 'nonEmpty';
|
el.modelValue = 'nonEmpty';
|
||||||
expect(syncSpy.calledBefore(resultSpy2)).to.be.true;
|
expect(syncSpy.calledBefore(resultSpy2)).to.be.true;
|
||||||
|
|
@ -337,7 +337,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
// @ts-ignore [allow-private] in test
|
// @ts-ignore [allow-private] in test
|
||||||
const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
|
const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
|
||||||
// @ts-ignore [allow-private] in test
|
// @ts-ignore [allow-private] in test
|
||||||
const resultSpy = sinon.spy(el, '__executeResultValidators');
|
const resultSpy = sinon.spy(el, '__executeMetaValidators');
|
||||||
|
|
||||||
el.modelValue = 'nonEmpty';
|
el.modelValue = 'nonEmpty';
|
||||||
expect(resultSpy.callCount).to.equal(1);
|
expect(resultSpy.callCount).to.equal(1);
|
||||||
|
|
@ -855,6 +855,34 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
expect(alwaysInvalidSpy.callCount).to.equal(1); // __isRequired returned true (valid)
|
expect(alwaysInvalidSpy.callCount).to.equal(1); // __isRequired returned true (valid)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not prevent other Validators from being called when input is empty, but at least one Validator has "executesOnEmpty"', async () => {
|
||||||
|
class AlwaysInvalidExecutingOnEmpty extends Validator {
|
||||||
|
static validatorName = 'AlwaysInvalidExecutingOnEmpty';
|
||||||
|
|
||||||
|
static executesOnEmpty = true;
|
||||||
|
|
||||||
|
execute() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const alwaysInvalidExecutingOnEmpty = new AlwaysInvalidExecutingOnEmpty();
|
||||||
|
const aalwaysInvalidExecutingOnEmptySpy = sinon.spy(
|
||||||
|
alwaysInvalidExecutingOnEmpty,
|
||||||
|
'execute',
|
||||||
|
);
|
||||||
|
const el = /** @type {ValidateElement} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.validators=${[new Required(), alwaysInvalidExecutingOnEmpty]}
|
||||||
|
.modelValue=${''}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
expect(aalwaysInvalidExecutingOnEmptySpy.callCount).to.equal(1);
|
||||||
|
el.modelValue = 'foo';
|
||||||
|
expect(aalwaysInvalidExecutingOnEmptySpy.callCount).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
it('adds [aria-required="true"] to "._inputNode"', async () => {
|
it('adds [aria-required="true"] to "._inputNode"', async () => {
|
||||||
const el = /** @type {ValidateElement} */ (
|
const el = /** @type {ValidateElement} */ (
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
|
|
|
||||||
|
|
@ -313,7 +313,7 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: wait for updated on disconnected to be fixed: https://github.com/lit/lit/issues/1901
|
// TODO: wait for updated on disconnected to be fixed: https://github.com/lit/lit/issues/1901
|
||||||
it.skip(`cleans up feedback message belonging to fieldset on disconnect`, async () => {
|
it.skip(`cleans up feedback message belonging to fieldset on disconnect`, async () => {
|
||||||
const el = await childAriaFixture('feedback');
|
const el = await childAriaFixture('feedback');
|
||||||
await childAriaTest(el, { cleanupPhase: true });
|
await childAriaTest(el, { cleanupPhase: true });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
import { IsNumber, LionField, Validator, FormGroupMixin } from '@lion/ui/form-core.js';
|
import { IsNumber, Required, LionField, Validator, FormGroupMixin } from '@lion/ui/form-core.js';
|
||||||
import '@lion/ui/define/lion-field.js';
|
import '@lion/ui/define/lion-field.js';
|
||||||
import '@lion/ui/define/lion-validation-feedback.js';
|
import '@lion/ui/define/lion-validation-feedback.js';
|
||||||
|
|
||||||
|
|
@ -577,6 +577,25 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
expect(el.validationStates.error).to.deep.equal({});
|
expect(el.validationStates.error).to.deep.equal({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('works with Required', async () => {
|
||||||
|
const el = /** @type {FormGroup} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${tag} name="myGroup" .validators="${[new Required()]}">
|
||||||
|
<${childTag} name="fieldA"></${childTag}>
|
||||||
|
<${childTag} name="fieldB"></${childTag}>
|
||||||
|
</${tag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
// initially the group is invalid
|
||||||
|
expect(el.validationStates.error.Required).to.be.true;
|
||||||
|
el.formElements.fieldA.modelValue = 'foo';
|
||||||
|
// If at least one child is filled, the group is valid
|
||||||
|
expect(el.validationStates.error.Required).to.be.undefined;
|
||||||
|
// // initially the group is invalid
|
||||||
|
el.formElements.fieldA.modelValue = '';
|
||||||
|
expect(el.validationStates.error.Required).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
it('validates on children (de)registration', async () => {
|
it('validates on children (de)registration', async () => {
|
||||||
class HasEvenNumberOfChildren extends Validator {
|
class HasEvenNumberOfChildren extends Validator {
|
||||||
static get validatorName() {
|
static get validatorName() {
|
||||||
|
|
@ -1129,9 +1148,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
|
|
||||||
it('has correct validation afterwards', async () => {
|
it('has correct validation afterwards', async () => {
|
||||||
class IsCat extends Validator {
|
class IsCat extends Validator {
|
||||||
static get validatorName() {
|
static validatorName = 'IsCat';
|
||||||
return 'IsCat';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} value
|
* @param {string} value
|
||||||
|
|
@ -1142,9 +1159,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ColorContainsA extends Validator {
|
class ColorContainsA extends Validator {
|
||||||
static get validatorName() {
|
static validatorName = 'ColorContainsA';
|
||||||
return 'ColorContainsA';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ [x:string]:any }} value
|
* @param {{ [x:string]:any }} value
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue