179 lines
5.8 KiB
JavaScript
179 lines
5.8 KiB
JavaScript
import { dedupeMixin } from '@lion/core';
|
|
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
|
import { Unparseable } from '@lion/validate';
|
|
|
|
/**
|
|
* @desc `InteractionStateMixin` adds meta information about touched and dirty states, that can
|
|
* be read by other form components (ing-uic-input-error for instance, uses the touched state
|
|
* to determine whether an error message needs to be shown).
|
|
* Interaction states will be set when a user:
|
|
* - leaves a form field(blur) -> 'touched' will be set to true. 'prefilled' when a
|
|
* field is left non-empty
|
|
* - on keyup (actually, on the model-value-changed event) -> 'dirty' will be set to true
|
|
* @param {HTMLElement} superclass
|
|
*/
|
|
export const InteractionStateMixin = dedupeMixin(
|
|
superclass =>
|
|
// eslint-disable-next-line no-unused-vars, no-shadow
|
|
class InteractionStateMixin extends ObserverMixin(superclass) {
|
|
static get properties() {
|
|
return {
|
|
/**
|
|
* True when user has focused and left(blurred) the field.
|
|
*/
|
|
touched: {
|
|
type: Boolean,
|
|
reflect: true,
|
|
},
|
|
|
|
/**
|
|
* True when user has typed in something in the input field.
|
|
*/
|
|
dirty: {
|
|
type: Boolean,
|
|
reflect: true,
|
|
},
|
|
|
|
/**
|
|
* True when user has left non-empty field or input is prefilled.
|
|
* The name must be seen from the point of view of the input field:
|
|
* once the user enters the input field, the value is non-empty.
|
|
*/
|
|
prefilled: {
|
|
type: Boolean,
|
|
},
|
|
};
|
|
}
|
|
|
|
static get syncObservers() {
|
|
return {
|
|
...super.syncObservers,
|
|
_onTouchedChanged: ['touched'],
|
|
_onDirtyChanged: ['dirty'],
|
|
};
|
|
}
|
|
|
|
static _isPrefilled(modelValue) {
|
|
let value = modelValue;
|
|
if (modelValue instanceof Unparseable) {
|
|
value = modelValue.viewValue;
|
|
}
|
|
// Checks for empty platform types: Objects, Arrays, Dates
|
|
if (typeof value === 'object' && value !== null && !(value instanceof Date)) {
|
|
return !!Object.keys(value).length;
|
|
}
|
|
// eslint-disable-next-line no-mixed-operators
|
|
// Checks for empty platform types: Numbers, Booleans
|
|
const isNumberValue = typeof value === 'number' && (value === 0 || Number.isNaN(value));
|
|
const isBooleanValue = typeof value === 'boolean' && value === false;
|
|
|
|
return !!value || isNumberValue || isBooleanValue;
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.touched = false;
|
|
this.dirty = false;
|
|
this.prefilled = false;
|
|
this._leaveEvent = 'blur';
|
|
this._valueChangedEvent = 'model-value-changed';
|
|
|
|
this._iStateOnLeave = this._iStateOnLeave.bind(this);
|
|
this._iStateOnValueChange = this._iStateOnValueChange.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Register event handlers and validate prefilled inputs
|
|
*/
|
|
connectedCallback() {
|
|
if (super.connectedCallback) {
|
|
super.connectedCallback();
|
|
}
|
|
this.addEventListener(this._leaveEvent, this._iStateOnLeave);
|
|
this.addEventListener(this._valueChangedEvent, this._iStateOnValueChange);
|
|
this.initInteractionState();
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (super.disconnectedCallback) {
|
|
super.disconnectedCallback();
|
|
}
|
|
this.removeEventListener(this._leaveEvent, this._iStateOnLeave);
|
|
this.removeEventListener(this._valueChangedEvent, this._iStateOnValueChange);
|
|
}
|
|
|
|
updated(changedProperties) {
|
|
super.updated(changedProperties);
|
|
// classes are added only for backward compatibility - they are deprecated
|
|
if (changedProperties.has('touched')) {
|
|
this.classList[this.touched ? 'add' : 'remove']('state-touched');
|
|
}
|
|
if (changedProperties.has('dirty')) {
|
|
this.classList[this.dirty ? 'add' : 'remove']('state-dirty');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Evaluations performed on connectedCallback. Since some components can be out of sync
|
|
* (due to interdependence on light children that can only be processed
|
|
* after connectedCallback and affect the initial value).
|
|
* This method is exposed, so it can be called after they are initialized themselves.
|
|
* Since this method will be called twice in last mentioned scenario, it must stay idempotent.
|
|
*/
|
|
initInteractionState() {
|
|
if (this.constructor._isPrefilled(this.modelValue)) {
|
|
this.prefilled = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets touched value to true
|
|
* Reevaluates prefilled state.
|
|
* When false, on next interaction, user will start with a clean state.
|
|
* @private
|
|
*/
|
|
_iStateOnLeave() {
|
|
this.touched = true;
|
|
this.prefilled = this.constructor._isPrefilled(this.modelValue);
|
|
}
|
|
|
|
/**
|
|
* Sets dirty value and validates when already touched or invalid
|
|
* @private
|
|
*/
|
|
_iStateOnValueChange() {
|
|
this.dirty = true;
|
|
}
|
|
|
|
/**
|
|
* Resets touched and dirty, and recomputes prefilled
|
|
*/
|
|
resetInteractionState() {
|
|
this.touched = false;
|
|
this.dirty = false;
|
|
this.prefilled = this.constructor._isPrefilled(this.modelValue);
|
|
}
|
|
|
|
_onTouchedChanged() {
|
|
this.dispatchEvent(new CustomEvent('touched-changed', { bubbles: true, composed: true }));
|
|
}
|
|
|
|
_onDirtyChanged() {
|
|
this.dispatchEvent(new CustomEvent('dirty-changed', { bubbles: true, composed: true }));
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
get leaveEvent() {
|
|
return this._leaveEvent;
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
set leaveEvent(eventName) {
|
|
this._leaveEvent = eventName;
|
|
}
|
|
},
|
|
);
|