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).
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- Validators with knowledge about context
|
||||
|
|
|
|||
|
|
@ -48,6 +48,14 @@ export class LionValidationFeedback extends LocalizeMixin(LitElement) {
|
|||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
/**
|
||||
* @type {FeedbackMessage[] | undefined}
|
||||
*/
|
||||
this.feedbackData = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @overridable
|
||||
* @param {Object} opts
|
||||
|
|
@ -69,16 +77,6 @@ export class LionValidationFeedback extends LocalizeMixin(LitElement) {
|
|||
if (this.feedbackData && this.feedbackData[0]) {
|
||||
this.setAttribute('type', 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') {
|
||||
this.removeAttribute('type');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -590,7 +590,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
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)) {
|
||||
this.__prevShownValidationResult = [];
|
||||
return [];
|
||||
|
|
@ -619,7 +619,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
* - on sync validation
|
||||
* - 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
|
||||
* - hasFeedbackFor
|
||||
*
|
||||
|
|
@ -763,6 +763,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
* @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 {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
|
||||
* (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
|
||||
|
|
@ -788,7 +789,12 @@ export const ValidateMixinImplementation = superclass =>
|
|||
fieldName,
|
||||
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
|
||||
*/
|
||||
_updateFeedbackComponent() {
|
||||
window.clearTimeout(this.removeMessage);
|
||||
|
||||
const { _feedbackNode } = this;
|
||||
if (!_feedbackNode) {
|
||||
return;
|
||||
|
|
@ -840,6 +848,18 @@ export const ValidateMixinImplementation = superclass =>
|
|||
|
||||
const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
|
||||
_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 {
|
||||
this.__feedbackQueue.add(async () => {
|
||||
|
|
|
|||
|
|
@ -349,6 +349,125 @@ export function runValidateMixinFeedbackPart() {
|
|||
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 () => {
|
||||
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||
static get validationTypes() {
|
||||
|
|
|
|||
|
|
@ -33,28 +33,6 @@ describe('lion-validation-feedback', () => {
|
|||
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 () => {
|
||||
const el = /** @type {LionValidationFeedback} */ (
|
||||
await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export type ValidatorConfig = {
|
|||
type?: ValidationType;
|
||||
node?: FormControlHost;
|
||||
fieldName?: string | Promise<string>;
|
||||
visibilityDuration?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue