feat: allow Required validator on Fieldset and Form; "static executesOnEmpty" flag on Validators

This commit is contained in:
Thijs Louisse 2023-07-31 22:12:28 +02:00 committed by Thijs Louisse
parent 55d6c7588d
commit 4cc72b1251
6 changed files with 262 additions and 165 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ui': patch
---
feat: allow Required validator on Fieldset and Form; `static executesOnEmpty` flag on Validators

View file

@ -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);

View file

@ -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,82 +414,109 @@ 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.
// 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; 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?
@ -478,47 +525,33 @@ export const ValidateMixinImplementation = superclass =>
})) }))
.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(
v.onFormControlDisconnect(
/** @type {import('../../types/FormControlMixinTypes.js').FormControlHost} */ ( /** @type {import('../../types/FormControlMixinTypes.js').FormControlHost} */ (
/** @type {unknown} */ (this) /** @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
/**
* Updated the code to fix issue #1607 to sync the calendar date with validators params
* Here _onValidatorUpdated is responsible for responding to the event * 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: v }); this._onValidatorUpdated(e, { validator: validatorToSetup });
}); });
} }
}); 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)
);
} }
/** /**

View file

@ -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`

View file

@ -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