feat: adds display options configurability to the success validation
This commit is contained in:
parent
9266a786f2
commit
713429ac80
7 changed files with 173 additions and 36 deletions
5
.changeset/serious-toys-hug.md
Normal file
5
.changeset/serious-toys-hug.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
adds configuration options to the success message validation
|
||||||
|
|
@ -684,10 +684,26 @@ export const validationTypes = () => {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
Success validators work a bit differently. Their success state is defined by the lack of a previously existing erroneous state (which can be an error or warning state).
|
Success validators work a bit differently. Their success state is defined by the lack of a previously existing erroneous state (which can be an error or warning state).
|
||||||
|
|
||||||
So, an error validator going from invalid (true) state to invalid(false) state, will trigger the success validator.
|
So, an error validator going from invalid (true) state to invalid(false) state, will trigger the success validator.
|
||||||
|
|
||||||
|
By default the success message will disappear, both the duration and the removal can be configured by passing `displayOptions` to the validator configuration:
|
||||||
|
|
||||||
|
The `duration` property overwrites the default 3000ms timer.
|
||||||
|
|
||||||
|
```js
|
||||||
|
new DefaultSuccess({}, { visibilityDuration: 4000 });
|
||||||
|
```
|
||||||
|
|
||||||
|
`Infinity` can be added to the `duration` property to allow the success message to stay on screen, like the behaviour of the other validation types' messages.
|
||||||
|
|
||||||
|
```js
|
||||||
|
new DefaultSuccess({}, visibilityDuration: Infinity });
|
||||||
|
```
|
||||||
|
|
||||||
## Asynchronous validation
|
## Asynchronous validation
|
||||||
|
|
||||||
By default, all validations are run synchronously. However, for instance when validation can only take place on server level, asynchronous validation will be needed.
|
By default, all validations are run synchronously. However, for instance when validation can only take place on server level, asynchronous validation will be needed.
|
||||||
|
|
@ -873,7 +889,7 @@ export const backendValidation = () => {
|
||||||
|
|
||||||
## Multiple field validation
|
## Multiple field validation
|
||||||
|
|
||||||
When validation is dependent on muliple fields, two approaches can be considered:
|
When validation is dependent on multiple fields, two approaches can be considered:
|
||||||
|
|
||||||
- Fieldset validation
|
- Fieldset validation
|
||||||
- Validators with knowledge about context
|
- Validators with knowledge about context
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,14 @@ export class LionValidationFeedback extends LocalizeMixin(LitElement) {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
/**
|
||||||
|
* @type {FeedbackMessage[] | undefined}
|
||||||
|
*/
|
||||||
|
this.feedbackData = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @overridable
|
* @overridable
|
||||||
* @param {Object} opts
|
* @param {Object} opts
|
||||||
|
|
@ -69,16 +77,6 @@ export class LionValidationFeedback extends LocalizeMixin(LitElement) {
|
||||||
if (this.feedbackData && this.feedbackData[0]) {
|
if (this.feedbackData && this.feedbackData[0]) {
|
||||||
this.setAttribute('type', this.feedbackData[0].type);
|
this.setAttribute('type', this.feedbackData[0].type);
|
||||||
this.currentType = this.feedbackData[0].type;
|
this.currentType = this.feedbackData[0].type;
|
||||||
window.clearTimeout(this.removeMessage);
|
|
||||||
// TODO: this logic should be in ValidateMixin, so that [show-feedback-for] is in sync,
|
|
||||||
// plus duration should be configurable
|
|
||||||
if (this.currentType === 'success') {
|
|
||||||
this.removeMessage = window.setTimeout(() => {
|
|
||||||
this.removeAttribute('type');
|
|
||||||
/** @type {FeedbackMessage[]} */
|
|
||||||
this.feedbackData = [];
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
} else if (this.currentType !== 'success') {
|
} else if (this.currentType !== 'success') {
|
||||||
this.removeAttribute('type');
|
this.removeAttribute('type');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -590,7 +590,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If empty, do not show the ResulValidation message (e.g. Correct!)
|
// If empty, do not show the ResultValidation message (e.g. Correct!)
|
||||||
if (this._isEmpty(this.modelValue)) {
|
if (this._isEmpty(this.modelValue)) {
|
||||||
this.__prevShownValidationResult = [];
|
this.__prevShownValidationResult = [];
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -619,7 +619,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
* - on sync validation
|
* - on sync validation
|
||||||
* - on async validation (can depend on server response)
|
* - on async validation (can depend on server response)
|
||||||
*
|
*
|
||||||
* This method inishes a pass by adding the properties to the instance:
|
* This method finishes a pass by adding the properties to the instance:
|
||||||
* - validationStates
|
* - validationStates
|
||||||
* - hasFeedbackFor
|
* - hasFeedbackFor
|
||||||
*
|
*
|
||||||
|
|
@ -763,6 +763,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
* @property {string} type will be 'error' for messages from default Validators. Could be
|
* @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
|
* 'warning', 'info' etc. for Validators with custom types. Needed as a directive for
|
||||||
* feedbackNode how to render a message of a certain type
|
* feedbackNode how to render a message of a certain type
|
||||||
|
* @property {number} visibilityDuration duration in ms for how long the message should be shown
|
||||||
* @property {Validator} [validator] when the message is directly coupled to a Validator
|
* @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 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
|
* (in case of success feedback which is based on a diff or current and previous validation
|
||||||
|
|
@ -788,7 +789,12 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
fieldName,
|
fieldName,
|
||||||
outcome,
|
outcome,
|
||||||
});
|
});
|
||||||
return { message, type: validator.type, validator };
|
return {
|
||||||
|
message,
|
||||||
|
type: validator.type,
|
||||||
|
validator,
|
||||||
|
visibilityDuration: validator.config?.visibilityDuration || 3000,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -809,6 +815,8 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_updateFeedbackComponent() {
|
_updateFeedbackComponent() {
|
||||||
|
window.clearTimeout(this.removeMessage);
|
||||||
|
|
||||||
const { _feedbackNode } = this;
|
const { _feedbackNode } = this;
|
||||||
if (!_feedbackNode) {
|
if (!_feedbackNode) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -840,6 +848,18 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
|
|
||||||
const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
|
const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
|
||||||
_feedbackNode.feedbackData = messageMap || [];
|
_feedbackNode.feedbackData = messageMap || [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
messageMap?.[0] &&
|
||||||
|
messageMap[0].type === 'success' &&
|
||||||
|
messageMap[0].visibilityDuration !== Infinity
|
||||||
|
) {
|
||||||
|
this.removeMessage = window.setTimeout(() => {
|
||||||
|
_feedbackNode.removeAttribute('type');
|
||||||
|
/** @type {FeedbackMessage[]} */
|
||||||
|
_feedbackNode.feedbackData = [];
|
||||||
|
}, messageMap[0].visibilityDuration);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.__feedbackQueue.add(async () => {
|
this.__feedbackQueue.add(async () => {
|
||||||
|
|
|
||||||
|
|
@ -349,6 +349,125 @@ export function runValidateMixinFeedbackPart() {
|
||||||
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Nachricht für MinLength');
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Nachricht für MinLength');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows success message and clears after 3s', async () => {
|
||||||
|
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||||
|
static get validationTypes() {
|
||||||
|
return ['error', 'success'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const clock = sinon.useFakeTimers();
|
||||||
|
const elTagString = defineCE(ValidateElementCustomTypes);
|
||||||
|
const elTag = unsafeStatic(elTagString);
|
||||||
|
const el = /** @type {ValidateElementCustomTypes} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${elTag}
|
||||||
|
.submitted=${true}
|
||||||
|
.validators=${[
|
||||||
|
new MinLength(3),
|
||||||
|
new DefaultSuccess(null, { getMessage: () => 'This is a success message' }),
|
||||||
|
]}
|
||||||
|
>${lightDom}</${elTag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
|
el.modelValue = 'a';
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
|
||||||
|
|
||||||
|
el.modelValue = 'abcd';
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
|
||||||
|
clock.tick(2900);
|
||||||
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
|
||||||
|
clock.tick(200);
|
||||||
|
expect(_feedbackNode.feedbackData).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows success message and clears after configured time', async () => {
|
||||||
|
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||||
|
static get validationTypes() {
|
||||||
|
return ['error', 'success'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const clock = sinon.useFakeTimers();
|
||||||
|
const elTagString = defineCE(ValidateElementCustomTypes);
|
||||||
|
const elTag = unsafeStatic(elTagString);
|
||||||
|
const el = /** @type {ValidateElementCustomTypes} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${elTag}
|
||||||
|
.submitted=${true}
|
||||||
|
.validators=${[
|
||||||
|
new MinLength(3),
|
||||||
|
new DefaultSuccess(null, {
|
||||||
|
getMessage: () => 'This is a success message',
|
||||||
|
visibilityDuration: 6000,
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>${lightDom}</${elTag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
|
el.modelValue = 'a';
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
|
||||||
|
|
||||||
|
el.modelValue = 'abcd';
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
|
||||||
|
clock.tick(5900);
|
||||||
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
|
||||||
|
clock.tick(200);
|
||||||
|
expect(_feedbackNode.feedbackData).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows success message and stays persistent', async () => {
|
||||||
|
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||||
|
static get validationTypes() {
|
||||||
|
return ['error', 'success'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const clock = sinon.useFakeTimers();
|
||||||
|
const elTagString = defineCE(ValidateElementCustomTypes);
|
||||||
|
const elTag = unsafeStatic(elTagString);
|
||||||
|
const el = /** @type {ValidateElementCustomTypes} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${elTag}
|
||||||
|
.submitted=${true}
|
||||||
|
.validators=${[
|
||||||
|
new MinLength(3),
|
||||||
|
new DefaultSuccess(null, {
|
||||||
|
getMessage: () => 'This is a success message',
|
||||||
|
visibilityDuration: Infinity,
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>${lightDom}</${elTag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
|
el.modelValue = 'a';
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
|
||||||
|
|
||||||
|
el.modelValue = 'abcd';
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
|
||||||
|
clock.tick(2900);
|
||||||
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
|
||||||
|
clock.tick(200);
|
||||||
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
|
||||||
|
clock.tick(20000);
|
||||||
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
|
||||||
|
});
|
||||||
|
|
||||||
it('shows success message after fixing an error', async () => {
|
it('shows success message after fixing an error', async () => {
|
||||||
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||||
static get validationTypes() {
|
static get validationTypes() {
|
||||||
|
|
|
||||||
|
|
@ -33,28 +33,6 @@ describe('lion-validation-feedback', () => {
|
||||||
expect(el.getAttribute('type')).to.equal('warning');
|
expect(el.getAttribute('type')).to.equal('warning');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('success message clears after 3s', async () => {
|
|
||||||
const el = /** @type {LionValidationFeedback} */ (
|
|
||||||
await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)
|
|
||||||
);
|
|
||||||
|
|
||||||
const clock = sinon.useFakeTimers();
|
|
||||||
|
|
||||||
el.feedbackData = [{ message: 'hello', type: 'success', validator: new AlwaysValid() }];
|
|
||||||
await el.updateComplete;
|
|
||||||
expect(el.getAttribute('type')).to.equal('success');
|
|
||||||
|
|
||||||
clock.tick(2900);
|
|
||||||
|
|
||||||
expect(el.getAttribute('type')).to.equal('success');
|
|
||||||
|
|
||||||
clock.tick(200);
|
|
||||||
|
|
||||||
expect(el).to.not.have.attribute('type');
|
|
||||||
|
|
||||||
clock.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not clear error messages', async () => {
|
it('does not clear error messages', async () => {
|
||||||
const el = /** @type {LionValidationFeedback} */ (
|
const el = /** @type {LionValidationFeedback} */ (
|
||||||
await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)
|
await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export type ValidatorConfig = {
|
||||||
type?: ValidationType;
|
type?: ValidationType;
|
||||||
node?: FormControlHost;
|
node?: FormControlHost;
|
||||||
fieldName?: string | Promise<string>;
|
fieldName?: string | Promise<string>;
|
||||||
|
visibilityDuration?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue