feat(form-core): allow developers to add .showFeedbackConditionFor

This commit is contained in:
Thijs Louisse 2021-04-08 17:00:19 +02:00
parent d4dcb7c1fb
commit 0e910e3f80
6 changed files with 104 additions and 7 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/form-core': minor
---
allow fine grained feedback visibility control via `.showFeedConditionFor(type, meta, currentCondition)` for Application Developers

View file

@ -3,6 +3,7 @@ import { FormControlMixin } from './FormControlMixin.js';
/** /**
* @typedef {import('../types/InteractionStateMixinTypes').InteractionStateMixin} InteractionStateMixin * @typedef {import('../types/InteractionStateMixinTypes').InteractionStateMixin} InteractionStateMixin
* @typedef {import('../types/InteractionStateMixinTypes').InteractionStates} InteractionStates
*/ */
/** /**
@ -190,9 +191,24 @@ const InteractionStateMixinImplementation = superclass =>
* When a user enters a field without altering the value(making it `dirty`), * When a user enters a field without altering the value(making it `dirty`),
* an error message shouldn't be shown either. * an error message shouldn't be shown either.
* @protected * @protected
* @param {string} type
* @param {InteractionStates} meta
*/ */
_showFeedbackConditionFor() { // eslint-disable-next-line class-methods-use-this, no-unused-vars
return (this.touched && this.dirty) || this.prefilled || this.submitted; _showFeedbackConditionFor(type, meta) {
const { touched, dirty, prefilled, submitted } = meta;
return (touched && dirty) || prefilled || submitted;
}
get _feedbackConditionMeta() {
return {
...super._feedbackConditionMeta,
submitted: this.submitted,
touched: this.touched,
dirty: this.dirty,
filled: this.filled,
prefilled: this.prefilled,
};
} }
}; };

View file

@ -628,15 +628,53 @@ export const ValidateMixinImplementation = superclass =>
} }
/** /**
* The default showFeedbackConditionFor condition that will be used when the
* showFeedbackConditionFor is not overridden.
* Show the validity feedback when returning true, don't show when false * Show the validity feedback when returning true, don't show when false
* @param {string} type * @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom
* Validator type
* @param {object} meta meta info (interaction states etc)
* @protected * @protected
*/ */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
_showFeedbackConditionFor(type) { _showFeedbackConditionFor(type, meta) {
return true; return true;
} }
/**
* Allows super classes to add meta info for showFeedbackConditionFor
* @configurable
*/
get _feedbackConditionMeta() {
return {};
}
/**
* Allows the end user to specify when a feedback message should be shown
* @example
* showFeedbackConditionFor(type, defaultCondition) {
* if (type === 'info') {
* return true;
* }
* return defaultCondition(type);
* }
* @overridable
* @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom
* Validator type
* @param {object} meta meta info (interaction states etc)
* @param {((type: string, meta: object) => boolean)} currentCondition this is the _showFeedbackConditionFor
* that can be used if a developer wants to override for a certain type, but wants to fallback
* for other types
* @returns {boolean}
*/
showFeedbackConditionFor(
type,
meta = this._feedbackConditionMeta,
currentCondition = this._showFeedbackConditionFor.bind(this),
) {
return currentCondition(type, meta);
}
/** /**
* @param {string} type * @param {string} type
* @protected * @protected
@ -677,7 +715,7 @@ export const ValidateMixinImplementation = superclass =>
// Necessary typecast because types aren't smart enough to understand that we filter out undefined // Necessary typecast because types aren't smart enough to understand that we filter out undefined
const newShouldShowFeedbackFor = /** @type {string[]} */ (ctor.validationTypes const newShouldShowFeedbackFor = /** @type {string[]} */ (ctor.validationTypes
.map(type => (this._showFeedbackConditionFor(type) ? type : undefined)) .map(type => (this.showFeedbackConditionFor(type) ? type : undefined))
.filter(_ => !!_)); .filter(_ => !!_));
if (JSON.stringify(this.shouldShowFeedbackFor) !== JSON.stringify(newShouldShowFeedbackFor)) { if (JSON.stringify(this.shouldShowFeedbackFor) !== JSON.stringify(newShouldShowFeedbackFor)) {
@ -700,7 +738,7 @@ export const ValidateMixinImplementation = superclass =>
const types = ctor.validationTypes; const types = ctor.validationTypes;
// Sort all validators based on the type provided. // Sort all validators based on the type provided.
const res = validationResult const res = validationResult
.filter(v => this._showFeedbackConditionFor(v.type)) .filter(v => this.showFeedbackConditionFor(v.type))
.sort((a, b) => types.indexOf(a.type) - types.indexOf(b.type)); .sort((a, b) => types.indexOf(a.type) - types.indexOf(b.type));
return res.slice(0, this._visibleMessagesAmount); return res.slice(0, this._visibleMessagesAmount);
} }

View file

@ -841,6 +841,26 @@ export function runValidateMixinSuite(customConfig) {
expect(el.validationStates.error).to.not.eql({}); expect(el.validationStates.error).to.not.eql({});
}); });
it('can be configured to change visibility conditions per type', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
<${tag}
.validators="${[new Required({}, { type: 'error' })]}"
.showFeedbackConditionFor="${(
/** @type {string} */ type,
/** @type {object} */ meta,
/** @type {(type: string) => any} */ defaultCondition,
) => {
if (type === 'error') {
return true;
}
return defaultCondition(type);
}}"
>${lightDom}</${tag}>
`));
expect(el.showsFeedbackFor).to.eql(['error']);
});
describe('Events', () => { describe('Events', () => {
it('fires "showsFeedbackForChanged" event async after feedbackData got synced to feedbackElement', async () => { it('fires "showsFeedbackForChanged" event async after feedbackData got synced to feedbackElement', async () => {
const spy = sinon.spy(); const spy = sinon.spy();

View file

@ -2,6 +2,17 @@ import { Constructor } from '@open-wc/dedupe-mixin';
import { LitElement } from '@lion/core'; import { LitElement } from '@lion/core';
import { FormControlHost } from './FormControlMixinTypes'; import { FormControlHost } from './FormControlMixinTypes';
/**
* A set of meta info about a FormControl that helps in the context of determining validation
* feedback visibility
*/
type InteractionStates = {
submitted: boolean;
touched: boolean;
dirty: boolean;
filled: boolean;
prefilled: boolean;
};
export declare class InteractionStateHost { export declare class InteractionStateHost {
prefilled: boolean; prefilled: boolean;
filled: boolean; filled: boolean;
@ -20,6 +31,12 @@ export declare class InteractionStateHost {
_iStateOnValueChange(): void; _iStateOnValueChange(): void;
_onTouchedChanged(): void; _onTouchedChanged(): void;
_onDirtyChanged(): void; _onDirtyChanged(): void;
showFeedbackConditionFor(
type: string,
meta: InteractionStates,
currentCondition: Function,
): boolean;
} }
export declare function InteractionStateImplementation<T extends Constructor<LitElement>>( export declare function InteractionStateImplementation<T extends Constructor<LitElement>>(

View file

@ -66,7 +66,8 @@ export declare class ValidateHost {
__isEmpty(v: unknown): boolean; __isEmpty(v: unknown): boolean;
__getFeedbackMessages(validators: Validator[]): Promise<FeedbackMessage[]>; __getFeedbackMessages(validators: Validator[]): Promise<FeedbackMessage[]>;
_updateFeedbackComponent(): void; _updateFeedbackComponent(): void;
_showFeedbackConditionFor(type: string): boolean; _showFeedbackConditionFor(type: string, meta: object): boolean;
showFeedbackConditionFor(type: string, meta: object, currentCondition: Function): boolean;
_hasFeedbackVisibleFor(type: string): boolean; _hasFeedbackVisibleFor(type: string): boolean;
_updateShouldShowFeedbackFor(): void; _updateShouldShowFeedbackFor(): void;
_prioritizeAndFilterFeedback(opts: { validationResult: Validator[] }): Validator[]; _prioritizeAndFilterFeedback(opts: { validationResult: Validator[] }): Validator[];