feat(validate): new validation api, async validation and more
Co-authored-by: Thomas Allmer <Thomas.Allmer@ing.com>
This commit is contained in:
parent
e1485188a0
commit
6e81b55e3c
33 changed files with 3535 additions and 924 deletions
17
packages/validate/docs/FlowDiagram.md
Normal file
17
packages/validate/docs/FlowDiagram.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- markdownlint-disable MD041 -->
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A(value changed) --> validate
|
||||||
|
B(validators changed) --> validate
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
validate --> B{Check value}
|
||||||
|
B -->|is empty| C[Run required validator]
|
||||||
|
B -->|is not empty| syncOrAsync[non empty value]
|
||||||
|
syncOrAsync -->|has sync validators| F[Run sync]
|
||||||
|
syncOrAsync -->|has async validators| G((debounce))
|
||||||
|
G --> H[Run async]
|
||||||
|
```
|
||||||
|
|
@ -1,40 +1,26 @@
|
||||||
export { ValidateMixin } from './src/ValidateMixin.js';
|
export { ValidateMixin } from './src/ValidateMixin.js';
|
||||||
|
export { FeedbackMixin } from './src/FeedbackMixin.js';
|
||||||
export { Unparseable } from './src/Unparseable.js';
|
export { Unparseable } from './src/Unparseable.js';
|
||||||
export { isValidatorApplied } from './src/isValidatorApplied.js';
|
export { Validator } from './src/Validator.js';
|
||||||
|
export { ResultValidator } from './src/ResultValidator.js';
|
||||||
|
|
||||||
|
export { loadDefaultFeedbackMessages } from './src/loadDefaultFeedbackMessages.js';
|
||||||
|
|
||||||
|
export { Required } from './src/validators/Required.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
defaultOk,
|
IsString,
|
||||||
defaultOkValidator,
|
EqualsLength,
|
||||||
isDateDisabled,
|
MinLength,
|
||||||
isDateDisabledValidator,
|
MaxLength,
|
||||||
equalsLength,
|
MinMaxLength,
|
||||||
equalsLengthValidator,
|
IsEmail,
|
||||||
isDate,
|
} from './src/validators/StringValidators.js';
|
||||||
isDateValidator,
|
|
||||||
isEmail,
|
export {
|
||||||
isEmailValidator,
|
IsDate,
|
||||||
isNumber,
|
MinDate,
|
||||||
isNumberValidator,
|
MaxDate,
|
||||||
isString,
|
MinMaxDate,
|
||||||
isStringValidator,
|
IsDateDisabled,
|
||||||
maxDate,
|
} from './src/validators/DateValidators.js';
|
||||||
maxDateValidator,
|
|
||||||
maxLength,
|
|
||||||
maxLengthValidator,
|
|
||||||
maxNumber,
|
|
||||||
maxNumberValidator,
|
|
||||||
minDate,
|
|
||||||
minDateValidator,
|
|
||||||
minLength,
|
|
||||||
minLengthValidator,
|
|
||||||
minMaxDate,
|
|
||||||
minMaxDateValidator,
|
|
||||||
minMaxLength,
|
|
||||||
minMaxLengthValidator,
|
|
||||||
minMaxNumber,
|
|
||||||
minMaxNumberValidator,
|
|
||||||
minNumber,
|
|
||||||
minNumberValidator,
|
|
||||||
randomOk,
|
|
||||||
randomOkValidator,
|
|
||||||
} from './src/validators.js';
|
|
||||||
|
|
|
||||||
3
packages/validate/lion-validation-feedback.js
Normal file
3
packages/validate/lion-validation-feedback.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionValidationFeedback } from './src/LionValidationFeedback.js';
|
||||||
|
|
||||||
|
customElements.define('lion-validation-feedback', LionValidationFeedback);
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
"stories",
|
"stories",
|
||||||
"test",
|
"test",
|
||||||
"test-helpers",
|
"test-helpers",
|
||||||
|
"test-suites",
|
||||||
"translations",
|
"translations",
|
||||||
"*.js"
|
"*.js"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
230
packages/validate/src/FeedbackMixin.js
Normal file
230
packages/validate/src/FeedbackMixin.js
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
/* eslint-disable class-methods-use-this, camelcase, no-param-reassign */
|
||||||
|
|
||||||
|
import { dedupeMixin, SlotMixin } from '@lion/core';
|
||||||
|
import { localize } from '@lion/localize';
|
||||||
|
import { pascalCase } from './utils/pascal-case.js';
|
||||||
|
import { SyncUpdatableMixin } from './utils/SyncUpdatableMixin.js';
|
||||||
|
import '../lion-validation-feedback.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @desc Handles all UI/dom integration with regard to validation reporting,
|
||||||
|
* feedback visibility and accessibility.
|
||||||
|
* Should be used on top of ValidateMixin.
|
||||||
|
*/
|
||||||
|
export const FeedbackMixin = dedupeMixin(
|
||||||
|
superclass =>
|
||||||
|
// eslint-disable-next-line no-unused-vars, no-shadow
|
||||||
|
class FeedbackMixin extends SyncUpdatableMixin(SlotMixin(superclass)) {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* @desc Derived from the result of _prioritizeAndFilterFeedback
|
||||||
|
* @type {boolean}
|
||||||
|
* @example
|
||||||
|
* FormControl.hasError; // => true
|
||||||
|
* FormControl.hasErrorVisible; // => false
|
||||||
|
* // Interaction state changes (for instance: user blurs the field)
|
||||||
|
* FormControl.hasErrorVisible; // => true
|
||||||
|
*/
|
||||||
|
hasErrorVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
attribute: 'has-error-visible',
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Subclassers can enable this to show multiple feedback messages at the same time
|
||||||
|
* By default, just like the platform, only one message (with highest prio) is visible.
|
||||||
|
*/
|
||||||
|
_visibleMessagesAmount: Number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Promise<string>|string} will be passed as an argument to the `.getMessage`
|
||||||
|
* method of a Validator. When filled in, this field namme can be used to enhance
|
||||||
|
* error messages.
|
||||||
|
*/
|
||||||
|
fieldName: String,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* Adds "._feedbackNode" as described below
|
||||||
|
*/
|
||||||
|
get slots() {
|
||||||
|
return {
|
||||||
|
...super.slots,
|
||||||
|
feedback: () => document.createElement('lion-validation-feedback'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* @type {Element} _feedbackNode:
|
||||||
|
* Gets a `FeedbackData` object as its input.
|
||||||
|
* This element can be a custom made (web) component that renders messages in accordance with
|
||||||
|
* the implemented Design System. For instance, it could add an icon in front of a message.
|
||||||
|
* The _feedbackNode is only responsible for the visual rendering part, it should NOT contain
|
||||||
|
* state. All state will be determined by the outcome of `FormControl.filterFeeback()`.
|
||||||
|
* FormControl delegates to individual sub elements and decides who renders what.
|
||||||
|
* For instance, FormControl itself is responsible for reflecting error-state and error-show
|
||||||
|
* to its host element.
|
||||||
|
* This means filtering out messages should happen in FormControl and NOT in `_feedbackNode`
|
||||||
|
*
|
||||||
|
* - gets a FeedbackData object as input
|
||||||
|
* - should know about the FeedbackMessage types('error', 'success' etc.) that the FormControl
|
||||||
|
* (having ValidateMixin applied) returns
|
||||||
|
* - renders result and
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
get _feedbackNode() {
|
||||||
|
return this.querySelector('[slot=feedback]');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @abstract get _inputNode()
|
||||||
|
*/
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.hasErrorVisible = false;
|
||||||
|
this._visibleMessagesAmount = 1;
|
||||||
|
|
||||||
|
this._renderFeedback = this._renderFeedback.bind(this);
|
||||||
|
this.addEventListener('validate-performed', this._renderFeedback);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
// TODO: move to extending layer
|
||||||
|
localize.addEventListener('localeChanged', this._renderFeedback);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
// TODO: move to extending layer
|
||||||
|
localize.removeEventListener('localeChanged', this._renderFeedback);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSync(name, oldValue) {
|
||||||
|
super.updateSync(name, oldValue);
|
||||||
|
|
||||||
|
if (name === 'hasErrorVisible') {
|
||||||
|
// This can't be reflected asynchronously in Safari
|
||||||
|
this.__handleA11yErrorVisible();
|
||||||
|
this[this.hasErrorVisible ? 'setAttribute' : 'removeAttribute']('has-error-visible', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated(c) {
|
||||||
|
super.updated(c);
|
||||||
|
|
||||||
|
// TODO: Interaction state knowledge should be moved to FormControl...
|
||||||
|
['touched', 'dirty', 'submitted', 'prefilled'].forEach(iState => {
|
||||||
|
if (c.has(iState)) {
|
||||||
|
this._renderFeedback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} FeedbackMessage
|
||||||
|
* @property {string} message this
|
||||||
|
* @property {string} type will be 'error' for messages from default Validators. Could be
|
||||||
|
* 'warning', 'info' etc. for Validators with custom types. Needed as a directive for
|
||||||
|
* feedbackNode how to render a message of a certain type
|
||||||
|
* @property {Validator} [validator] when the message is directly coupled to a Validator
|
||||||
|
* (in most cases), this property is filled. When a message is not coupled to a Validator
|
||||||
|
* (in case of success feedback which is based on a diff or current and previous validation
|
||||||
|
* results), this property can be left empty.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Validator[]} validators list of objects having a .getMessage method
|
||||||
|
* @return {FeedbackMessage[]}
|
||||||
|
*/
|
||||||
|
async __getFeedbackMessages(validators) {
|
||||||
|
let fieldName = await this.fieldName;
|
||||||
|
return Promise.all(
|
||||||
|
validators.map(async validator => {
|
||||||
|
if (validator.config.fieldName) {
|
||||||
|
fieldName = await validator.config.fieldName;
|
||||||
|
}
|
||||||
|
const message = await validator._getMessage({
|
||||||
|
validatorParams: validator.param,
|
||||||
|
modelValue: this.modelValue,
|
||||||
|
formControl: this,
|
||||||
|
fieldName,
|
||||||
|
});
|
||||||
|
return { message, type: validator.type, validator };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Responsible for retrieving messages from Validators and
|
||||||
|
* (delegation of) rendering them.
|
||||||
|
*
|
||||||
|
* For `._feedbackNode` (extension of LionValidationFeedback):
|
||||||
|
* - retrieve messages from highest prio Validators
|
||||||
|
* - provide the result to custom feedback node and let the
|
||||||
|
* custom node decide on their renderings
|
||||||
|
*
|
||||||
|
* In both cases:
|
||||||
|
* - we compute the 'show' flag (like 'hasErrorVisible') for all types
|
||||||
|
* - we set the customValidity message of the highest prio Validator
|
||||||
|
* - we set aria-invalid="true" in case hasErrorVisible is true
|
||||||
|
*/
|
||||||
|
async _renderFeedback() {
|
||||||
|
let feedbackCompleteResolve;
|
||||||
|
this.feedbackComplete = new Promise(resolve => {
|
||||||
|
feedbackCompleteResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @type {Validator[]} */
|
||||||
|
this.__prioritizedResult = this._prioritizeAndFilterFeedback({
|
||||||
|
validationResult: this.__validationResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
|
||||||
|
|
||||||
|
this._feedbackNode.feedbackData = messageMap.length ? messageMap : undefined;
|
||||||
|
this.__storeTypeVisibilityOnInstance(this.__prioritizedResult);
|
||||||
|
feedbackCompleteResolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
__storeTypeVisibilityOnInstance(prioritizedValidators) {
|
||||||
|
const result = {};
|
||||||
|
this.__validatorTypeHistoryCache.forEach(previouslyStoredType => {
|
||||||
|
result[`has${pascalCase(previouslyStoredType)}Visible`] = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
prioritizedValidators.forEach(v => {
|
||||||
|
result[`has${pascalCase(v.type)}Visible`] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(this, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* @desc Orders all active validators in this.__validationResult. Can
|
||||||
|
* also filter out occurrences (based on interaction states)
|
||||||
|
* @returns {Validator[]} ordered list of Validators with feedback messages visible to the
|
||||||
|
* end user
|
||||||
|
*/
|
||||||
|
_prioritizeAndFilterFeedback({ validationResult }) {
|
||||||
|
const types = this.constructor.validationTypes;
|
||||||
|
// Sort all validators based on the type provided.
|
||||||
|
const res = validationResult.sort((a, b) => types.indexOf(a.type) - types.indexOf(b.type));
|
||||||
|
return res.slice(0, this._visibleMessagesAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
__handleA11yErrorVisible() {
|
||||||
|
// Screen reader output should be in sync with visibility of error messages
|
||||||
|
if (this._inputNode) {
|
||||||
|
this._inputNode.setAttribute('aria-invalid', this.hasErrorVisible);
|
||||||
|
// this._inputNode.setCustomValidity(this._validationMessage || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
35
packages/validate/src/LionValidationFeedback.js
Normal file
35
packages/validate/src/LionValidationFeedback.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { html, LitElement } from '@lion/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Takes care of accessible rendering of error messages
|
||||||
|
* Should be used in conjunction with FormControl having ValidateMixin applied
|
||||||
|
*/
|
||||||
|
export class LionValidationFeedback extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* @property {FeedbackData} feedbackData
|
||||||
|
*/
|
||||||
|
feedbackData: Array,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_messageTemplate({ message }) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
${this.feedbackData &&
|
||||||
|
this.feedbackData.map(
|
||||||
|
({ message, type, validator }) => html`
|
||||||
|
${this._messageTemplate({ message, type, validator })}
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/validate/src/ResultValidator.js
Normal file
18
packages/validate/src/ResultValidator.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Validator } from './Validator.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Instead of evaluating the result of a regular validator, a HolisticValidator looks
|
||||||
|
* at the total result of regular Validators. Instead of an execute function, it uses a
|
||||||
|
* 'executeOnResults' Validator.
|
||||||
|
* ResultValidators cannot be async, and should noy contain an execute method.
|
||||||
|
*/
|
||||||
|
export class ResultValidator extends Validator {
|
||||||
|
/**
|
||||||
|
* @param {object} context
|
||||||
|
* @param {Validator[]} context.validationResult
|
||||||
|
* @param {Validator[]} context.prevValidationResult
|
||||||
|
* @param {Validator[]} context.validators
|
||||||
|
* @returns {Feedback[]}
|
||||||
|
*/
|
||||||
|
executeOnResults({ validationResult, prevValidationResult, validators }) {} // eslint-disable-line
|
||||||
|
}
|
||||||
|
|
@ -1,662 +1,399 @@
|
||||||
/* eslint-disable class-methods-use-this, camelcase, no-param-reassign */
|
/* eslint-disable class-methods-use-this, camelcase, no-param-reassign, max-classes-per-file */
|
||||||
|
|
||||||
import { dedupeMixin, SlotMixin } from '@lion/core';
|
import { dedupeMixin } from '@lion/core';
|
||||||
import { localize, LocalizeMixin } from '@lion/localize';
|
|
||||||
import { Unparseable } from './Unparseable.js';
|
import { Unparseable } from './Unparseable.js';
|
||||||
import { randomOk } from './validators.js';
|
import { pascalCase } from './utils/pascal-case.js';
|
||||||
|
import { Required } from './validators/Required.js';
|
||||||
// TODO: extract from module like import { pascalCase } from 'lion-element/CaseMapUtils.js'
|
import { ResultValidator } from './ResultValidator.js';
|
||||||
const pascalCase = str => str.charAt(0).toUpperCase() + str.slice(1);
|
import { SyncUpdatableMixin } from './utils/SyncUpdatableMixin.js';
|
||||||
|
|
||||||
/* @polymerMixin */
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 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.
|
||||||
|
*
|
||||||
|
* @event error-state-changed fires when FormControl goes from non-error to error state and vice versa
|
||||||
|
* @event error-changed fires when the Validator(s) leading to the error state, change
|
||||||
|
*/
|
||||||
export const ValidateMixin = dedupeMixin(
|
export const ValidateMixin = dedupeMixin(
|
||||||
superclass =>
|
superclass =>
|
||||||
// eslint-disable-next-line no-unused-vars, no-shadow, max-len
|
// eslint-disable-next-line no-unused-vars, no-shadow
|
||||||
class ValidateMixin extends LocalizeMixin(SlotMixin(superclass)) {
|
class ValidateMixin extends SyncUpdatableMixin(superclass) {
|
||||||
/* * * * * * * * * *
|
|
||||||
Configuration */
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.__oldValues = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
get slots() {
|
|
||||||
return {
|
|
||||||
...super.slots,
|
|
||||||
feedback: () => document.createElement('div'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get localizeNamespaces() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
/* FIXME: This awful switch statement is used to make sure it works with polymer build.. */
|
|
||||||
'lion-validate': locale => {
|
|
||||||
switch (locale) {
|
|
||||||
case 'bg-BG':
|
|
||||||
return import('../translations/bg-BG.js');
|
|
||||||
case 'bg':
|
|
||||||
return import('../translations/bg.js');
|
|
||||||
case 'cs-CZ':
|
|
||||||
return import('../translations/cs-CZ.js');
|
|
||||||
case 'cs':
|
|
||||||
return import('../translations/cs.js');
|
|
||||||
case 'de-DE':
|
|
||||||
return import('../translations/de-DE.js');
|
|
||||||
case 'de':
|
|
||||||
return import('../translations/de.js');
|
|
||||||
case 'en-AU':
|
|
||||||
return import('../translations/en-AU.js');
|
|
||||||
case 'en-GB':
|
|
||||||
return import('../translations/en-GB.js');
|
|
||||||
case 'en-US':
|
|
||||||
return import('../translations/en-US.js');
|
|
||||||
case 'en-PH':
|
|
||||||
case 'en':
|
|
||||||
return import('../translations/en.js');
|
|
||||||
case 'es-ES':
|
|
||||||
return import('../translations/es-ES.js');
|
|
||||||
case 'es':
|
|
||||||
return import('../translations/es.js');
|
|
||||||
case 'fr-FR':
|
|
||||||
return import('../translations/fr-FR.js');
|
|
||||||
case 'fr-BE':
|
|
||||||
return import('../translations/fr-BE.js');
|
|
||||||
case 'fr':
|
|
||||||
return import('../translations/fr.js');
|
|
||||||
case 'hu-HU':
|
|
||||||
return import('../translations/hu-HU.js');
|
|
||||||
case 'hu':
|
|
||||||
return import('../translations/hu.js');
|
|
||||||
case 'it-IT':
|
|
||||||
return import('../translations/it-IT.js');
|
|
||||||
case 'it':
|
|
||||||
return import('../translations/it.js');
|
|
||||||
case 'nl-BE':
|
|
||||||
return import('../translations/nl-BE.js');
|
|
||||||
case 'nl-NL':
|
|
||||||
return import('../translations/nl-NL.js');
|
|
||||||
case 'nl':
|
|
||||||
return import('../translations/nl.js');
|
|
||||||
case 'pl-PL':
|
|
||||||
return import('../translations/pl-PL.js');
|
|
||||||
case 'pl':
|
|
||||||
return import('../translations/pl.js');
|
|
||||||
case 'ro-RO':
|
|
||||||
return import('../translations/ro-RO.js');
|
|
||||||
case 'ro':
|
|
||||||
return import('../translations/ro.js');
|
|
||||||
case 'ru-RU':
|
|
||||||
return import('../translations/ru-RU.js');
|
|
||||||
case 'ru':
|
|
||||||
return import('../translations/ru.js');
|
|
||||||
case 'sk-SK':
|
|
||||||
return import('../translations/sk-SK.js');
|
|
||||||
case 'sk':
|
|
||||||
return import('../translations/sk.js');
|
|
||||||
case 'uk-UA':
|
|
||||||
return import('../translations/uk-UA.js');
|
|
||||||
case 'uk':
|
|
||||||
return import('../translations/uk.js');
|
|
||||||
case 'zh-CN':
|
|
||||||
case 'zh':
|
|
||||||
return import('../translations/zh.js');
|
|
||||||
default:
|
|
||||||
return import(`../translations/${locale}.js`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...super.localizeNamespaces,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
* List of validators that should set the input to invalid
|
* @desc List of all Validator instances applied to FormControl
|
||||||
|
* @type {Validator[]}
|
||||||
|
* @example
|
||||||
|
* FormControl.validators = [new Required(), new MinLength(3, { type: 'warning' })];
|
||||||
*/
|
*/
|
||||||
errorValidators: {
|
validators: Array,
|
||||||
type: Array,
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
errorState: {
|
|
||||||
type: Boolean,
|
|
||||||
attribute: 'error-state',
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
errorShow: {
|
|
||||||
type: Boolean,
|
|
||||||
attribute: 'error-show',
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
warningValidators: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
warningState: {
|
|
||||||
type: Boolean,
|
|
||||||
attribute: 'warning-state',
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
warningShow: {
|
|
||||||
type: Boolean,
|
|
||||||
attribute: 'warning-show',
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
infoValidators: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
info: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
infoState: {
|
|
||||||
type: Boolean,
|
|
||||||
attribute: 'info-state',
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
infoShow: {
|
|
||||||
type: Boolean,
|
|
||||||
attribute: 'info-show',
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
successValidators: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
successState: {
|
|
||||||
type: Boolean,
|
|
||||||
attribute: 'success-state',
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
successShow: {
|
|
||||||
type: Boolean,
|
|
||||||
attribute: 'success-show',
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
invalid: {
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
defaultSuccessFeedback: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* The currently displayed message(s)
|
* @desc Readonly validity states for all Validators of type 'error'
|
||||||
|
* @type {ValidityStatesObject}
|
||||||
|
* @example
|
||||||
|
* FormControl.errorStates; // => { required: true, minLength: false }
|
||||||
|
* FormControl.errorStates.required; // => true
|
||||||
*/
|
*/
|
||||||
_validationMessage: {
|
errorStates: {
|
||||||
type: String,
|
type: Object,
|
||||||
|
hasChanged: this._hasObjectChanged,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Readonly state for the error type. When at least one Validator of
|
||||||
|
* type 'error' is active (for instance required in case of an empty field),
|
||||||
|
* this Boolean flag will be true.
|
||||||
|
* For styling purposes, this state is reflected to an attribute
|
||||||
|
* @type {boolean}
|
||||||
|
* @example
|
||||||
|
* FormControl.hasError; // => true
|
||||||
|
*/
|
||||||
|
hasError: {
|
||||||
|
type: Boolean,
|
||||||
|
attribute: 'has-error',
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc flag that indicates whether async validation is pending
|
||||||
|
*/
|
||||||
|
isPending: {
|
||||||
|
type: Boolean,
|
||||||
|
attribute: 'is-pending',
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc value that al validation revolves around: once changed (usually triggered by
|
||||||
|
* end user entering input), it will automatically trigger validation.
|
||||||
|
*/
|
||||||
|
modelValue: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc specialized fields (think of input-date and input-email) can have preconfigured
|
||||||
|
* validators.
|
||||||
|
*/
|
||||||
|
defaultValidators: Array,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
updated(changedProperties) {
|
/**
|
||||||
super.updated(changedProperties);
|
* @overridable
|
||||||
|
*/
|
||||||
if (
|
static get validationTypes() {
|
||||||
[
|
return ['error'];
|
||||||
'error',
|
|
||||||
'warning',
|
|
||||||
'info',
|
|
||||||
'success',
|
|
||||||
'touched',
|
|
||||||
'dirty',
|
|
||||||
'submitted',
|
|
||||||
'prefilled',
|
|
||||||
'label',
|
|
||||||
].some(key => changedProperties.has(key))
|
|
||||||
) {
|
|
||||||
this._createMessageAndRenderFeedback();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperties.has('errorShow')) {
|
get _allValidators() {
|
||||||
this._onErrorShowChangedAsync();
|
return [...this.validators, ...this.defaultValidators];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_requestUpdate(name, oldVal) {
|
constructor() {
|
||||||
super._requestUpdate(name, oldVal);
|
super();
|
||||||
|
|
||||||
|
this.isPending = false;
|
||||||
|
/** @type {Validator[]} */
|
||||||
|
this.validators = [];
|
||||||
|
/** @type {Validator[]} */
|
||||||
|
this.defaultValidators = [];
|
||||||
|
|
||||||
|
/** @type {Validator[]} */
|
||||||
|
this.__syncValidationResult = [];
|
||||||
|
|
||||||
|
/** @type {Validator[]} */
|
||||||
|
this.__asyncValidationResult = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validation needs to happen before other updates
|
* @desc contains results from sync Validators, async Validators and ResultValidators
|
||||||
* E.g. formatting should not happen before we know the updated errorState
|
* @type {Validator[]}
|
||||||
*/
|
*/
|
||||||
if (
|
this.__validationResult = [];
|
||||||
[
|
|
||||||
'errorValidators',
|
/**
|
||||||
'warningValidators',
|
* Stores all types that have been validated. Needed for clearing
|
||||||
'infoValidators',
|
* previously stored states on the instance
|
||||||
'successValidators',
|
*/
|
||||||
'modelValue',
|
this.__validatorTypeHistoryCache = new Set();
|
||||||
].some(key => name === key)
|
this.constructor.validationTypes.forEach(t => this.__validatorTypeHistoryCache.add(t));
|
||||||
) {
|
|
||||||
|
this.__onValidatorUpdated = this.__onValidatorUpdated.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated(c) {
|
||||||
|
super.firstUpdated(c);
|
||||||
|
this.__validateInitialized = true;
|
||||||
this.validate();
|
this.validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @deprecated adding css classes for backwards compatibility
|
updateSync(name, oldValue) {
|
||||||
this.constructor.validationTypes.forEach(type => {
|
super.updateSync(name, oldValue);
|
||||||
if (name === `${type}State`) {
|
if (name === 'validators') {
|
||||||
this.classList[this[`${type}State`] ? 'add' : 'remove'](`state-${type}`);
|
// trigger validation (ideally only for the new or changed validator)
|
||||||
|
this.__setupValidators();
|
||||||
|
this.validate();
|
||||||
|
} else if (name === 'modelValue') {
|
||||||
|
this.validate({ clearCurrentResult: true });
|
||||||
}
|
}
|
||||||
if (name === `${type}Show`) {
|
}
|
||||||
this.classList[this[`${type}Show`] ? 'add' : 'remove'](`state-${type}-show`);
|
|
||||||
|
updated(c) {
|
||||||
|
super.updated(c);
|
||||||
|
this.constructor.validationTypes.forEach(type => {
|
||||||
|
if (c.has(`${type}States`)) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new Event(`${type}-states-changed`, { bubbles: true, composed: true }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.has(`has${pascalCase(type)}`)) {
|
||||||
|
this.dispatchEvent(new Event(`has-${type}-changed`, { bubbles: true, composed: true }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (name === 'invalid') {
|
|
||||||
this.classList[this.invalid ? 'add' : 'remove'](`state-invalid`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'error' && this.error !== oldVal) {
|
|
||||||
this._onErrorChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'warning' && this.warning !== oldVal) {
|
|
||||||
this._onWarningChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'info' && this.info !== oldVal) {
|
|
||||||
this._onInfoChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'success' && this.success !== oldVal) {
|
|
||||||
this._onSuccessChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'errorState' && this.errorState !== oldVal) {
|
|
||||||
this._onErrorStateChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'warningState' && this.warningState !== oldVal) {
|
|
||||||
this._onWarningStateChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'infoState' && this.infoState !== oldVal) {
|
|
||||||
this._onInfoStateChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'successState' && this.successState !== oldVal) {
|
|
||||||
this._onSuccessStateChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get validationTypes() {
|
|
||||||
return ['error', 'warning', 'info', 'success'];
|
|
||||||
}
|
|
||||||
|
|
||||||
get _feedbackElement() {
|
|
||||||
return Array.from(this.children).find(child => child.slot === 'feedback');
|
|
||||||
}
|
|
||||||
|
|
||||||
getFieldName(validatorParams) {
|
|
||||||
const labelEl = Array.from(this.children).find(child => child.slot === 'label');
|
|
||||||
const label = this.label || (labelEl && labelEl.textContent);
|
|
||||||
|
|
||||||
if (validatorParams && validatorParams.fieldName) {
|
|
||||||
return validatorParams.fieldName;
|
|
||||||
}
|
|
||||||
if (label) {
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onErrorStateChanged() {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('error-state-changed', { bubbles: true, composed: true }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onWarningStateChanged() {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('warning-state-changed', { bubbles: true, composed: true }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onInfoStateChanged() {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('info-state-changed', { bubbles: true, composed: true }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSuccessStateChanged() {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('success-state-changed', { bubbles: true, composed: true }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* * * * * * * * * * * *
|
|
||||||
Observer Handlers */
|
|
||||||
|
|
||||||
onLocaleUpdated() {
|
|
||||||
if (super.onLocaleUpdated) {
|
|
||||||
super.onLocaleUpdated();
|
|
||||||
}
|
|
||||||
this._createMessageAndRenderFeedback();
|
|
||||||
}
|
|
||||||
|
|
||||||
_createMessageAndRenderFeedback() {
|
|
||||||
this._createMessage();
|
|
||||||
const details = {};
|
|
||||||
|
|
||||||
this.constructor.validationTypes.forEach(type => {
|
|
||||||
details[type] = this[type];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this._feedbackElement) {
|
|
||||||
// Only write to light DOM not put there by Application Developer, but by <lion-component>
|
|
||||||
if (typeof this._feedbackElement.renderFeedback === 'function') {
|
|
||||||
this._feedbackElement.renderFeedback(this.getValidationStates(), this.message, details);
|
|
||||||
} else {
|
|
||||||
this.renderFeedback(this.getValidationStates(), this.message, details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onErrorChanged() {
|
|
||||||
if (!this.constructor._objectEquals(this.error, this.__oldValues.error)) {
|
|
||||||
this.dispatchEvent(new CustomEvent('error-changed', { bubbles: true, composed: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onWarningChanged() {
|
|
||||||
if (!this.constructor._objectEquals(this.warning, this.__oldValues.warning)) {
|
|
||||||
this.dispatchEvent(new CustomEvent('warning-changed', { bubbles: true, composed: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onInfoChanged() {
|
|
||||||
if (!this.constructor._objectEquals(this.info, this.__oldValues.info)) {
|
|
||||||
this.dispatchEvent(new CustomEvent('info-changed', { bubbles: true, composed: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSuccessChanged() {
|
|
||||||
if (!this.constructor._objectEquals(this.success, this.__oldValues.success)) {
|
|
||||||
this.dispatchEvent(new CustomEvent('success-changed', { bubbles: true, composed: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_createMessage() {
|
|
||||||
const newStates = this.getValidationStates();
|
|
||||||
this.message = { list: [], message: '' };
|
|
||||||
this.constructor.validationTypes.forEach(type => {
|
|
||||||
if (this[`show${pascalCase(type)}Condition`](newStates, this.__oldValidationStates)) {
|
|
||||||
this[`${type}Show`] = true;
|
|
||||||
this.message.list.push(...this[type].list);
|
|
||||||
} else {
|
|
||||||
this[`${type}Show`] = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (this.message.list.length > 0) {
|
|
||||||
this.messageState = true;
|
|
||||||
const { translationKeys, data } = this.message.list[0];
|
|
||||||
data.fieldName = this.getFieldName(data.validatorParams);
|
|
||||||
this._validationMessage = this.translateMessage(translationKeys, data);
|
|
||||||
this.message.message = this._validationMessage;
|
|
||||||
} else {
|
|
||||||
this.messageState = false;
|
|
||||||
this._validationMessage = '';
|
|
||||||
this.message.message = this._validationMessage;
|
|
||||||
}
|
|
||||||
return this.message.message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can be overridden by sub classers
|
* @desc The main function of this mixin. Triggered by:
|
||||||
* Note that it's important to always render your feedback to the _feedbackElement textContent!
|
* - a modelValue change
|
||||||
* This is necessary because it is allocated as the feedback slot, which is what the mixin renders feedback to.
|
* - a change in the 'validators' array
|
||||||
|
* - a change in the config of an individual Validator
|
||||||
|
*
|
||||||
|
* Three situations are handled:
|
||||||
|
* - A.1 The FormControl is empty: further execution is halted. When the Required Validator
|
||||||
|
* (being mutually exclusive to the other Validators) is applied, it will end up in the
|
||||||
|
* validation result (as the only Validator, since further execution was halted).
|
||||||
|
* - A.2 There are synchronous Validators: this is the most common flow. When modelValue hasn't
|
||||||
|
* changed since last async results were generated, 'sync results' are merged with the
|
||||||
|
* 'async results'.
|
||||||
|
* - A.3 There are asynchronous Validators: for instance when server side evaluation is needed.
|
||||||
|
* Executions are scheduled and awaited and the 'async results' are merged with the
|
||||||
|
* 'sync results'.
|
||||||
|
*
|
||||||
|
* - B. There are ResultValidators. After steps A.1, A.2, or A.3 are finished, the holistic
|
||||||
|
* ResultValidators (evaluating the total result of the 'regular' (A.1, A.2 and A.3) validators)
|
||||||
|
* will be run...
|
||||||
|
*
|
||||||
|
* Situations A.2 and A.3 are not mutually exclusive and can be triggered within one validate()
|
||||||
|
* call. Situation B will occur after every call.
|
||||||
*/
|
*/
|
||||||
renderFeedback() {
|
async validate({ clearCurrentResult } = {}) {
|
||||||
if (this._feedbackElement) {
|
if (!this.__validateInitialized) {
|
||||||
this._feedbackElement.textContent = this._validationMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onErrorShowChangedAsync() {
|
|
||||||
// Screen reader output should be in sync with visibility of error messages
|
|
||||||
if (this._inputNode) {
|
|
||||||
this._inputNode.setAttribute('aria-invalid', this.errorShow);
|
|
||||||
// TODO: test and see if needed for a11y
|
|
||||||
// this._inputNode.setCustomValidity(this._validationMessage || '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* * * * * * * * * *
|
|
||||||
Public Methods */
|
|
||||||
|
|
||||||
getValidationStates() {
|
|
||||||
const result = {};
|
|
||||||
this.constructor.validationTypes.forEach(type => {
|
|
||||||
result[type] = this[`${type}State`];
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Order is: Error, Warning, Info
|
|
||||||
* Transition from Error to "nothing" results in success
|
|
||||||
* Other transitions (from Warning/Info) are not followed by a success message
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
if (this.modelValue === undefined) {
|
|
||||||
this.__resetValidationStates();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.__oldValidationStates = this.getValidationStates();
|
|
||||||
this.constructor.validationTypes.forEach(type => {
|
this.__storePrevResult();
|
||||||
this.validateType(type);
|
if (clearCurrentResult) {
|
||||||
});
|
// Clear ('invalidate') all pending and existing validation results.
|
||||||
this.dispatchEvent(new CustomEvent('validation-done', { bubbles: true, composed: true }));
|
// This is needed because we have async (pending) validators whose results
|
||||||
|
// need to be merged with those of sync validators and vice versa.
|
||||||
|
this.__clearValidationResults();
|
||||||
|
}
|
||||||
|
await this.__executeValidators();
|
||||||
}
|
}
|
||||||
|
|
||||||
__resetValidationStates() {
|
__storePrevResult() {
|
||||||
this.constructor.validationTypes.forEach(type => {
|
this.__prevValidationResult = this.__validationResult;
|
||||||
this[`${type}State`] = false;
|
|
||||||
this[type] = {};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override if needed
|
* @desc step A1-3 + B (as explained in 'validate')
|
||||||
*/
|
*/
|
||||||
translateMessage(keys, data) {
|
async __executeValidators() {
|
||||||
return localize.msg(keys, data);
|
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} */
|
||||||
|
const requiredValidator = this._allValidators.find(v => v instanceof Required);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Handle the 'exceptional' 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
|
||||||
|
* to trigger formControl.__isEmpty.
|
||||||
|
* - when __isEmpty returns false, the input was empty. This means we need to stop
|
||||||
|
* validation here, because all other Validators' execute functions assume the
|
||||||
|
* value is not empty (there would be nothing to validate).
|
||||||
|
*/
|
||||||
|
const isEmpty = this.__isEmpty(value);
|
||||||
|
if (isEmpty) {
|
||||||
|
if (requiredValidator) {
|
||||||
|
this.__syncValidationResult = [requiredValidator];
|
||||||
|
}
|
||||||
|
this.__finishValidation({ source: 'sync' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showErrorCondition(newStates) {
|
// Separate Validators in sync and async
|
||||||
return newStates.error;
|
const /** @type {Validator[]} */ filteredValidators = this._allValidators.filter(
|
||||||
}
|
v => !(v instanceof ResultValidator) && !(v instanceof Required),
|
||||||
|
|
||||||
showWarningCondition(newStates) {
|
|
||||||
return newStates.warning && !newStates.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
showInfoCondition(newStates) {
|
|
||||||
return newStates.info && !newStates.error && !newStates.warning;
|
|
||||||
}
|
|
||||||
|
|
||||||
showSuccessCondition(newStates, oldStates) {
|
|
||||||
return (
|
|
||||||
newStates.success &&
|
|
||||||
!newStates.error &&
|
|
||||||
!newStates.warning &&
|
|
||||||
!newStates.info &&
|
|
||||||
oldStates.error
|
|
||||||
);
|
);
|
||||||
}
|
const /** @type {Validator[]} */ syncValidators = filteredValidators.filter(v => !v.async);
|
||||||
|
const /** @type {Validator[]} */ asyncValidators = filteredValidators.filter(v => v.async);
|
||||||
|
|
||||||
getErrorTranslationsKeys(data) {
|
/**
|
||||||
return this.constructor.__getLocalizeKeys(
|
* 2. Synchronous validators
|
||||||
`error.${data.validatorName}`,
|
*/
|
||||||
data.validatorName,
|
this.__executeSyncValidators(syncValidators, value, {
|
||||||
);
|
hasAsync: Boolean(asyncValidators.length),
|
||||||
}
|
});
|
||||||
|
|
||||||
getWarningTranslationsKeys(data) {
|
/**
|
||||||
return this.constructor.__getLocalizeKeys(
|
* 3. Asynchronous validators
|
||||||
`warning.${data.validatorName}`,
|
*/
|
||||||
data.validatorName,
|
await this.__executeAsyncValidators(asyncValidators, value);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getInfoTranslationsKeys(data) {
|
|
||||||
return this.constructor.__getLocalizeKeys(`info.${data.validatorName}`, data.validatorName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Special case for ok validators starting with 'random'. Example for randomOk:
|
* @desc step A2, calls __finishValidation
|
||||||
* - will fetch translation for randomOk (should contain multiple translations keys)
|
* @param {Validator[]} syncValidators
|
||||||
* - split by ',' and then use one of those keys
|
|
||||||
* - will remember last random choice so it does not change on key stroke
|
|
||||||
* - remembering can be reset with this.__lastGetSuccessResult = false;
|
|
||||||
*/
|
*/
|
||||||
getSuccessTranslationsKeys(data) {
|
__executeSyncValidators(syncValidators, value, { hasAsync }) {
|
||||||
let key = `success.${data.validatorName}`;
|
if (syncValidators.length) {
|
||||||
if (this.__lastGetSuccessResult && data.validatorName.indexOf('random') === 0) {
|
this.__syncValidationResult = syncValidators.filter(v => v.execute(value, v.param));
|
||||||
return this.__lastGetSuccessResult;
|
|
||||||
}
|
}
|
||||||
if (data.validatorName.indexOf('random') === 0) {
|
this.__finishValidation({ source: 'sync', hasAsync });
|
||||||
const getKeys = this.constructor.__getLocalizeKeys(key, data.validatorName);
|
|
||||||
const keysToConsider = this.translateMessage(getKeys); // eslint-disable-line max-len
|
|
||||||
if (keysToConsider) {
|
|
||||||
const randomKeys = keysToConsider.split(',');
|
|
||||||
key = randomKeys[Math.floor(Math.random() * randomKeys.length)].trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const result = this.constructor.__getLocalizeKeys(key, data.validatorName);
|
|
||||||
this.__lastGetSuccessResult = result;
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the translation paths in right priority order
|
* @desc step A3, calls __finishValidation
|
||||||
*
|
* @param {Validator[]} filteredValidators all Validators except required and ResultValidators
|
||||||
* @param {string} key usually `${type}.${validatorName}`
|
|
||||||
* @param {string} validatorName for which to create the keys
|
|
||||||
*/
|
*/
|
||||||
static __getLocalizeKeys(key, validatorName) {
|
async __executeAsyncValidators(asyncValidators, value) {
|
||||||
const result = [];
|
if (asyncValidators.length) {
|
||||||
this.localizeNamespaces.forEach(ns => {
|
this.isPending = true;
|
||||||
const namespace = typeof ns === 'object' ? Object.keys(ns)[0] : ns;
|
const resultPromises = asyncValidators.map(v => v.execute(value, v.param));
|
||||||
result.push(`${namespace}+${validatorName}:${key}`);
|
const booleanResults = await Promise.all(resultPromises);
|
||||||
result.push(`${namespace}:${key}`);
|
this.__asyncValidationResult = booleanResults
|
||||||
});
|
.map((r, i) => asyncValidators[i]) // Create an array of Validators
|
||||||
return result;
|
.filter((v, i) => booleanResults[i]); // Only leave the ones returning true
|
||||||
|
this.__finishValidation({ source: 'async' });
|
||||||
|
this.isPending = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* type can be 'error', 'warning', 'info', 'success'
|
* @desc step B, called by __finishValidation
|
||||||
*
|
* @param {Validator[]} regularValidationResult result of steps 1-3
|
||||||
* a Validator can be
|
|
||||||
* - special string
|
|
||||||
* 'required'
|
|
||||||
* - function e.g
|
|
||||||
* MyValidate.isEmail, isCat, ...
|
|
||||||
* - array for parameters e.g.
|
|
||||||
* [minMaxLength, {min: 10, max: 15}],
|
|
||||||
* [minLength, {min: 5}],
|
|
||||||
* [contains, 'thisString']
|
|
||||||
*/
|
*/
|
||||||
validateType(type) {
|
__executeResultValidators(regularValidationResult) {
|
||||||
const validators = this.getValidatorsForType(type);
|
/** @type {ResultValidator[]} */
|
||||||
if (!(validators && Array.isArray(validators) && validators.length > 0)) return;
|
const resultValidators = this._allValidators.filter(
|
||||||
|
v => !v.async && v instanceof ResultValidator,
|
||||||
|
);
|
||||||
|
|
||||||
const resultList = [];
|
return resultValidators.filter(v =>
|
||||||
let value = this.modelValue; // This will end up being modelValue or Unparseable.viewValue
|
v.executeOnResults({
|
||||||
|
regularValidationResult,
|
||||||
for (let i = 0; i < validators.length; i += 1) {
|
prevValidationResult: this.__prevValidationResult,
|
||||||
const validatorArray = Array.isArray(validators[i]) ? validators[i] : [validators[i]];
|
}),
|
||||||
let validatorFn = validatorArray[0];
|
|
||||||
const validatorParams = validatorArray[1];
|
|
||||||
const validatorConfig = validatorArray[2];
|
|
||||||
|
|
||||||
let isRequiredValidator = false; // Whether the current is the required validator
|
|
||||||
if (typeof validatorFn === 'string' && validatorFn === 'required' && this.__isRequired) {
|
|
||||||
validatorFn = this.__isRequired;
|
|
||||||
isRequiredValidator = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the modelValue can't be created, still allow all validators to give valuable
|
|
||||||
// feedbback to the user based on the current viewValue.
|
|
||||||
if (value instanceof Unparseable) {
|
|
||||||
value = value.viewValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't validate empty values, unless its 'required'
|
|
||||||
const shouldValidate = isRequiredValidator || !this.constructor.__isEmpty(value);
|
|
||||||
|
|
||||||
if (typeof validatorFn === 'function') {
|
|
||||||
if (shouldValidate) {
|
|
||||||
const result = validatorFn(value, validatorParams);
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
for (const validatorName in result) {
|
|
||||||
if (!result[validatorName]) {
|
|
||||||
const data = {
|
|
||||||
validatorName,
|
|
||||||
validatorParams,
|
|
||||||
validatorConfig,
|
|
||||||
validatorType: type,
|
|
||||||
name: this.name,
|
|
||||||
value: this.modelValue,
|
|
||||||
};
|
|
||||||
resultList.push({
|
|
||||||
data,
|
|
||||||
translationKeys: this[`get${pascalCase(type)}TranslationsKeys`](data),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('That does not look like a validator function', validatorFn); // eslint-disable-line
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.warn(
|
|
||||||
// eslint-disable-next-line
|
|
||||||
'You should provide options like so errorValidators=${[[functionName, {min: 5, max: 10}]]}',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} options
|
||||||
|
* @param {'sync'|'async'} options.source
|
||||||
|
* @param {boolean} [options.hasAsync] whether async validators are configured in this run.
|
||||||
|
* If not, we have nothing left to wait for.
|
||||||
|
*/
|
||||||
|
__finishValidation({ source, hasAsync }) {
|
||||||
|
const /** @type {Validator[]} */ syncAndAsyncOutcome = [
|
||||||
|
...this.__syncValidationResult,
|
||||||
|
...this.__asyncValidationResult,
|
||||||
|
];
|
||||||
|
// if we have any ResultValidators left, now is the time to run them...
|
||||||
|
const resultOutCome = this.__executeResultValidators(syncAndAsyncOutcome);
|
||||||
|
|
||||||
|
/** @typedef {Validator[]} TotalValidationResult */
|
||||||
|
this.__validationResult = [...resultOutCome, ...syncAndAsyncOutcome];
|
||||||
|
this._storeResultsOnInstance(this.__validationResult);
|
||||||
|
|
||||||
|
/** private event that should be listened to by FeedbackMixin / LionFieldSet */
|
||||||
|
this.dispatchEvent(new Event('validate-performed', { bubbles: true, composed: true }));
|
||||||
|
if (source === 'async' || !hasAsync) {
|
||||||
|
this.__validateCompleteResolve();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = {};
|
/**
|
||||||
if (resultList.length > 0) {
|
* @desc For all results, for all types, stores results on instance.
|
||||||
result = {
|
* For errors, this means:
|
||||||
list: resultList, // TODO: maybe call this details?
|
* - this.hasError = true/false;
|
||||||
};
|
* - this.errorStates = {
|
||||||
// <lion-form> will have a reference to lion-field by name, so user can do:
|
* [validatorName1]: true,
|
||||||
// formName.fieldName.errors.validatorName
|
* [validatorName2]: true,
|
||||||
resultList.forEach(resultListElement => {
|
* }
|
||||||
result[resultListElement.data.validatorName] = true;
|
* Note that 'this.hasErrorVisible' won't be set here: it will be based on the outcome of
|
||||||
|
* method `._proritizeAndFilterFeedback`.
|
||||||
|
* @param {Validator[]} valResult
|
||||||
|
*/
|
||||||
|
_storeResultsOnInstance(valResult) {
|
||||||
|
const instanceResult = {};
|
||||||
|
this.__resetInstanceValidationStates(instanceResult);
|
||||||
|
|
||||||
|
valResult.forEach(validator => {
|
||||||
|
// By default, this will be reflected to attr 'error-state' in case of
|
||||||
|
// 'error' type. Subclassers supporting different types need to
|
||||||
|
// configure attribute reflection themselves.
|
||||||
|
instanceResult[`has${pascalCase(validator.type)}`] = true;
|
||||||
|
instanceResult[`${validator.type}States`] =
|
||||||
|
instanceResult[`${validator.type}States`] || {};
|
||||||
|
instanceResult[`${validator.type}States`][validator.name] = true;
|
||||||
|
this.__validatorTypeHistoryCache.add(validator.type);
|
||||||
|
});
|
||||||
|
Object.assign(this, instanceResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
__resetInstanceValidationStates(instanceResult) {
|
||||||
|
this.__validatorTypeHistoryCache.forEach(previouslyStoredType => {
|
||||||
|
instanceResult[`has${pascalCase(previouslyStoredType)}`] = false;
|
||||||
|
instanceResult[`${previouslyStoredType}States`] = {};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this[`${type}State`] = resultList.length > 0;
|
__clearValidationResults() {
|
||||||
this.__oldValues[type] = this[type];
|
this.__syncValidationResult = [];
|
||||||
this[type] = result;
|
this.__asyncValidationResult = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidatorsForType(type) {
|
__onValidatorUpdated(e) {
|
||||||
if (this.defaultSuccessFeedback && type === 'success') {
|
if (e.type === 'param-changed' || e.type === 'config-changed') {
|
||||||
return [[randomOk]].concat(this.successValidators || []);
|
this.validate();
|
||||||
}
|
}
|
||||||
return this[`${type}Validators`] || [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static _objectEquals(result, prevResult) {
|
__setupValidators() {
|
||||||
if (!prevResult) return false;
|
const events = ['param-changed', 'config-changed'];
|
||||||
return Object.keys(result).join('') === Object.keys(prevResult).join('');
|
if (this.__prevValidators) {
|
||||||
|
this.__prevValidators.forEach(v => {
|
||||||
|
events.forEach(e => v.removeEventListener(e, this.__onValidatorUpdated));
|
||||||
|
v.onFormControlDisconnect(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._allValidators.forEach(v => {
|
||||||
|
events.forEach(e => v.addEventListener(e, this.__onValidatorUpdated));
|
||||||
|
v.onFormControlConnect(this);
|
||||||
|
});
|
||||||
|
this.__prevValidators = this._allValidators;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When empty (model)value,
|
static _hasObjectChanged(result, prevResult) {
|
||||||
static __isEmpty(v) {
|
return JSON.stringify(result) !== JSON.stringify(prevResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
__isEmpty(v) {
|
||||||
|
if (typeof this._isEmpty === 'function') {
|
||||||
|
return this._isEmpty(v);
|
||||||
|
}
|
||||||
|
// // TODO: move to compat layer. Be sure to keep this, because people use this a lot
|
||||||
|
// if (typeof this.__isRequired === 'function') {
|
||||||
|
// return !this.__isRequired(v);
|
||||||
|
// }
|
||||||
return v === null || typeof v === 'undefined' || v === '';
|
return v === null || typeof v === 'undefined' || v === '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
92
packages/validate/src/Validator.js
Normal file
92
packages/validate/src/Validator.js
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { fakeExtendsEventTarget } from './utils/fake-extends-event-target.js';
|
||||||
|
|
||||||
|
export class Validator {
|
||||||
|
constructor(param, config) {
|
||||||
|
fakeExtendsEventTarget(this);
|
||||||
|
|
||||||
|
this.name = '';
|
||||||
|
this.async = false;
|
||||||
|
this.__param = param;
|
||||||
|
this.__config = config || {};
|
||||||
|
this.type = (config && config.type) || 'error'; // Default type supported by ValidateMixin
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc The function that returns a Boolean
|
||||||
|
* @param {string|Date|Number|object} modelValue
|
||||||
|
* @param {object} param
|
||||||
|
* @returns {Boolean|Promise<Boolean>}
|
||||||
|
*/
|
||||||
|
execute(modelValue, param) {} // eslint-disable-line
|
||||||
|
|
||||||
|
set param(p) {
|
||||||
|
this.__param = p;
|
||||||
|
this.dispatchEvent(new Event('param-changed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
get param() {
|
||||||
|
return this.__param;
|
||||||
|
}
|
||||||
|
|
||||||
|
set config(c) {
|
||||||
|
this.__config = c;
|
||||||
|
this.dispatchEvent(new Event('config-changed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
get config() {
|
||||||
|
return this.__config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* @param {object} data
|
||||||
|
* @param {*} data.modelValue
|
||||||
|
* @param {string} data.fieldName
|
||||||
|
* @param {*} data.validatorParams
|
||||||
|
* @returns {string|Node|Promise<stringOrNode>|() => stringOrNode)}
|
||||||
|
*/
|
||||||
|
async _getMessage(data) {
|
||||||
|
if (typeof this.config.getMessage === 'function') {
|
||||||
|
return this.config.getMessage(data);
|
||||||
|
}
|
||||||
|
return this.constructor.getMessage(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* @param {object} data
|
||||||
|
* @param {*} data.modelValue
|
||||||
|
* @param {string} data.fieldName
|
||||||
|
* @param {*} data.validatorParams
|
||||||
|
* @returns {string|Node|Promise<stringOrNode>|() => stringOrNode)}
|
||||||
|
*/
|
||||||
|
static async getMessage(data) {} // eslint-disable-line no-unused-vars, no-empty-function
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FormControl} formControl
|
||||||
|
*/
|
||||||
|
onFormControlConnect(formControl) {} // eslint-disable-line
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FormControl} formControl
|
||||||
|
*/
|
||||||
|
onFormControlDisconnect(formControl) {} // eslint-disable-line
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Used on async Validators, makes it able to do perf optimizations when there are
|
||||||
|
* pending "execute" calls with outdated values.
|
||||||
|
* ValidateMixin calls Validator.abortExecution() an async Validator can act accordingly,
|
||||||
|
* depending on its implementation of the "execute" function.
|
||||||
|
* - For instance, when fetch was called:
|
||||||
|
* https://stackoverflow.com/questions/31061838/how-do-i-cancel-an-http-fetch-request
|
||||||
|
* - Or, when a webworker was started, its process could be aborted and then restarted.
|
||||||
|
*/
|
||||||
|
abortExecution() {} // eslint-disable-line
|
||||||
|
}
|
||||||
|
|
||||||
|
// For simplicity, a default validator only handles one state:
|
||||||
|
// it can either be true or false an it will only have one message.
|
||||||
|
// In more advanced cases (think of the feedback mechanism for the maximum number of
|
||||||
|
// characters in Twitter), more states are needed. The alternative of
|
||||||
|
// having multiple distinct validators would be cumbersome to create and maintain,
|
||||||
|
// also because the validations would tie too much into each others logic.
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
/**
|
|
||||||
* TODO: refactor validators to classes, putting needed meta info on instance.
|
|
||||||
* Note that direct function comparison (Validator[0] === minDate) doesn't work when code
|
|
||||||
* is transpiled
|
|
||||||
* @param {String} name - a name like minDate, maxDate, minMaxDate
|
|
||||||
* @param {Function} fn - the validator function to execute provided in [fn, param, config]
|
|
||||||
* @param {Function} requiredSignature - arguments needed to execute fn without failing
|
|
||||||
* @returns {Boolean} - whether the validator (name) is applied
|
|
||||||
*/
|
|
||||||
export function isValidatorApplied(name, fn, requiredSignature) {
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = Object.keys(fn(new Date(), requiredSignature))[0] === name;
|
|
||||||
} catch (e) {
|
|
||||||
result = false;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
112
packages/validate/src/loadDefaultFeedbackMessages.js
Normal file
112
packages/validate/src/loadDefaultFeedbackMessages.js
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { localize } from '@lion/localize';
|
||||||
|
import { Required } from './validators/Required.js';
|
||||||
|
import { EqualsLength, MaxLength } from './validators/StringValidators.js';
|
||||||
|
import { DefaultSuccess } from './resultValidators/DefaultSuccess.js';
|
||||||
|
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
|
export function loadDefaultFeedbackMessages() {
|
||||||
|
if (loaded === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const validateNamespace = localize.loadNamespace({
|
||||||
|
'lion-validate': locale => {
|
||||||
|
switch (locale) {
|
||||||
|
case 'bg-BG':
|
||||||
|
return import('../translations/bg-BG.js');
|
||||||
|
case 'bg':
|
||||||
|
return import('../translations/bg.js');
|
||||||
|
case 'cs-CZ':
|
||||||
|
return import('../translations/cs-CZ.js');
|
||||||
|
case 'cs':
|
||||||
|
return import('../translations/cs.js');
|
||||||
|
case 'de-DE':
|
||||||
|
return import('../translations/de-DE.js');
|
||||||
|
case 'de':
|
||||||
|
return import('../translations/de.js');
|
||||||
|
case 'en-AU':
|
||||||
|
return import('../translations/en-AU.js');
|
||||||
|
case 'en-GB':
|
||||||
|
return import('../translations/en-GB.js');
|
||||||
|
case 'en-US':
|
||||||
|
return import('../translations/en-US.js');
|
||||||
|
case 'en-PH':
|
||||||
|
case 'en':
|
||||||
|
return import('../translations/en.js');
|
||||||
|
case 'es-ES':
|
||||||
|
return import('../translations/es-ES.js');
|
||||||
|
case 'es':
|
||||||
|
return import('../translations/es.js');
|
||||||
|
case 'fr-FR':
|
||||||
|
return import('../translations/fr-FR.js');
|
||||||
|
case 'fr-BE':
|
||||||
|
return import('../translations/fr-BE.js');
|
||||||
|
case 'fr':
|
||||||
|
return import('../translations/fr.js');
|
||||||
|
case 'hu-HU':
|
||||||
|
return import('../translations/hu-HU.js');
|
||||||
|
case 'hu':
|
||||||
|
return import('../translations/hu.js');
|
||||||
|
case 'it-IT':
|
||||||
|
return import('../translations/it-IT.js');
|
||||||
|
case 'it':
|
||||||
|
return import('../translations/it.js');
|
||||||
|
case 'nl-BE':
|
||||||
|
return import('../translations/nl-BE.js');
|
||||||
|
case 'nl-NL':
|
||||||
|
return import('../translations/nl-NL.js');
|
||||||
|
case 'nl':
|
||||||
|
return import('../translations/nl.js');
|
||||||
|
case 'pl-PL':
|
||||||
|
return import('../translations/pl-PL.js');
|
||||||
|
case 'pl':
|
||||||
|
return import('../translations/pl.js');
|
||||||
|
case 'ro-RO':
|
||||||
|
return import('../translations/ro-RO.js');
|
||||||
|
case 'ro':
|
||||||
|
return import('../translations/ro.js');
|
||||||
|
case 'ru-RU':
|
||||||
|
return import('../translations/ru-RU.js');
|
||||||
|
case 'ru':
|
||||||
|
return import('../translations/ru.js');
|
||||||
|
case 'sk-SK':
|
||||||
|
return import('../translations/sk-SK.js');
|
||||||
|
case 'sk':
|
||||||
|
return import('../translations/sk.js');
|
||||||
|
case 'uk-UA':
|
||||||
|
return import('../translations/uk-UA.js');
|
||||||
|
case 'uk':
|
||||||
|
return import('../translations/uk.js');
|
||||||
|
case 'zh-CN':
|
||||||
|
case 'zh':
|
||||||
|
return import('../translations/zh.js');
|
||||||
|
default:
|
||||||
|
return import(`../translations/${locale}.js`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Required.getMessage = async data => {
|
||||||
|
await validateNamespace;
|
||||||
|
return localize.msg('lion-validate:error.required', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
EqualsLength.getMessage = async data => {
|
||||||
|
await validateNamespace;
|
||||||
|
return localize.msg('lion-validate:error.equalsLength', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
MaxLength.getMessage = async data => {
|
||||||
|
await validateNamespace;
|
||||||
|
return localize.msg('lion-validate:error.maxLength', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
DefaultSuccess.getMessage = async data => {
|
||||||
|
await validateNamespace;
|
||||||
|
const randomKeys = localize.msg('lion-validate:success.randomOk').split(',');
|
||||||
|
const key = randomKeys[Math.floor(Math.random() * randomKeys.length)].trim();
|
||||||
|
return localize.msg(`lion-validate:${key}`, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
16
packages/validate/src/resultValidators/DefaultSuccess.js
Normal file
16
packages/validate/src/resultValidators/DefaultSuccess.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { ResultValidator } from '../ResultValidator.js';
|
||||||
|
|
||||||
|
export class DefaultSuccess extends ResultValidator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.type = 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
executeOnResults({ regularValidationResult, prevValidationResult }) {
|
||||||
|
const errorOrWarning = v => v.type === 'error' || v.type === 'warning';
|
||||||
|
const hasErrorOrWarning = !!regularValidationResult.filter(errorOrWarning).length;
|
||||||
|
const prevHadErrorOrWarning = !!prevValidationResult.filter(errorOrWarning).length;
|
||||||
|
return !hasErrorOrWarning && prevHadErrorOrWarning;
|
||||||
|
}
|
||||||
|
}
|
||||||
93
packages/validate/src/utils/SyncUpdatableMixin.js
Normal file
93
packages/validate/src/utils/SyncUpdatableMixin.js
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { dedupeMixin } from '@lion/core';
|
||||||
|
|
||||||
|
// TODO: will be moved to @Lion/core later
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Why this mixin?
|
||||||
|
* - it adheres to the "Member Order Independence" web components standard:
|
||||||
|
* https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence
|
||||||
|
* - sync observers can be dependent on the outcome of the render function (or, more generically
|
||||||
|
* speaking, the light and shadow dom). This aligns with the 'updated' callback that is supported
|
||||||
|
* out of the box by LitElement, which runs after connectedCallback as well.
|
||||||
|
* - makes the propertyAccessor.`hasChanged` compatible in synchronous updates:
|
||||||
|
* `updateSync` will only be called when new value differs from old value.
|
||||||
|
* See: https://lit-element.polymer-project.org/guide/lifecycle#haschanged
|
||||||
|
* - it is a stable abstaction on top of a protected/non offical lifecycle LitElement api.
|
||||||
|
* Whenever the implementation of `_requestUpdate` changes (this happened in the past for
|
||||||
|
* `requestUpdate`) we only have to change our abstraction instead of all our components
|
||||||
|
*/
|
||||||
|
export const SyncUpdatableMixin = dedupeMixin(
|
||||||
|
superclass =>
|
||||||
|
class SyncUpdatable extends superclass {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
// Namespace for this mixin that guarantees naming clashes will not occur...
|
||||||
|
this.__SyncUpdatableNamespace = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated(c) {
|
||||||
|
super.firstUpdated(c);
|
||||||
|
this.__SyncUpdatableNamespace.connected = true;
|
||||||
|
this.__syncUpdatableInitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.__SyncUpdatableNamespace.connected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the propertyAccessor.`hasChanged` compatible in synchronous updates
|
||||||
|
* @param {string} name
|
||||||
|
* @param {*} oldValue
|
||||||
|
*/
|
||||||
|
static __syncUpdatableHasChanged(name, newValue, oldValue) {
|
||||||
|
const properties = this._classProperties;
|
||||||
|
if (properties.get(name) && properties.get(name).hasChanged) {
|
||||||
|
return properties.get(name).hasChanged(newValue, oldValue);
|
||||||
|
}
|
||||||
|
return newValue !== oldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
__syncUpdatableInitialize() {
|
||||||
|
const ns = this.__SyncUpdatableNamespace;
|
||||||
|
const ctor = this.constructor;
|
||||||
|
|
||||||
|
ns.initialized = true;
|
||||||
|
// Empty queue...
|
||||||
|
if (ns.queue) {
|
||||||
|
Array.from(ns.queue).forEach(name => {
|
||||||
|
if (ctor.__syncUpdatableHasChanged(name, this[name], undefined)) {
|
||||||
|
this.updateSync(name, undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestUpdate(name, oldValue) {
|
||||||
|
super._requestUpdate(name, oldValue);
|
||||||
|
|
||||||
|
this.__SyncUpdatableNamespace = this.__SyncUpdatableNamespace || {};
|
||||||
|
const ns = this.__SyncUpdatableNamespace;
|
||||||
|
const ctor = this.constructor;
|
||||||
|
|
||||||
|
// Before connectedCallback: queue
|
||||||
|
if (!ns.connected) {
|
||||||
|
ns.queue = ns.queue || new Set();
|
||||||
|
// Makes sure that we only initialize one time, with most up to date value
|
||||||
|
ns.queue.add(name);
|
||||||
|
} // After connectedCallback: guarded proxy to updateSync
|
||||||
|
else if (ctor.__syncUpdatableHasChanged(name, this[name], oldValue)) {
|
||||||
|
this.updateSync(name, oldValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc A public abstraction that has the exact same api as `_requestUpdate`.
|
||||||
|
* All code previously present in _requestUpdate can be placed in this method.
|
||||||
|
* @param {string} name
|
||||||
|
* @param {*} oldValue
|
||||||
|
*/
|
||||||
|
updateSync(name, oldValue) {} // eslint-disable-line class-methods-use-this, no-unused-vars
|
||||||
|
},
|
||||||
|
);
|
||||||
8
packages/validate/src/utils/fake-extends-event-target.js
Normal file
8
packages/validate/src/utils/fake-extends-event-target.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// TODO: this method has to be removed when EventTarget polyfill is available on IE11
|
||||||
|
export function fakeExtendsEventTarget(instance) {
|
||||||
|
const delegate = document.createDocumentFragment();
|
||||||
|
['addEventListener', 'dispatchEvent', 'removeEventListener'].forEach(funcName => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
instance[funcName] = (...args) => delegate[funcName](...args);
|
||||||
|
});
|
||||||
|
}
|
||||||
3
packages/validate/src/utils/pascal-case.js
Normal file
3
packages/validate/src/utils/pascal-case.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function pascalCase(str) {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
85
packages/validate/src/validators/DateValidators.js
Normal file
85
packages/validate/src/validators/DateValidators.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { normalizeDateTime } from '@lion/localize';
|
||||||
|
import { Validator } from '../Validator.js';
|
||||||
|
|
||||||
|
function isDate(value) {
|
||||||
|
return (
|
||||||
|
Object.prototype.toString.call(value) === '[object Date]' && !Number.isNaN(value.getTime())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IsDate extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'IsDate';
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
execute(value) {
|
||||||
|
let hasError = false;
|
||||||
|
if (!isDate(value)) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MinDate extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'MinDate';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(value, min = this.param) {
|
||||||
|
let hasError = false;
|
||||||
|
if (!isDate(value) || value < normalizeDateTime(min)) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MaxDate extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'MaxDate';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(value, max = this.param) {
|
||||||
|
let hasError = false;
|
||||||
|
if (!isDate(value) || value > normalizeDateTime(max)) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MinMaxDate extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'MinMaxDate';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(value, { min = 0, max = 0 } = this.param) {
|
||||||
|
let hasError = false;
|
||||||
|
if (!isDate(value) || value < normalizeDateTime(min) || value > normalizeDateTime(max)) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IsDateDisabled extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'IsDateDisabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(value, isDisabledFn = this.param) {
|
||||||
|
let hasError = false;
|
||||||
|
if (!isDate(value) || isDisabledFn(value)) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
packages/validate/src/validators/NumberValidators.js
Normal file
72
packages/validate/src/validators/NumberValidators.js
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { Validator } from '../Validator.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check for not being NaN (NaN is the only value in javascript which is not equal to itself)
|
||||||
|
*
|
||||||
|
* @param {number} value to check
|
||||||
|
*/
|
||||||
|
function isNumber(value) {
|
||||||
|
return value === value && typeof value === 'number'; // eslint-disable-line no-self-compare
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IsNumber extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'IsNumber';
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
execute(value) {
|
||||||
|
let isEnabled = false;
|
||||||
|
if (!isNumber(value)) {
|
||||||
|
isEnabled = true;
|
||||||
|
}
|
||||||
|
return isEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MinNumber extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'MinNumber';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(value, min = this.param) {
|
||||||
|
let isEnabled = false;
|
||||||
|
if (!isNumber(value) || value < min) {
|
||||||
|
isEnabled = true;
|
||||||
|
}
|
||||||
|
return isEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MaxNumber extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'MaxNumber';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(value, max = this.param) {
|
||||||
|
let isEnabled = false;
|
||||||
|
if (!isNumber(value) || value > max) {
|
||||||
|
isEnabled = true;
|
||||||
|
}
|
||||||
|
return isEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MinMaxNumber extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'MinMaxNumber';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(value, { min = 0, max = 0 } = this.param) {
|
||||||
|
let isEnabled = false;
|
||||||
|
if (!isNumber(value) || value < min || value > max) {
|
||||||
|
isEnabled = true;
|
||||||
|
}
|
||||||
|
return isEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
packages/validate/src/validators/Required.js
Normal file
28
packages/validate/src/validators/Required.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Validator } from '../Validator.js';
|
||||||
|
|
||||||
|
export class Required extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'Required';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We don't have an execute function, since the Required validator is 'special'.
|
||||||
|
* The outcome depends on the modelValue of the FormControl and
|
||||||
|
* FormControl.__isEmpty / FormControl._isEmpty.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
onFormControlConnect(formControl) {
|
||||||
|
if (formControl._inputNode) {
|
||||||
|
formControl._inputNode.setAttribute('aria-required', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
onFormControlDisconnect(formControl) {
|
||||||
|
if (formControl._inputNode) {
|
||||||
|
formControl._inputNode.removeAttribute('aria-required');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
packages/validate/src/validators/StringValidators.js
Normal file
97
packages/validate/src/validators/StringValidators.js
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { Validator } from '../Validator.js';
|
||||||
|
|
||||||
|
const isString = value => typeof value === 'string';
|
||||||
|
|
||||||
|
export class IsString extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'IsString';
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
execute(value) {
|
||||||
|
let hasError = false;
|
||||||
|
if (!isString(value)) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EqualsLength extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'EqualsLength';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(value, length = this.param) {
|
||||||
|
let hasError = false;
|
||||||
|
if (!isString(value) || value.length !== length) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MinLength extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'MinLength';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(value, min = this.param) {
|
||||||
|
let hasError = false;
|
||||||
|
if (!isString(value) || value.length < min) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MaxLength extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'MaxLength';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(value, max = this.param) {
|
||||||
|
let hasError = false;
|
||||||
|
if (!isString(value) || value.length > max) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MinMaxLength extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'MinMaxLength';
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(value, { min = 0, max = 0 } = this.param) {
|
||||||
|
let hasError = false;
|
||||||
|
if (!isString(value) || value.length <= min || value.length >= max) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEmailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
export class IsEmail extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'IsEmail';
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
execute(value) {
|
||||||
|
let hasError = false;
|
||||||
|
if (!isString(value) || !isEmailRegex.test(value.toLowerCase())) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
packages/validate/test-helpers/helper-validators.js
Normal file
52
packages/validate/test-helpers/helper-validators.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { Validator } from '../src/Validator.js';
|
||||||
|
|
||||||
|
export class AlwaysInvalid extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'AlwaysInvalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
execute() {
|
||||||
|
const showMessage = true;
|
||||||
|
return showMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AlwaysValid extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'AlwaysValid';
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
execute() {
|
||||||
|
const showMessage = false;
|
||||||
|
return showMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AsyncAlwaysValid extends AlwaysValid {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.async = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
execute() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AsyncAlwaysInvalid extends AlwaysValid {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.async = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
async execute() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
344
packages/validate/test-suites/FeedbackMixin.suite.js
Normal file
344
packages/validate/test-suites/FeedbackMixin.suite.js
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
/* eslint-disable max-classes-per-file, no-param-reassign, no-unused-expressions */
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { expect, fixture, html, unsafeStatic, defineCE } from '@open-wc/testing';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
import { ValidateMixin } from '../src/ValidateMixin.js';
|
||||||
|
import { Validator } from '../src/Validator.js';
|
||||||
|
import { Required } from '../src/validators/Required.js';
|
||||||
|
import { MinLength } from '../src/validators/StringValidators.js';
|
||||||
|
import { DefaultSuccess } from '../src/resultValidators/DefaultSuccess.js';
|
||||||
|
import { AlwaysInvalid } from '../test-helpers/helper-validators.js';
|
||||||
|
import '../lion-validation-feedback.js';
|
||||||
|
import { FeedbackMixin } from '../src/FeedbackMixin.js';
|
||||||
|
|
||||||
|
export function runFeedbackMixinSuite(customConfig) {
|
||||||
|
const cfg = {
|
||||||
|
tagString: null,
|
||||||
|
...customConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
const lightDom = cfg.lightDom || '';
|
||||||
|
|
||||||
|
describe('Validity Feedback', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends FeedbackMixin(ValidateMixin(LitElement)) {
|
||||||
|
static get properties() {
|
||||||
|
return { modelValue: String };
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.appendChild(document.createElement('input'));
|
||||||
|
}
|
||||||
|
|
||||||
|
get _inputNode() {
|
||||||
|
return this.querySelector('input');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
const tag = unsafeStatic(tagString);
|
||||||
|
|
||||||
|
class ContainsLowercaseA extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'ContainsLowercaseA';
|
||||||
|
this.execute = modelValue => !modelValue.includes('a');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContainsCat extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = 'containsCat';
|
||||||
|
this.execute = modelValue => !modelValue.includes('cat');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlwaysInvalid.getMessage = () => 'Message for AlwaysInvalid';
|
||||||
|
MinLength.getMessage = () => 'Message for MinLength';
|
||||||
|
ContainsLowercaseA.getMessage = () => 'Message for ContainsLowercaseA';
|
||||||
|
ContainsCat.getMessage = () => 'Message for ContainsCat';
|
||||||
|
|
||||||
|
it('sets ".hasErrorVisible"/[has-error-visible] when visibility condition is met', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag} .validators=${[new MinLength(3)]}>${lightDom}</${tag}>`);
|
||||||
|
|
||||||
|
if (cfg.enableFeedbackVisible) {
|
||||||
|
cfg.enableFeedbackVisible(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
el.modelValue = 'a';
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el.hasErrorVisible).to.be.true;
|
||||||
|
expect(el.hasAttribute('has-error-visible')).to.be.true;
|
||||||
|
|
||||||
|
el.modelValue = 'abc';
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el.hasErrorVisible).to.be.false;
|
||||||
|
expect(el.hasAttribute('has-error-visible')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes a message to the "._feedbackNode"', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.modelValue=${'cat'}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
||||||
|
el.validators = [new AlwaysInvalid()];
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for AlwaysInvalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has configurable feedback visibility hook', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.modelValue=${'cat'}
|
||||||
|
.validators=${[new AlwaysInvalid()]}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for AlwaysInvalid');
|
||||||
|
el._prioritizeAndFilterFeedback = () => []; // filter out all errors
|
||||||
|
await el.validate();
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes prioritized result to "._feedbackNode" based on Validator order', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.modelValue=${'cat'}
|
||||||
|
.validators=${[new AlwaysInvalid(), new MinLength(4)]}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for AlwaysInvalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders validation result to "._feedbackNode" when async messages are resolved', async () => {
|
||||||
|
let unlockMessage;
|
||||||
|
const messagePromise = new Promise(resolve => {
|
||||||
|
unlockMessage = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
AlwaysInvalid.getMessage = async () => {
|
||||||
|
await messagePromise;
|
||||||
|
return 'this ends up in "._feedbackNode"';
|
||||||
|
};
|
||||||
|
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.modelValue=${'cat'}
|
||||||
|
.validators=${[new AlwaysInvalid()]}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
||||||
|
unlockMessage();
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData[0].message).to.equal('this ends up in "._feedbackNode"');
|
||||||
|
});
|
||||||
|
|
||||||
|
// N.B. this replaces the 'config.hideFeedback' option we had before...
|
||||||
|
it('renders empty result when Validator.getMessage() returns "null"', async () => {
|
||||||
|
let unlockMessage;
|
||||||
|
const messagePromise = new Promise(resolve => {
|
||||||
|
unlockMessage = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
AlwaysInvalid.getMessage = async () => {
|
||||||
|
await messagePromise;
|
||||||
|
return 'this ends up in "._feedbackNode"';
|
||||||
|
};
|
||||||
|
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.modelValue=${'cat'}
|
||||||
|
.validators=${[new AlwaysInvalid()]}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
||||||
|
unlockMessage();
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData[0].message).to.equal('this ends up in "._feedbackNode"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports custom element to render feedback', async () => {
|
||||||
|
const customFeedbackTagString = defineCE(
|
||||||
|
class extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
feedbackData: Array,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
Custom for ${this.feedbackData[0].validator.name}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const customFeedbackTag = unsafeStatic(customFeedbackTagString);
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.validators=${[new ContainsLowercaseA(), new AlwaysInvalid()]}>
|
||||||
|
<${customFeedbackTag} slot="feedback"><${customFeedbackTag}>
|
||||||
|
</${tag}>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(el._feedbackNode.localName).to.equal(customFeedbackTagString);
|
||||||
|
|
||||||
|
el.modelValue = 'dog';
|
||||||
|
await el.feedbackComplete;
|
||||||
|
await el._feedbackNode.updateComplete;
|
||||||
|
expect(el._feedbackNode).shadowDom.to.equal('Custom for ContainsLowercaseA');
|
||||||
|
|
||||||
|
el.modelValue = 'cat';
|
||||||
|
await el.feedbackComplete;
|
||||||
|
await el._feedbackNode.updateComplete;
|
||||||
|
expect(el._feedbackNode).shadowDom.to.equal('Custom for AlwaysInvalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports custom messages in Validator instance configuration object', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag} .validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
|
||||||
|
el.modelValue = 'a';
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData[0].message).to.equal('custom via config');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows success message after fixing an error', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.validators=${[
|
||||||
|
new MinLength(3),
|
||||||
|
new DefaultSuccess(null, { getMessage: () => 'This is a success message' }),
|
||||||
|
]}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
|
||||||
|
el.modelValue = 'a';
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for MinLength');
|
||||||
|
|
||||||
|
el.modelValue = 'abcd';
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData[0].message).to.equal('This is a success message');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('sets [aria-invalid="true"] to "._inputNode" when ".hasError" is true', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.validators=${[new Required()]}
|
||||||
|
.modelValue=${'a'}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
const inputNode = el._inputNode;
|
||||||
|
|
||||||
|
expect(inputNode.getAttribute('aria-invalid')).to.equal('false');
|
||||||
|
|
||||||
|
el.modelValue = '';
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(inputNode.getAttribute('aria-invalid')).to.equal('true');
|
||||||
|
el.modelValue = 'a';
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(inputNode.getAttribute('aria-invalid')).to.equal('false');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Meta data', () => {
|
||||||
|
it('".getMessage()" gets a reference to formControl, validatorParams and modelValue', async () => {
|
||||||
|
let el;
|
||||||
|
const constructorValidator = new MinLength(4, { type: 'x' }); // type to prevent duplicates
|
||||||
|
const constructorMessageSpy = sinon.spy(constructorValidator.constructor, 'getMessage');
|
||||||
|
|
||||||
|
el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.validators=${[constructorValidator]}
|
||||||
|
.modelValue=${'cat'}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(constructorMessageSpy.args[0][0]).to.eql({
|
||||||
|
validatorParams: 4,
|
||||||
|
modelValue: 'cat',
|
||||||
|
formControl: el,
|
||||||
|
fieldName: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const instanceMessageSpy = sinon.spy();
|
||||||
|
const instanceValidator = new MinLength(4, { getMessage: instanceMessageSpy });
|
||||||
|
|
||||||
|
el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.validators=${[instanceValidator]}
|
||||||
|
.modelValue=${'cat'}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(instanceMessageSpy.args[0][0]).to.eql({
|
||||||
|
validatorParams: 4,
|
||||||
|
modelValue: 'cat',
|
||||||
|
formControl: el,
|
||||||
|
fieldName: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('".getMessage()" gets .fieldName defined on instance', async () => {
|
||||||
|
const constructorValidator = new MinLength(4, { type: 'x' }); // type to prevent duplicates
|
||||||
|
const spy = sinon.spy(constructorValidator.constructor, 'getMessage');
|
||||||
|
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.validators=${[constructorValidator]}
|
||||||
|
.modelValue=${'cat'}
|
||||||
|
.fieldName=${new Promise(resolve => resolve('myField'))}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(spy.args[0][0]).to.eql({
|
||||||
|
validatorParams: 4,
|
||||||
|
modelValue: 'cat',
|
||||||
|
formControl: el,
|
||||||
|
fieldName: 'myField',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('".getMessage()" gets .fieldName defined on Validator config', async () => {
|
||||||
|
const constructorValidator = new MinLength(4, {
|
||||||
|
fieldName: new Promise(resolve => resolve('myFieldViaCfg')),
|
||||||
|
});
|
||||||
|
const spy = sinon.spy(constructorValidator.constructor, 'getMessage');
|
||||||
|
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.validators=${[constructorValidator]}
|
||||||
|
.modelValue=${'cat'}
|
||||||
|
.fieldName=${new Promise(resolve => resolve('myField'))}
|
||||||
|
>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(spy.args[0][0]).to.eql({
|
||||||
|
validatorParams: 4,
|
||||||
|
modelValue: 'cat',
|
||||||
|
formControl: el,
|
||||||
|
fieldName: 'myFieldViaCfg',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
1155
packages/validate/test-suites/ValidateMixin.suite.js
Normal file
1155
packages/validate/test-suites/ValidateMixin.suite.js
Normal file
File diff suppressed because it is too large
Load diff
99
packages/validate/test/DateValidators.test.js
Normal file
99
packages/validate/test/DateValidators.test.js
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { expect } from '@open-wc/testing';
|
||||||
|
|
||||||
|
import { normalizeDateTime } from '@lion/localize';
|
||||||
|
import {
|
||||||
|
IsDate,
|
||||||
|
MinDate,
|
||||||
|
MaxDate,
|
||||||
|
MinMaxDate,
|
||||||
|
IsDateDisabled,
|
||||||
|
} from '../src/validators/DateValidators.js';
|
||||||
|
|
||||||
|
describe('Date Validation', () => {
|
||||||
|
it('provides new isDate() to allow only dates', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new IsDate();
|
||||||
|
expect(validator.name).to.equal('IsDate');
|
||||||
|
|
||||||
|
isEnabled = validator.execute(new Date());
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(4);
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides new minDate(x) to allow only dates after min', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new MinDate(new Date('2018/02/02'));
|
||||||
|
expect(validator.name).to.equal('MinDate');
|
||||||
|
|
||||||
|
isEnabled = validator.execute(new Date('2018-02-03'));
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(new Date('2018-02-01'));
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const todayFormatted = normalizeDateTime(today);
|
||||||
|
const todayValidator = new MinDate(today);
|
||||||
|
isEnabled = todayValidator.execute(todayFormatted);
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides maxDate() to allow only dates before max', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new MaxDate(new Date('2018/02/02'));
|
||||||
|
expect(validator.name).to.equal('MaxDate');
|
||||||
|
|
||||||
|
isEnabled = validator.execute(new Date('2018-02-01'));
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(new Date('2018-02-03'));
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const todayFormatted = normalizeDateTime(today);
|
||||||
|
const todayValidator = new MaxDate(today);
|
||||||
|
isEnabled = todayValidator.execute(todayFormatted);
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides new MinMaxDate() to allow only dates between min and max', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new MinMaxDate({
|
||||||
|
min: new Date('2018/02/02'),
|
||||||
|
max: new Date('2018/02/04'),
|
||||||
|
});
|
||||||
|
expect(validator.name).to.equal('MinMaxDate');
|
||||||
|
|
||||||
|
isEnabled = validator.execute(new Date('2018/02/03'));
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(new Date('2018/02/01'));
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(new Date('2018/02/05'));
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const todayFormatted = normalizeDateTime(today);
|
||||||
|
const todayValidator = new MinMaxDate({ min: today, max: today });
|
||||||
|
isEnabled = todayValidator.execute(todayFormatted);
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides new IsDateDisabled() to disable dates matching specified condition', () => {
|
||||||
|
let isDisabled;
|
||||||
|
const validator = new IsDateDisabled(d => d.getDate() === 3);
|
||||||
|
expect(validator.name).to.equal('IsDateDisabled');
|
||||||
|
|
||||||
|
isDisabled = validator.execute(new Date('2018/02/04'));
|
||||||
|
expect(isDisabled).to.be.false;
|
||||||
|
|
||||||
|
isDisabled = validator.execute(new Date('2018/02/03'));
|
||||||
|
expect(isDisabled).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
3
packages/validate/test/FeedbackMixin.test.js
Normal file
3
packages/validate/test/FeedbackMixin.test.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { runFeedbackMixinSuite } from '../test-suites/FeedbackMixin.suite.js';
|
||||||
|
|
||||||
|
runFeedbackMixinSuite();
|
||||||
66
packages/validate/test/NumberValidators.test.js
Normal file
66
packages/validate/test/NumberValidators.test.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { expect } from '@open-wc/testing';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IsNumber,
|
||||||
|
MinNumber,
|
||||||
|
MaxNumber,
|
||||||
|
MinMaxNumber,
|
||||||
|
} from '../src/validators/NumberValidators.js';
|
||||||
|
|
||||||
|
describe('Number Validation', () => {
|
||||||
|
it('provides new IsNumber() to allow only numbers', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new IsNumber();
|
||||||
|
expect(validator.name).to.equal('IsNumber');
|
||||||
|
|
||||||
|
isEnabled = validator.execute(4);
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(NaN);
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('4');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides new MinNumber(x) to allow only numbers longer then min', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new MinNumber(3);
|
||||||
|
expect(validator.name).to.equal('MinNumber');
|
||||||
|
|
||||||
|
isEnabled = validator.execute(3);
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(2);
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides new MaxNumber(x) to allow only number shorter then max', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new MaxNumber(3);
|
||||||
|
expect(validator.name).to.equal('MaxNumber');
|
||||||
|
|
||||||
|
isEnabled = validator.execute(3);
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(4);
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides new MinMaxNumber({ min: x, max: y}) to allow only numbers between min and max', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new MinMaxNumber({ min: 2, max: 4 });
|
||||||
|
expect(validator.name).to.equal('MinMaxNumber');
|
||||||
|
|
||||||
|
isEnabled = validator.execute(2);
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
isEnabled = validator.execute(4);
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(1);
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(5);
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
114
packages/validate/test/StringValidators.test.js
Normal file
114
packages/validate/test/StringValidators.test.js
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { expect } from '@open-wc/testing';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
EqualsLength,
|
||||||
|
MinLength,
|
||||||
|
MaxLength,
|
||||||
|
MinMaxLength,
|
||||||
|
IsEmail,
|
||||||
|
} from '../src/validators/StringValidators.js';
|
||||||
|
|
||||||
|
describe('String Validation', () => {
|
||||||
|
it('provides new IsString() to allow only strings', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new IsString();
|
||||||
|
expect(validator.name).to.equal('IsString');
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo');
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(NaN);
|
||||||
|
expect(validator.execute(NaN)).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute(4);
|
||||||
|
expect(validator.execute(4)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides new EqualsLength(x) to allow only a specific string length', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new EqualsLength(3);
|
||||||
|
expect(validator.name).to.equal('EqualsLength');
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo');
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('fo');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foobar');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides new MinLength(x) to allow only strings longer then min', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new MinLength(3);
|
||||||
|
expect(validator.name).to.equal('MinLength');
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo');
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('fo');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides new MaxLength(x) to allow only strings shorter then max', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new MaxLength(3);
|
||||||
|
expect(validator.name).to.equal('MaxLength');
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo');
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foobar');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides new MinMaxValidator({ min: x, max: y}) to allow only strings between min and max', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new MinMaxLength({ min: 2, max: 4 });
|
||||||
|
expect(validator.name).to.equal('MinMaxLength');
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo');
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('f');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foobar');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides new IsEmail() to allow only valid email formats', () => {
|
||||||
|
let isEnabled;
|
||||||
|
const validator = new IsEmail();
|
||||||
|
expect(validator.name).to.equal('IsEmail');
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo@bar.com');
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('name!#$%*@bar.com');
|
||||||
|
expect(isEnabled).to.be.false;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo@');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('bar.com');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('@bar.com');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo@bar@example.com');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo@bar');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
|
||||||
|
isEnabled = validator.execute('foo@120.120.120.93');
|
||||||
|
expect(isEnabled).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
255
packages/validate/test/SyncUpdatableMixin.test.js
Normal file
255
packages/validate/test/SyncUpdatableMixin.test.js
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
import { expect, fixtureSync, defineCE, unsafeStatic, html, fixture } from '@open-wc/testing';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import { UpdatingElement } from '@lion/core';
|
||||||
|
import { SyncUpdatableMixin } from '../src/utils/SyncUpdatableMixin.js';
|
||||||
|
|
||||||
|
describe('SyncUpdatableMixin', () => {
|
||||||
|
describe('Until firstUpdated', () => {
|
||||||
|
it('initializes all properties', async () => {
|
||||||
|
let hasCalledFirstUpdated = false;
|
||||||
|
let hasCalledUpdateSync = false;
|
||||||
|
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends SyncUpdatableMixin(UpdatingElement) {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
propA: { type: String },
|
||||||
|
propB: {
|
||||||
|
type: String,
|
||||||
|
attribute: 'prop-b',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.propA = 'init-a';
|
||||||
|
this.propB = 'init-b';
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated(c) {
|
||||||
|
super.firstUpdated(c);
|
||||||
|
hasCalledFirstUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSync(...args) {
|
||||||
|
super.updateSync(...args);
|
||||||
|
hasCalledUpdateSync = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const tag = unsafeStatic(tagString);
|
||||||
|
const el = fixtureSync(html`<${tag} prop-b="b"></${tag}>`);
|
||||||
|
|
||||||
|
// Getters setters work as expected, without running property effects
|
||||||
|
expect(el.propA).to.equal('init-a');
|
||||||
|
expect(el.propB).to.equal('b');
|
||||||
|
el.propA = 'a2';
|
||||||
|
expect(el.propA).to.equal('a2');
|
||||||
|
expect(hasCalledFirstUpdated).to.be.false;
|
||||||
|
expect(hasCalledUpdateSync).to.be.false;
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(hasCalledFirstUpdated).to.be.true;
|
||||||
|
expect(hasCalledUpdateSync).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence
|
||||||
|
it('guarantees Member Order Independence', async () => {
|
||||||
|
let hasCalledRunPropertyEffect = false;
|
||||||
|
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends SyncUpdatableMixin(UpdatingElement) {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
propA: { type: String },
|
||||||
|
propB: {
|
||||||
|
type: String,
|
||||||
|
attribute: 'prop-b',
|
||||||
|
},
|
||||||
|
derived: { type: String },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.propA = 'init-a';
|
||||||
|
this.propB = 'init-b';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSync(name, oldValue) {
|
||||||
|
super.updateSync(name, oldValue);
|
||||||
|
|
||||||
|
if (name === 'propB') {
|
||||||
|
this._runPropertyEffect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_runPropertyEffect() {
|
||||||
|
hasCalledRunPropertyEffect = true;
|
||||||
|
this.derived = this.propA + this.propB;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const tag = unsafeStatic(tagString);
|
||||||
|
const el = fixtureSync(html`<${tag} prop-b="b" .propA="${'a'}"></${tag}>`);
|
||||||
|
|
||||||
|
// Derived
|
||||||
|
expect(el.derived).to.be.undefined;
|
||||||
|
expect(hasCalledRunPropertyEffect).to.be.false;
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.derived).to.equal('ab');
|
||||||
|
expect(hasCalledRunPropertyEffect).to.be.true;
|
||||||
|
|
||||||
|
const el2 = await fixture(html`<${tag} .propA="${'a'}"></${tag}>`);
|
||||||
|
expect(el2.derived).to.equal('ainit-b');
|
||||||
|
|
||||||
|
const el3 = await fixture(html`<${tag} .propB="${'b'}"></${tag}>`);
|
||||||
|
expect(el3.derived).to.equal('init-ab');
|
||||||
|
|
||||||
|
const el4 = await fixture(html`<${tag} .propA=${'a'} .propB="${'b'}"></${tag}>`);
|
||||||
|
expect(el4.derived).to.equal('ab');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs "updateSync" once per property with most current value', async () => {
|
||||||
|
let propChangedCount = 0;
|
||||||
|
let propUpdateSyncCount = 0;
|
||||||
|
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends SyncUpdatableMixin(UpdatingElement) {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
prop: { type: String },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.prop = 'a';
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestUpdate(name, oldValue) {
|
||||||
|
super._requestUpdate(name, oldValue);
|
||||||
|
if (name === 'prop') {
|
||||||
|
propChangedCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSync(name, oldValue) {
|
||||||
|
super.updateSync(name, oldValue);
|
||||||
|
if (name === 'prop') {
|
||||||
|
propUpdateSyncCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const tag = unsafeStatic(tagString);
|
||||||
|
const el = fixtureSync(html`<${tag}></${tag}>`);
|
||||||
|
el.prop = 'a';
|
||||||
|
// Getters setters work as expected, without running property effects
|
||||||
|
expect(propChangedCount).to.equal(2);
|
||||||
|
expect(propUpdateSyncCount).to.equal(0);
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(propChangedCount).to.equal(2);
|
||||||
|
expect(propUpdateSyncCount).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('After firstUpdated', () => {
|
||||||
|
it('calls "updateSync" immediately when the observed property is changed (newValue !== oldValue)', async () => {
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends SyncUpdatableMixin(UpdatingElement) {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
propA: { type: String },
|
||||||
|
propB: {
|
||||||
|
type: String,
|
||||||
|
attribute: 'prop-b',
|
||||||
|
},
|
||||||
|
derived: { type: String },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.propA = 'init-a';
|
||||||
|
this.propB = 'init-b';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSync(name, oldValue) {
|
||||||
|
super.updateSync(name, oldValue);
|
||||||
|
|
||||||
|
if (name === 'propB') {
|
||||||
|
this._runPropertyEffect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_runPropertyEffect() {
|
||||||
|
this.derived = this.propA + this.propB;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const tag = unsafeStatic(tagString);
|
||||||
|
const el = fixtureSync(html`<${tag} prop-b="b" .propA="${'a'}"></${tag}>`);
|
||||||
|
const spy = sinon.spy(el, '_runPropertyEffect');
|
||||||
|
expect(spy.callCount).to.equal(0);
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.derived).to.equal('ab');
|
||||||
|
expect(spy.callCount).to.equal(1);
|
||||||
|
el.propB = 'b2';
|
||||||
|
expect(el.derived).to.equal('ab2');
|
||||||
|
expect(spy.callCount).to.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Features', () => {
|
||||||
|
// See: https://lit-element.polymer-project.org/guide/lifecycle#haschanged
|
||||||
|
it('supports "hasChanged" from UpdatingElement', async () => {
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends SyncUpdatableMixin(UpdatingElement) {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
complexProp: {
|
||||||
|
type: Object,
|
||||||
|
hasChanged: (result, prevResult) => {
|
||||||
|
// Simple way of doing a deep comparison
|
||||||
|
if (JSON.stringify(result) !== JSON.stringify(prevResult)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSync(name, oldValue) {
|
||||||
|
super.updateSync(name, oldValue);
|
||||||
|
|
||||||
|
if (name === 'complexProp') {
|
||||||
|
this._onComplexPropChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onComplexPropChanged() {
|
||||||
|
// do smth
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const tag = unsafeStatic(tagString);
|
||||||
|
const el = fixtureSync(html`<${tag}></${tag}>`);
|
||||||
|
const spy = sinon.spy(el, '_onComplexPropChanged');
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(spy.callCount).to.equal(0);
|
||||||
|
el.complexProp = { key1: true };
|
||||||
|
expect(spy.callCount).to.equal(1);
|
||||||
|
el.complexProp = { key1: false };
|
||||||
|
expect(spy.callCount).to.equal(2);
|
||||||
|
el.complexProp = { key1: false };
|
||||||
|
expect(spy.callCount).to.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { expect } from '@open-wc/testing';
|
|
||||||
import { Unparseable } from '../src/Unparseable.js';
|
|
||||||
|
|
||||||
describe('Unparseable', () => {
|
|
||||||
it(`can be instantiated`, async () => {
|
|
||||||
const instance = new Unparseable('my view value');
|
|
||||||
expect(instance instanceof Unparseable).to.equal(true);
|
|
||||||
});
|
|
||||||
it(`contains a viewValue`, async () => {
|
|
||||||
const instance = new Unparseable('my view value');
|
|
||||||
expect(instance.viewValue).to.equal('my view value');
|
|
||||||
});
|
|
||||||
it(`contains a type`, async () => {
|
|
||||||
const instance = new Unparseable('my view value');
|
|
||||||
expect(instance.type).to.equal('unparseable');
|
|
||||||
});
|
|
||||||
it(`is serialized as an object`, async () => {
|
|
||||||
const instance = new Unparseable('my view value');
|
|
||||||
expect(instance.toString()).to.equal('{"type":"unparseable","viewValue":"my view value"}');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
<<<<<<< HEAD
|
||||||
/* eslint-disable no-unused-vars, no-param-reassign */
|
/* eslint-disable no-unused-vars, no-param-reassign */
|
||||||
import { expect, fixture, html, unsafeStatic, defineCE, aTimeout } from '@open-wc/testing';
|
import { expect, fixture, html, unsafeStatic, defineCE, aTimeout } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
@ -1341,3 +1342,8 @@ describe('ValidateMixin', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
=======
|
||||||
|
import { runValidateMixinSuite } from '../test-suites/ValidateMixin.suite.js';
|
||||||
|
|
||||||
|
runValidateMixinSuite();
|
||||||
|
>>>>>>> feat(validate): new validation api, async validation and more
|
||||||
|
|
|
||||||
128
packages/validate/test/Validator.test.js
Normal file
128
packages/validate/test/Validator.test.js
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { expect, fixture, html, unsafeStatic, defineCE } from '@open-wc/testing';
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import { ValidateMixin } from '../src/ValidateMixin.js';
|
||||||
|
import { Validator } from '../src/Validator.js';
|
||||||
|
import { ResultValidator } from '../src/ResultValidator.js';
|
||||||
|
import { Required } from '../src/validators/Required.js';
|
||||||
|
import { MinLength } from '../src/validators/StringValidators.js';
|
||||||
|
|
||||||
|
describe('Validator', () => {
|
||||||
|
it('has an "execute" function returning "shown" state', async () => {
|
||||||
|
class MyValidator extends Validator {
|
||||||
|
execute(modelValue, param) {
|
||||||
|
const hasError = modelValue === 'test' && param === 'me';
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(new MyValidator().execute('test', 'me')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receives a "param" as a first argument on instantiation', async () => {
|
||||||
|
const vali = new Validator('myParam');
|
||||||
|
expect(vali.param).to.equal('myParam');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receives a config object (optionally) as a second argument on instantiation', async () => {
|
||||||
|
const vali = new Validator('myParam', { my: 'config' });
|
||||||
|
expect(vali.config).to.eql({ my: 'config' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires "param-changed" event on param change', async () => {
|
||||||
|
const vali = new Validator('foo');
|
||||||
|
const cb = sinon.spy(() => {});
|
||||||
|
vali.addEventListener('param-changed', cb);
|
||||||
|
vali.param = 'bar';
|
||||||
|
expect(cb.callCount).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires "config-changed" event on config change', async () => {
|
||||||
|
const vali = new Validator('foo', { foo: 'bar' });
|
||||||
|
const cb = sinon.spy(() => {});
|
||||||
|
vali.addEventListener('config-changed', cb);
|
||||||
|
vali.config = { bar: 'foo' };
|
||||||
|
expect(cb.callCount).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has access to FormControl', async () => {
|
||||||
|
const lightDom = '';
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends ValidateMixin(LitElement) {
|
||||||
|
static get properties() {
|
||||||
|
return { modelValue: String };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const tag = unsafeStatic(tagString);
|
||||||
|
|
||||||
|
class MyValidator extends Validator {
|
||||||
|
execute(modelValue, param) {
|
||||||
|
const hasError = modelValue === 'forbidden' && param === 'values';
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
onFormControlConnect(formControl) {
|
||||||
|
// I could do something like:
|
||||||
|
// - add aria-required="true"
|
||||||
|
// - add type restriction for MaxLength(3, { isBlocking: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
onFormControlDisconnect(formControl) {
|
||||||
|
// I will cleanup what I did in connect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const myVal = new MyValidator();
|
||||||
|
const connectSpy = sinon.spy(myVal, 'onFormControlConnect');
|
||||||
|
const disconnectSpy = sinon.spy(myVal, 'onFormControlDisconnect');
|
||||||
|
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag} .validators=${[myVal]}>${lightDom}</${tag}>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(connectSpy.callCount).to.equal(1);
|
||||||
|
expect(connectSpy.calledWith(el)).to.equal(true);
|
||||||
|
expect(disconnectSpy.callCount).to.equal(0);
|
||||||
|
|
||||||
|
el.validators = [];
|
||||||
|
expect(connectSpy.callCount).to.equal(1);
|
||||||
|
expect(disconnectSpy.callCount).to.equal(1);
|
||||||
|
expect(disconnectSpy.calledWith(el)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Types', () => {
|
||||||
|
it('has type "error" by default', async () => {
|
||||||
|
expect(new Validator().type).to.equal('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports customized types', async () => {
|
||||||
|
// This test shows the best practice of adding custom types
|
||||||
|
class MyValidator extends Validator {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.type = 'my-type';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(new MyValidator().type).to.equal('my-type');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ResultValidator', () => {
|
||||||
|
it('has an "executeOnResults" function returning active state', async () => {
|
||||||
|
// This test shows the best practice of creating executeOnResults method
|
||||||
|
class MyResultValidator extends ResultValidator {
|
||||||
|
executeOnResults({ regularValidateResult, prevValidationResult }) {
|
||||||
|
const hasSuccess = regularValidateResult.length && !prevValidationResult.length;
|
||||||
|
return hasSuccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
new MyResultValidator().executeOnResults({
|
||||||
|
regularValidateResult: [new Required(), new MinLength(3)],
|
||||||
|
prevValidationResult: [],
|
||||||
|
}),
|
||||||
|
).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { expect } from '@open-wc/testing';
|
|
||||||
import { isValidatorApplied } from '../src/isValidatorApplied.js';
|
|
||||||
|
|
||||||
describe('isValidatorApplied', () => {
|
|
||||||
it(`checks if validator (provided name string) is applied`, async () => {
|
|
||||||
const myValFn = (val, param) => ({ myValFn: param === 'x' });
|
|
||||||
const myOtherValFn = (val, param) => ({ myOtherValFn: param === 'x' });
|
|
||||||
|
|
||||||
expect(isValidatorApplied('myValFn', myValFn, 'x')).to.equal(true);
|
|
||||||
expect(isValidatorApplied('myValFn', myValFn, 'y')).to.equal(true);
|
|
||||||
|
|
||||||
expect(isValidatorApplied('myValFn', myOtherValFn, 'x')).to.equal(false);
|
|
||||||
expect(isValidatorApplied('myValFn', myOtherValFn, 'y')).to.equal(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
18
packages/validate/test/lion-validation-feedback.test.js
Normal file
18
packages/validate/test/lion-validation-feedback.test.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/* eslint-disable no-unused-vars, no-param-reassign */
|
||||||
|
import { fixture, html, expect } from '@open-wc/testing';
|
||||||
|
import '../lion-validation-feedback.js';
|
||||||
|
import { AlwaysInvalid } from '../test-helpers/helper-validators.js';
|
||||||
|
|
||||||
|
describe('lion-validation-feedback', () => {
|
||||||
|
it('renders a validation message', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html`
|
||||||
|
<lion-validation-feedback></lion-validation-feedback>
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
expect(el).shadowDom.to.equal('');
|
||||||
|
el.feedbackData = [{ message: 'hello', type: 'error', validator: new AlwaysInvalid() }];
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el).shadowDom.to.equal('hello');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
import { expect } from '@open-wc/testing';
|
|
||||||
import { normalizeDateTime } from '@lion/localize';
|
|
||||||
import { smokeTestValidator } from '../test-helpers.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
isString,
|
|
||||||
equalsLength,
|
|
||||||
minLength,
|
|
||||||
maxLength,
|
|
||||||
minMaxLength,
|
|
||||||
isEmail,
|
|
||||||
isStringValidator,
|
|
||||||
equalsLengthValidator,
|
|
||||||
minLengthValidator,
|
|
||||||
maxLengthValidator,
|
|
||||||
minMaxLengthValidator,
|
|
||||||
isEmailValidator,
|
|
||||||
isNumber,
|
|
||||||
minNumber,
|
|
||||||
maxNumber,
|
|
||||||
minMaxNumber,
|
|
||||||
isNumberValidator,
|
|
||||||
minNumberValidator,
|
|
||||||
maxNumberValidator,
|
|
||||||
minMaxNumberValidator,
|
|
||||||
isDate,
|
|
||||||
minDate,
|
|
||||||
maxDate,
|
|
||||||
isDateDisabled,
|
|
||||||
minMaxDate,
|
|
||||||
isDateValidator,
|
|
||||||
minDateValidator,
|
|
||||||
maxDateValidator,
|
|
||||||
minMaxDateValidator,
|
|
||||||
isDateDisabledValidator,
|
|
||||||
randomOk,
|
|
||||||
defaultOk,
|
|
||||||
randomOkValidator,
|
|
||||||
defaultOkValidator,
|
|
||||||
} from '../src/validators.js';
|
|
||||||
|
|
||||||
describe('LionValidate', () => {
|
|
||||||
describe('String Validation', () => {
|
|
||||||
it('provides isString() to allow only strings', () => {
|
|
||||||
expect(isString('foo')).to.be.true;
|
|
||||||
expect(isString(NaN)).to.be.false;
|
|
||||||
expect(isString(4)).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides equalsLength() to allow only a specific string length', () => {
|
|
||||||
expect(equalsLength('foo', 3)).to.be.true;
|
|
||||||
expect(equalsLength('fo', 3)).to.be.false;
|
|
||||||
expect(equalsLength('foobar', 3)).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides minLength() to allow only strings longer then min', () => {
|
|
||||||
expect(minLength('foo', 3)).to.be.true;
|
|
||||||
expect(minLength('fo', 3)).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides maxLength() to allow only strings shorter then max', () => {
|
|
||||||
expect(maxLength('foo', 3)).to.be.true;
|
|
||||||
expect(maxLength('foobar', 3)).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides minMaxLength() to allow only strings between min and max', () => {
|
|
||||||
expect(minMaxLength('foo', { min: 2, max: 4 })).to.be.true;
|
|
||||||
expect(minMaxLength('f', { min: 2, max: 4 })).to.be.false;
|
|
||||||
expect(minMaxLength('foobar', { min: 2, max: 4 })).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides isEmail() to allow only valid email formats', () => {
|
|
||||||
expect(isEmail('foo@bar.com')).to.be.true;
|
|
||||||
expect(isEmail('name!#$%*@bar.com')).to.be.true;
|
|
||||||
expect(isEmail('foo')).to.be.false;
|
|
||||||
expect(isEmail('foo@')).to.be.false;
|
|
||||||
expect(isEmail('@bar')).to.be.false;
|
|
||||||
expect(isEmail('bar.com')).to.be.false;
|
|
||||||
expect(isEmail('@bar.com')).to.be.false;
|
|
||||||
expect(isEmail('foo@bar@example.com')).to.be.false;
|
|
||||||
expect(isEmail('foo@bar')).to.be.false;
|
|
||||||
expect(isEmail('foo@120.120.120.93')).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides {isString, equalsLength, minLength, maxLength, minMaxLength, isEmail}Validator factory function for all types', () => {
|
|
||||||
// do a smoke test for each type
|
|
||||||
smokeTestValidator('isString', isStringValidator, 'foo');
|
|
||||||
smokeTestValidator('equalsLength', equalsLengthValidator, 'foo', 3);
|
|
||||||
smokeTestValidator('minLength', minLengthValidator, 'foo', 3);
|
|
||||||
smokeTestValidator('maxLength', maxLengthValidator, 'foo', 3);
|
|
||||||
smokeTestValidator('minMaxLength', minMaxLengthValidator, 'foo', { min: 2, max: 4 });
|
|
||||||
smokeTestValidator('isEmail', isEmailValidator, 'foo@bar.com');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Number Validation', () => {
|
|
||||||
it('provides isNumber() to allow only numbers', () => {
|
|
||||||
expect(isNumber(4)).to.be.true;
|
|
||||||
expect(isNumber(NaN)).to.be.false;
|
|
||||||
expect(isNumber('4')).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides minNumber() to allow only numbers longer then min', () => {
|
|
||||||
expect(minNumber(3, 3)).to.be.true;
|
|
||||||
expect(minNumber(2, 3)).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides maxNumber() to allow only number shorter then max', () => {
|
|
||||||
expect(maxNumber(3, 3)).to.be.true;
|
|
||||||
expect(maxNumber(4, 3)).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides minMaxNumber() to allow only numbers between min and max', () => {
|
|
||||||
expect(minMaxNumber(3, { min: 2, max: 4 })).to.be.true;
|
|
||||||
expect(minMaxNumber(1, { min: 2, max: 4 })).to.be.false;
|
|
||||||
expect(minMaxNumber(5, { min: 2, max: 4 })).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides {isNumber, minNumber, maxNumber, minMaxNumber}Validator factory function for all types', () => {
|
|
||||||
// do a smoke test for each type
|
|
||||||
smokeTestValidator('isNumber', isNumberValidator, 4);
|
|
||||||
smokeTestValidator('minNumber', minNumberValidator, 3, 3);
|
|
||||||
smokeTestValidator('maxNumber', maxNumberValidator, 3, 3);
|
|
||||||
smokeTestValidator('minMaxNumber', minMaxNumberValidator, 3, { min: 2, max: 4 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Date Validation', () => {
|
|
||||||
it('provides isDate() to allow only dates', () => {
|
|
||||||
expect(isDate(new Date())).to.be.true;
|
|
||||||
expect(isDate('foo')).to.be.false;
|
|
||||||
expect(isDate(4)).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides minDate() to allow only dates after min', () => {
|
|
||||||
expect(minDate(new Date('2018-02-03'), new Date('2018/02/02'))).to.be.true;
|
|
||||||
expect(minDate(new Date('2018-02-01'), new Date('2018/02/02'))).to.be.false;
|
|
||||||
const today = new Date();
|
|
||||||
const todayFormatted = normalizeDateTime(today);
|
|
||||||
expect(minDate(todayFormatted, today)).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides maxDate() to allow only dates before max', () => {
|
|
||||||
expect(maxDate(new Date('2018-02-01'), new Date('2018/02/02'))).to.be.true;
|
|
||||||
expect(maxDate(new Date('2018-02-03'), new Date('2018/02/02'))).to.be.false;
|
|
||||||
const today = new Date();
|
|
||||||
const todayFormatted = normalizeDateTime(today);
|
|
||||||
expect(maxDate(todayFormatted, today)).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides minMaxDate() to allow only dates between min and max', () => {
|
|
||||||
const minMaxSetting = {
|
|
||||||
min: new Date('2018/02/02'),
|
|
||||||
max: new Date('2018/02/04'),
|
|
||||||
};
|
|
||||||
expect(minMaxDate(new Date('2018/02/03'), minMaxSetting)).to.be.true;
|
|
||||||
expect(minMaxDate(new Date('2018/02/01'), minMaxSetting)).to.be.false;
|
|
||||||
expect(minMaxDate(new Date('2018/02/05'), minMaxSetting)).to.be.false;
|
|
||||||
const today = new Date();
|
|
||||||
const todayFormatted = normalizeDateTime(today);
|
|
||||||
expect(minMaxDate(todayFormatted, { min: today, max: today })).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides isDateDisabled() to disable dates matching specified condition', () => {
|
|
||||||
expect(isDateDisabled(new Date('2018/02/03'), d => d.getDate() === 3)).to.be.false;
|
|
||||||
expect(isDateDisabled(new Date('2018/02/04'), d => d.getDate() === 3)).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('provides {isDate, minDate, maxDate, minMaxDate, isDateDisabled}Validator factory function for all types', () => {
|
|
||||||
// do a smoke test for each type
|
|
||||||
smokeTestValidator('isDate', isDateValidator, new Date());
|
|
||||||
smokeTestValidator(
|
|
||||||
'minDate',
|
|
||||||
minDateValidator,
|
|
||||||
new Date('2018/02/03'),
|
|
||||||
new Date('2018/02/02'),
|
|
||||||
);
|
|
||||||
smokeTestValidator(
|
|
||||||
'maxDate',
|
|
||||||
maxDateValidator,
|
|
||||||
new Date('2018/02/01'),
|
|
||||||
new Date('2018/02/02'),
|
|
||||||
);
|
|
||||||
const minMaxSetting = {
|
|
||||||
min: new Date('2018/02/02'),
|
|
||||||
max: new Date('2018/02/04'),
|
|
||||||
};
|
|
||||||
smokeTestValidator('minMaxDate', minMaxDateValidator, new Date('2018/02/03'), minMaxSetting);
|
|
||||||
smokeTestValidator(
|
|
||||||
'isDateDisabled',
|
|
||||||
isDateDisabledValidator,
|
|
||||||
new Date('2018/02/03'),
|
|
||||||
d => d.getDate() === 15,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Success Validation', () => {
|
|
||||||
it('provides randomOk() which fails always, so it can show the succeeds message', () => {
|
|
||||||
expect(randomOk('foo')).to.be.false;
|
|
||||||
expect(randomOkValidator()[0]('foo').randomOk).to.be.false;
|
|
||||||
});
|
|
||||||
it('provides defaultOk() which fails always, so it can show the succeeds message', () => {
|
|
||||||
expect(defaultOk('foo')).to.be.false;
|
|
||||||
expect(defaultOkValidator()[0]('foo').defaultOk).to.be.false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue