Skip to content

Commit 713429a

Browse files
feat: adds display options configurability to the success validation
1 parent 9266a78 commit 713429a

File tree

7 files changed

+173
-36
lines changed

7 files changed

+173
-36
lines changed

.changeset/serious-toys-hug.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@lion/ui': minor
3+
---
4+
5+
adds configuration options to the success message validation

docs/fundamentals/systems/form/validate.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,10 +684,26 @@ export const validationTypes = () => {
684684
};
685685
```
686686

687+
### Success
688+
687689
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).
688690

689691
So, an error validator going from invalid (true) state to invalid(false) state, will trigger the success validator.
690692

693+
By default the success message will disappear, both the duration and the removal can be configured by passing `displayOptions` to the validator configuration:
694+
695+
The `duration` property overwrites the default 3000ms timer.
696+
697+
```js
698+
new DefaultSuccess({}, { visibilityDuration: 4000 });
699+
```
700+
701+
`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.
702+
703+
```js
704+
new DefaultSuccess({}, visibilityDuration: Infinity });
705+
```
706+
691707
## Asynchronous validation
692708

693709
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 = () => {
873889
874890
## Multiple field validation
875891
876-
When validation is dependent on muliple fields, two approaches can be considered:
892+
When validation is dependent on multiple fields, two approaches can be considered:
877893
878894
- Fieldset validation
879895
- Validators with knowledge about context

packages/ui/components/form-core/src/validate/LionValidationFeedback.js

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ export class LionValidationFeedback extends LocalizeMixin(LitElement) {
4848
];
4949
}
5050

51+
constructor() {
52+
super();
53+
/**
54+
* @type {FeedbackMessage[] | undefined}
55+
*/
56+
this.feedbackData = undefined;
57+
}
58+
5159
/**
5260
* @overridable
5361
* @param {Object} opts
@@ -69,16 +77,6 @@ export class LionValidationFeedback extends LocalizeMixin(LitElement) {
6977
if (this.feedbackData && this.feedbackData[0]) {
7078
this.setAttribute('type', this.feedbackData[0].type);
7179
this.currentType = this.feedbackData[0].type;
72-
window.clearTimeout(this.removeMessage);
73-
// TODO: this logic should be in ValidateMixin, so that [show-feedback-for] is in sync,
74-
// plus duration should be configurable
75-
if (this.currentType === 'success') {
76-
this.removeMessage = window.setTimeout(() => {
77-
this.removeAttribute('type');
78-
/** @type {FeedbackMessage[]} */
79-
this.feedbackData = [];
80-
}, 3000);
81-
}
8280
} else if (this.currentType !== 'success') {
8381
this.removeAttribute('type');
8482
}

packages/ui/components/form-core/src/validate/ValidateMixin.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ export const ValidateMixinImplementation = superclass =>
590590
return [];
591591
}
592592

593-
// If empty, do not show the ResulValidation message (e.g. Correct!)
593+
// If empty, do not show the ResultValidation message (e.g. Correct!)
594594
if (this._isEmpty(this.modelValue)) {
595595
this.__prevShownValidationResult = [];
596596
return [];
@@ -619,7 +619,7 @@ export const ValidateMixinImplementation = superclass =>
619619
* - on sync validation
620620
* - on async validation (can depend on server response)
621621
*
622-
* This method inishes a pass by adding the properties to the instance:
622+
* This method finishes a pass by adding the properties to the instance:
623623
* - validationStates
624624
* - hasFeedbackFor
625625
*
@@ -763,6 +763,7 @@ export const ValidateMixinImplementation = superclass =>
763763
* @property {string} type will be 'error' for messages from default Validators. Could be
764764
* 'warning', 'info' etc. for Validators with custom types. Needed as a directive for
765765
* feedbackNode how to render a message of a certain type
766+
* @property {number} visibilityDuration duration in ms for how long the message should be shown
766767
* @property {Validator} [validator] when the message is directly coupled to a Validator
767768
* (in most cases), this property is filled. When a message is not coupled to a Validator
768769
* (in case of success feedback which is based on a diff or current and previous validation
@@ -788,7 +789,12 @@ export const ValidateMixinImplementation = superclass =>
788789
fieldName,
789790
outcome,
790791
});
791-
return { message, type: validator.type, validator };
792+
return {
793+
message,
794+
type: validator.type,
795+
validator,
796+
visibilityDuration: validator.config?.visibilityDuration || 3000,
797+
};
792798
}),
793799
);
794800
}
@@ -809,6 +815,8 @@ export const ValidateMixinImplementation = superclass =>
809815
* @protected
810816
*/
811817
_updateFeedbackComponent() {
818+
window.clearTimeout(this.removeMessage);
819+
812820
const { _feedbackNode } = this;
813821
if (!_feedbackNode) {
814822
return;
@@ -840,6 +848,18 @@ export const ValidateMixinImplementation = superclass =>
840848

841849
const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
842850
_feedbackNode.feedbackData = messageMap || [];
851+
852+
if (
853+
messageMap?.[0] &&
854+
messageMap[0].type === 'success' &&
855+
messageMap[0].visibilityDuration !== Infinity
856+
) {
857+
this.removeMessage = window.setTimeout(() => {
858+
_feedbackNode.removeAttribute('type');
859+
/** @type {FeedbackMessage[]} */
860+
_feedbackNode.feedbackData = [];
861+
}, messageMap[0].visibilityDuration);
862+
}
843863
});
844864
} else {
845865
this.__feedbackQueue.add(async () => {

packages/ui/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,125 @@ export function runValidateMixinFeedbackPart() {
349349
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Nachricht für MinLength');
350350
});
351351

352+
it('shows success message and clears after 3s', async () => {
353+
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
354+
static get validationTypes() {
355+
return ['error', 'success'];
356+
}
357+
}
358+
const clock = sinon.useFakeTimers();
359+
const elTagString = defineCE(ValidateElementCustomTypes);
360+
const elTag = unsafeStatic(elTagString);
361+
const el = /** @type {ValidateElementCustomTypes} */ (
362+
await fixture(html`
363+
<${elTag}
364+
.submitted=${true}
365+
.validators=${[
366+
new MinLength(3),
367+
new DefaultSuccess(null, { getMessage: () => 'This is a success message' }),
368+
]}
369+
>${lightDom}</${elTag}>
370+
`)
371+
);
372+
const { _feedbackNode } = getFormControlMembers(el);
373+
374+
el.modelValue = 'a';
375+
await el.updateComplete;
376+
await el.feedbackComplete;
377+
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
378+
379+
el.modelValue = 'abcd';
380+
await el.updateComplete;
381+
await el.feedbackComplete;
382+
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
383+
clock.tick(2900);
384+
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
385+
clock.tick(200);
386+
expect(_feedbackNode.feedbackData).to.be.empty;
387+
});
388+
389+
it('shows success message and clears after configured time', async () => {
390+
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
391+
static get validationTypes() {
392+
return ['error', 'success'];
393+
}
394+
}
395+
const clock = sinon.useFakeTimers();
396+
const elTagString = defineCE(ValidateElementCustomTypes);
397+
const elTag = unsafeStatic(elTagString);
398+
const el = /** @type {ValidateElementCustomTypes} */ (
399+
await fixture(html`
400+
<${elTag}
401+
.submitted=${true}
402+
.validators=${[
403+
new MinLength(3),
404+
new DefaultSuccess(null, {
405+
getMessage: () => 'This is a success message',
406+
visibilityDuration: 6000,
407+
}),
408+
]}
409+
>${lightDom}</${elTag}>
410+
`)
411+
);
412+
const { _feedbackNode } = getFormControlMembers(el);
413+
414+
el.modelValue = 'a';
415+
await el.updateComplete;
416+
await el.feedbackComplete;
417+
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
418+
419+
el.modelValue = 'abcd';
420+
await el.updateComplete;
421+
await el.feedbackComplete;
422+
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
423+
clock.tick(5900);
424+
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
425+
clock.tick(200);
426+
expect(_feedbackNode.feedbackData).to.be.empty;
427+
});
428+
429+
it('shows success message and stays persistent', async () => {
430+
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
431+
static get validationTypes() {
432+
return ['error', 'success'];
433+
}
434+
}
435+
const clock = sinon.useFakeTimers();
436+
const elTagString = defineCE(ValidateElementCustomTypes);
437+
const elTag = unsafeStatic(elTagString);
438+
const el = /** @type {ValidateElementCustomTypes} */ (
439+
await fixture(html`
440+
<${elTag}
441+
.submitted=${true}
442+
.validators=${[
443+
new MinLength(3),
444+
new DefaultSuccess(null, {
445+
getMessage: () => 'This is a success message',
446+
visibilityDuration: Infinity,
447+
}),
448+
]}
449+
>${lightDom}</${elTag}>
450+
`)
451+
);
452+
const { _feedbackNode } = getFormControlMembers(el);
453+
454+
el.modelValue = 'a';
455+
await el.updateComplete;
456+
await el.feedbackComplete;
457+
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
458+
459+
el.modelValue = 'abcd';
460+
await el.updateComplete;
461+
await el.feedbackComplete;
462+
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
463+
clock.tick(2900);
464+
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
465+
clock.tick(200);
466+
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
467+
clock.tick(20000);
468+
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
469+
});
470+
352471
it('shows success message after fixing an error', async () => {
353472
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
354473
static get validationTypes() {

packages/ui/components/form-core/test/validate/lion-validation-feedback.test.js

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,6 @@ describe('lion-validation-feedback', () => {
3333
expect(el.getAttribute('type')).to.equal('warning');
3434
});
3535

36-
it('success message clears after 3s', async () => {
37-
const el = /** @type {LionValidationFeedback} */ (
38-
await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)
39-
);
40-
41-
const clock = sinon.useFakeTimers();
42-
43-
el.feedbackData = [{ message: 'hello', type: 'success', validator: new AlwaysValid() }];
44-
await el.updateComplete;
45-
expect(el.getAttribute('type')).to.equal('success');
46-
47-
clock.tick(2900);
48-
49-
expect(el.getAttribute('type')).to.equal('success');
50-
51-
clock.tick(200);
52-
53-
expect(el).to.not.have.attribute('type');
54-
55-
clock.restore();
56-
});
57-
5836
it('does not clear error messages', async () => {
5937
const el = /** @type {LionValidationFeedback} */ (
6038
await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)

packages/ui/components/form-core/types/validate/validate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type ValidatorConfig = {
3131
type?: ValidationType;
3232
node?: FormControlHost;
3333
fieldName?: string | Promise<string>;
34+
visibilityDuration?: number;
3435
};
3536

3637
/**

0 commit comments

Comments
 (0)