commit
7661ae665e
4 changed files with 170 additions and 134 deletions
|
|
@ -4,29 +4,32 @@ It is designed to work in conjunction with `LionField`.
|
||||||
|
|
||||||
## Concepts of different values
|
## Concepts of different values
|
||||||
|
|
||||||
### modelValue
|
### model value
|
||||||
The model value is the result of the parser function.
|
The model value is the result of the parser function. It will be stored as `.modelValue`
|
||||||
It should be considered as the internal value used for validation and reasoning/logic.
|
and should be considered the internal value used for validation and reasoning/logic.
|
||||||
The model value is 'ready for consumption' by the outside world (think of a Date object
|
The model value is 'ready for consumption' by the outside world (think of a Date object
|
||||||
or a float). The modelValue can(and is recommended to) be used as both input value and
|
or a float). It can(and is recommended to) be used as both input value and
|
||||||
output value of the `<lion-field>`
|
output value of the `LionField`.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- For a date input: a String '20/01/1999' will be converted to new Date('1999/01/20')
|
- For a date input: a String '20/01/1999' will be converted to `new Date('1999/01/20')`
|
||||||
- For a number input: a formatted String '1.234,56' will be converted to a Number: 1234.56
|
- For a number input: a formatted String '1.234,56' will be converted to a Number: `1234.56`
|
||||||
|
|
||||||
### formattedValue
|
### view value
|
||||||
The view value is the result of the formatter function (when available).
|
The view value is the result of the formatter function.
|
||||||
The result will be stored in the native inputElement (usually an input[type=text]).
|
It will be stored as `.formattedValue` and synchronized to `.value` (a viewValue setter that
|
||||||
|
allows to synchronize to `.inputElement`).
|
||||||
|
Synchronization happens conditionally and is (by default) the result of a blur. Other conditions
|
||||||
|
(like error state/validity and whether the a model value was set programatically) also play a role.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- For a date input, this would be '20/01/1999' (dependent on locale).
|
- For a date input, this would be '20/01/1999' (dependent on locale).
|
||||||
- For a number input, this could be '1,234.56' (a String representation of modelValue
|
- For a number input, this could be '1,234.56' (a String representation of modelValue
|
||||||
1234.56)
|
1234.56)
|
||||||
|
|
||||||
### serializedValue
|
### serialized value
|
||||||
The serialized version of the model value.
|
This is the serialized version of the model value.
|
||||||
This value exists for maximal compatibility with the platform API.
|
It exists for maximal compatibility with the platform API.
|
||||||
The serialized value can be an interface in context where data binding is not supported
|
The serialized value can be an interface in context where data binding is not supported
|
||||||
and a serialized string needs to be set.
|
and a serialized string needs to be set.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,48 @@ import { dedupeMixin } from '@lion/core';
|
||||||
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
||||||
import { Unparseable } from '@lion/validate';
|
import { Unparseable } from '@lion/validate';
|
||||||
|
|
||||||
|
// For a future breaking release:
|
||||||
|
// - do not allow the private `.formattedValue` as property that can be set to
|
||||||
|
// trigger a computation loop.
|
||||||
|
// - do not fire events for those private and protected concepts
|
||||||
|
// - simplify _calculateValues: recursive trigger lock can be omitted, since need for connecting
|
||||||
|
// the loop via sync observers is not needed anymore.
|
||||||
|
// - consider `formatOn` as an overridable function, by default something like:
|
||||||
|
// `(!__isHandlingUserInput || !errorState) && !focused`
|
||||||
|
// This would allow for more advanced scenarios, like formatting an input whenever it becomes valid.
|
||||||
|
// This would make formattedValue as a concept obsolete, since for maximum flexibility, the
|
||||||
|
// formattedValue condition needs to be evaluated right before syncing back to the view
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc Designed to be applied on top of a LionField
|
* @desc Designed to be applied on top of a LionField.
|
||||||
|
* To understand all concepts within the Mixin, please consult the flow diagram in the
|
||||||
|
* documentation.
|
||||||
*
|
*
|
||||||
|
* ## Flows
|
||||||
* FormatMixin supports these two main flows:
|
* FormatMixin supports these two main flows:
|
||||||
* 1) Application Developer sets `.modelValue`:
|
* [1] Application Developer sets `.modelValue`:
|
||||||
* Flow: `.modelValue` -> `.formattedValue` -> `.inputElement.value`
|
* Flow: `.modelValue` (formatter) -> `.formattedValue` -> `.inputElement.value`
|
||||||
* -> `.serializedValue`
|
* (serializer) -> `.serializedValue`
|
||||||
* 2) End user interacts with field:
|
* [2] End user interacts with field:
|
||||||
* Flow: `@user-input-changed` -> `.modelValue` -> `.formattedValue` - (debounce till reflect condition (formatOn) is met) -> `.inputElement.value`
|
* Flow: `@user-input-changed` (parser) -> `.modelValue` (formatter) -> `.formattedValue` - (debounce till reflect condition (formatOn) is met) -> `.inputElement.value`
|
||||||
* -> `.serializedValue`
|
* (serializer) -> `.serializedValue`
|
||||||
*
|
*
|
||||||
* @mixinFunction
|
* For backwards compatibility with the platform, we also support `.value` as an api. In that case
|
||||||
|
* the flow will be like [2], without the debounce.
|
||||||
|
*
|
||||||
|
* ## Difference between value, viewValue and formattedValue
|
||||||
|
* A viewValue is a concept rather than a property. To be compatible with the platform api, the
|
||||||
|
* property for the concept of viewValue is thus called `.value`.
|
||||||
|
* When reading code and docs, one should be aware that the term viewValue is mostly used, but the
|
||||||
|
* terms can be used interchangeably.
|
||||||
|
* The `.formattedValue` should be seen as the 'scheduled' viewValue. It is computed realtime and
|
||||||
|
* stores the output of formatter. It will replace viewValue. once condition `formatOn` is met.
|
||||||
|
* Another difference is that formattedValue lives on `LionField`, whereas viewValue is shared
|
||||||
|
* across `LionField` and `.inputElement`.
|
||||||
|
*
|
||||||
|
* For restoring serialized values fetched from a server, we could consider one extra flow:
|
||||||
|
* [3] Application Developer sets `.serializedValue`:
|
||||||
|
* Flow: serializedValue (deserializer) -> `.modelValue` (formatter) -> `.formattedValue` -> `.inputElement.value`
|
||||||
*/
|
*/
|
||||||
export const FormatMixin = dedupeMixin(
|
export const FormatMixin = dedupeMixin(
|
||||||
superclass =>
|
superclass =>
|
||||||
|
|
@ -28,9 +58,9 @@ export const FormatMixin = dedupeMixin(
|
||||||
/**
|
/**
|
||||||
* The model value is the result of the parser function(when available).
|
* The model value is the result of the parser function(when available).
|
||||||
* It should be considered as the internal value used for validation and reasoning/logic.
|
* It should be considered as the internal value used for validation and reasoning/logic.
|
||||||
* The model value is 'ready for consumption' by the outside world (think of a Date object
|
* The model value is 'ready for consumption' by the outside world (think of a Date
|
||||||
* or a float). The modelValue can(and is recommended to) be used as both input value and
|
* object or a float). The modelValue can(and is recommended to) be used as both input
|
||||||
* output value of the <lion-field>
|
* value and output value of the `LionField`.
|
||||||
*
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
* - For a date input: a String '20/01/1999' will be converted to new Date('1999/01/20')
|
* - For a date input: a String '20/01/1999' will be converted to new Date('1999/01/20')
|
||||||
|
|
@ -49,6 +79,8 @@ export const FormatMixin = dedupeMixin(
|
||||||
* - For a date input, this would be '20/01/1999' (dependent on locale).
|
* - For a date input, this would be '20/01/1999' (dependent on locale).
|
||||||
* - For a number input, this could be '1,234.56' (a String representation of modelValue
|
* - For a number input, this could be '1,234.56' (a String representation of modelValue
|
||||||
* 1234.56)
|
* 1234.56)
|
||||||
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
formattedValue: {
|
formattedValue: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -67,6 +99,7 @@ export const FormatMixin = dedupeMixin(
|
||||||
*
|
*
|
||||||
* When no parser is available, the value is usually the same as the formattedValue
|
* When no parser is available, the value is usually the same as the formattedValue
|
||||||
* (being inputElement.value)
|
* (being inputElement.value)
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
serializedValue: {
|
serializedValue: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -99,11 +132,6 @@ export const FormatMixin = dedupeMixin(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* === Formatting and parsing ====
|
|
||||||
* To understand all concepts below, please consult the flow diagrams in the documentation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts formattedValue to modelValue
|
* Converts formattedValue to modelValue
|
||||||
* For instance, a localized date to a Date Object
|
* For instance, a localized date to a Date Object
|
||||||
|
|
@ -115,10 +143,11 @@ export const FormatMixin = dedupeMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts modelValue to formattedValue (formattedValue will be synced with <input>.value)
|
* Converts modelValue to formattedValue (formattedValue will be synced with
|
||||||
* For instance, a Date object to a localized date
|
* `.inputElement.value`)
|
||||||
* @param {Object} value - modelValue: can be an Object, Number, String depending on the input
|
* For instance, a Date object to a localized date.
|
||||||
* type(date, number, email etc)
|
* @param {Object} value - modelValue: can be an Object, Number, String depending on the
|
||||||
|
* input type(date, number, email etc)
|
||||||
* @returns {String} formattedValue
|
* @returns {String} formattedValue
|
||||||
*/
|
*/
|
||||||
formatter(v) {
|
formatter(v) {
|
||||||
|
|
@ -126,10 +155,10 @@ export const FormatMixin = dedupeMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts modelValue to serializedValue (<lion-field>.value).
|
* Converts `.modelValue` to `.serializedValue`
|
||||||
* For instance, a Date object to an iso formatted date string
|
* For instance, a Date object to an iso formatted date string
|
||||||
* @param {Object} value - modelValue: can be an Object, Number, String depending on the input
|
* @param {Object} value - modelValue: can be an Object, Number, String depending on the
|
||||||
* type(date, number, email etc)
|
* input type(date, number, email etc)
|
||||||
* @returns {String} serializedValue
|
* @returns {String} serializedValue
|
||||||
*/
|
*/
|
||||||
serializer(v) {
|
serializer(v) {
|
||||||
|
|
@ -137,10 +166,10 @@ export const FormatMixin = dedupeMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts <lion-field>.value to modelValue
|
* Converts `LionField.value` to `.modelValue`
|
||||||
* For instance, an iso formatted date string to a Date object
|
* For instance, an iso formatted date string to a Date object
|
||||||
* @param {Object} value - modelValue: can be an Object, Number, String depending on the input
|
* @param {Object} value - modelValue: can be an Object, Number, String depending on the
|
||||||
* type(date, number, email etc)
|
* input type(date, number, email etc)
|
||||||
* @returns {Object} modelValue
|
* @returns {Object} modelValue
|
||||||
*/
|
*/
|
||||||
deserializer(v) {
|
deserializer(v) {
|
||||||
|
|
@ -149,9 +178,8 @@ export const FormatMixin = dedupeMixin(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible for storing all representations(modelValue, serializedValue, formattedValue
|
* Responsible for storing all representations(modelValue, serializedValue, formattedValue
|
||||||
* and value) of the input value.
|
* and value) of the input value. Prevents infinite loops, so all value observers can be
|
||||||
* Prevents infinite loops, so all value observers can be treated like they will only be
|
* treated like they will only be called once, without indirectly calling other observers.
|
||||||
* called once, without indirectly calling other observers.
|
|
||||||
* (in fact, some are called twice, but the __preventRecursiveTrigger lock prevents the
|
* (in fact, some are called twice, but the __preventRecursiveTrigger lock prevents the
|
||||||
* second call from having effect).
|
* second call from having effect).
|
||||||
*
|
*
|
||||||
|
|
@ -189,7 +217,7 @@ export const FormatMixin = dedupeMixin(
|
||||||
// For backwards compatibility we return an empty string:
|
// For backwards compatibility we return an empty string:
|
||||||
// - it triggers validation for required validators (see ValidateMixin.validate())
|
// - it triggers validation for required validators (see ValidateMixin.validate())
|
||||||
// - it can be expected by 3rd parties (for instance unit tests)
|
// - it can be expected by 3rd parties (for instance unit tests)
|
||||||
// TODO: In a breaking refactor of the Validation System, this behaviot can be corrected.
|
// TODO: In a breaking refactor of the Validation System, this behavior can be corrected.
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,8 +225,8 @@ export const FormatMixin = dedupeMixin(
|
||||||
// inputElement.value was not available yet
|
// inputElement.value was not available yet
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
// This means there is nothing to find inside the view that can be of
|
// This means there is nothing to find inside the view that can be of
|
||||||
// interest to the Application Developer or needed to store for future form state
|
// interest to the Application Developer or needed to store for future
|
||||||
// retrieval.
|
// form state retrieval.
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,24 +243,28 @@ export const FormatMixin = dedupeMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
__callFormatter() {
|
__callFormatter() {
|
||||||
if (this.modelValue instanceof Unparseable) {
|
|
||||||
return this.modelValue.viewValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// - Why check for this.errorState?
|
// - Why check for this.errorState?
|
||||||
// We only want to format values that are considered valid. For best UX,
|
// We only want to format values that are considered valid. For best UX,
|
||||||
// we only 'reward' valid inputs.
|
// we only 'reward' valid inputs.
|
||||||
// - Why check for __isHandlingUserInput?
|
// - Why check for __isHandlingUserInput?
|
||||||
// Downwards sync is prevented whenever we are in a `@user-input-changed` flow.
|
// Downwards sync is prevented whenever we are in an `@user-input-changed` flow, [2].
|
||||||
// If we are in a 'imperatively set `.modelValue`' flow, we want to reflect back
|
// If we are in a 'imperatively set `.modelValue`' flow, [1], we want to reflect back
|
||||||
// the value, no matter what.
|
// the value, no matter what.
|
||||||
// This means, whenever we are in errorState, we and modelValue is set
|
// This means, whenever we are in errorState and modelValue is set
|
||||||
// imperatively, we DO want to format a value (it is the only way to get meaningful
|
// imperatively, we DO want to format a value (it is the only way to get meaningful
|
||||||
// input into `.inputElement` with modelValue as input)
|
// input into `.inputElement` with modelValue as input)
|
||||||
|
|
||||||
if (this.__isHandlingUserInput && this.errorState && this.inputElement) {
|
if (this.__isHandlingUserInput && this.errorState && this.inputElement) {
|
||||||
return this.inputElement ? this.value : undefined;
|
return this.inputElement ? this.value : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.modelValue instanceof Unparseable) {
|
||||||
|
// When the modelValue currently is unparseable, we need to sync back the supplied
|
||||||
|
// viewValue. In flow [2], this should not be needed.
|
||||||
|
// In flow [1] (we restore a previously stored modelValue) we should sync down, however.
|
||||||
|
return this.modelValue.viewValue;
|
||||||
|
}
|
||||||
|
|
||||||
return this.formatter(this.modelValue, this.formatOptions);
|
return this.formatter(this.modelValue, this.formatOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,19 +274,19 @@ export const FormatMixin = dedupeMixin(
|
||||||
this._dispatchModelValueChangedEvent(...args);
|
this._dispatchModelValueChangedEvent(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: investigate if this also can be solved by using 'hasChanged' on property accessor
|
|
||||||
// inside choiceInputs
|
|
||||||
/**
|
/**
|
||||||
* This is wrapped in a distinct method, so that parents can control when the changed event is
|
* This is wrapped in a distinct method, so that parents can control when the changed event
|
||||||
* fired. For instance: when modelValue is an object, a deep comparison is needed first
|
* is fired. For objects, a deep comparison might be needed.
|
||||||
*/
|
*/
|
||||||
_dispatchModelValueChangedEvent() {
|
_dispatchModelValueChangedEvent() {
|
||||||
|
/** @event model-value-changed */
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('model-value-changed', { bubbles: true, composed: true }),
|
new CustomEvent('model-value-changed', { bubbles: true, composed: true }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFormattedValueChanged() {
|
_onFormattedValueChanged() {
|
||||||
|
/** @deprecated */
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('formatted-value-changed', {
|
new CustomEvent('formatted-value-changed', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
|
|
@ -265,6 +297,7 @@ export const FormatMixin = dedupeMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSerializedValueChanged() {
|
_onSerializedValueChanged() {
|
||||||
|
/** @deprecated */
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('serialized-value-changed', {
|
new CustomEvent('serialized-value-changed', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
|
|
@ -275,33 +308,32 @@ export const FormatMixin = dedupeMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronization from <input>.value to <lion-field>.formattedValue
|
* Synchronization from `.inputElement.value` to `LionField` (flow [2])
|
||||||
*/
|
*/
|
||||||
_syncValueUpwards() {
|
_syncValueUpwards() {
|
||||||
// Downwards syncing should only happen for <lion-field>.value changes from 'above'
|
// Downwards syncing should only happen for `LionField`.value changes from 'above'
|
||||||
// This triggers _onModelValueChanged and connects user input to the
|
// This triggers _onModelValueChanged and connects user input to the
|
||||||
// parsing/formatting/serializing loop
|
// parsing/formatting/serializing loop
|
||||||
this.modelValue = this.__callParser(this.value);
|
this.modelValue = this.__callParser(this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronization from <lion-field>.value to <input>.value
|
* Synchronization from `LionField.value` to `.inputElement.value`
|
||||||
|
* - flow [1] will always be reflected back
|
||||||
|
* - flow [2] will not be reflected back when this flow was triggered via
|
||||||
|
* `@user-input-changed` (this will happen later, when `formatOn` condition is met)
|
||||||
*/
|
*/
|
||||||
_reflectBackFormattedValueToUser() {
|
_reflectBackFormattedValueToUser() {
|
||||||
// Downwards syncing 'back and forth' prevents change event from being fired in IE.
|
|
||||||
// So only sync when the source of new <lion-field>.value change was not the 'input' event
|
|
||||||
// of inputElement
|
|
||||||
if (!this.__isHandlingUserInput) {
|
if (!this.__isHandlingUserInput) {
|
||||||
// Text 'undefined' should not end up in <input>
|
// Text 'undefined' should not end up in <input>
|
||||||
this.value = typeof this.formattedValue !== 'undefined' ? this.formattedValue : '';
|
this.value = typeof this.formattedValue !== 'undefined' ? this.formattedValue : '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: rename to __dispatchNormalizedInputEvent?
|
|
||||||
// This can be called whenever the view value should be updated. Dependent on component type
|
// This can be called whenever the view value should be updated. Dependent on component type
|
||||||
// ("input" for <input> or "change" for <select>(mainly for IE)) a different event should be used
|
// ("input" for <input> or "change" for <select>(mainly for IE)) a different event should be
|
||||||
// as "source" for the "user-input-changed" event (which can be seen as an abstraction layer on
|
// used as source for the "user-input-changed" event (which can be seen as an abstraction
|
||||||
// top of other events (input, change, whatever))
|
// layer on top of other events (input, change, whatever))
|
||||||
_proxyInputEvent() {
|
_proxyInputEvent() {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('user-input-changed', {
|
new CustomEvent('user-input-changed', {
|
||||||
|
|
@ -313,8 +345,7 @@ export const FormatMixin = dedupeMixin(
|
||||||
|
|
||||||
_onUserInputChanged() {
|
_onUserInputChanged() {
|
||||||
// Upwards syncing. Most properties are delegated right away, value is synced to
|
// Upwards syncing. Most properties are delegated right away, value is synced to
|
||||||
// <lion-field>, to be able to act on (imperatively set) value changes
|
// `LionField`, to be able to act on (imperatively set) value changes
|
||||||
|
|
||||||
this.__isHandlingUserInput = true;
|
this.__isHandlingUserInput = true;
|
||||||
this._syncValueUpwards();
|
this._syncValueUpwards();
|
||||||
this.__isHandlingUserInput = false;
|
this.__isHandlingUserInput = false;
|
||||||
|
|
@ -338,9 +369,9 @@ export const FormatMixin = dedupeMixin(
|
||||||
this.addEventListener('user-input-changed', this._onUserInputChanged);
|
this.addEventListener('user-input-changed', this._onUserInputChanged);
|
||||||
// Connect the value found in <input> to the formatting/parsing/serializing loop as a
|
// Connect the value found in <input> to the formatting/parsing/serializing loop as a
|
||||||
// fallback mechanism. Assume the user uses the value property of the
|
// fallback mechanism. Assume the user uses the value property of the
|
||||||
// <lion-field>(recommended api) as the api (this is a downwards sync).
|
// `LionField`(recommended api) as the api (this is a downwards sync).
|
||||||
// However, when no value is specified on <lion-field>, have support for sync of the real
|
// However, when no value is specified on `LionField`, have support for sync of the real
|
||||||
// input to the <lion-field> (upwards sync).
|
// input to the `LionField` (upwards sync).
|
||||||
if (typeof this.modelValue === 'undefined') {
|
if (typeof this.modelValue === 'undefined') {
|
||||||
this._syncValueUpwards();
|
this._syncValueUpwards();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,43 @@
|
||||||
import { DelegateMixin, SlotMixin } from '@lion/core';
|
import { DelegateMixin, SlotMixin, LitElement } from '@lion/core';
|
||||||
import { LionLitElement } from '@lion/core/src/LionLitElement.js';
|
|
||||||
import { ElementMixin } from '@lion/core/src/ElementMixin.js';
|
import { ElementMixin } from '@lion/core/src/ElementMixin.js';
|
||||||
import { CssClassMixin } from '@lion/core/src/CssClassMixin.js';
|
import { CssClassMixin } from '@lion/core/src/CssClassMixin.js';
|
||||||
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
||||||
import { ValidateMixin } from '@lion/validate';
|
import { ValidateMixin } from '@lion/validate';
|
||||||
|
|
||||||
import { FormControlMixin } from './FormControlMixin.js';
|
import { FormControlMixin } from './FormControlMixin.js';
|
||||||
import { InteractionStateMixin } from './InteractionStateMixin.js'; // applies FocusMixin
|
import { InteractionStateMixin } from './InteractionStateMixin.js'; // applies FocusMixin
|
||||||
import { FormatMixin } from './FormatMixin.js';
|
import { FormatMixin } from './FormatMixin.js';
|
||||||
import { FocusMixin } from './FocusMixin.js';
|
import { FocusMixin } from './FocusMixin.js';
|
||||||
|
|
||||||
|
/* eslint-disable wc/guard-super-call */
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - Consider exporting as FieldMixin
|
||||||
|
// - Add submitted prop to InteractionStateMixin
|
||||||
|
// - Find a better way to do value delegation via attr
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LionField: wraps components input, textarea and select and potentially others
|
* `LionField`: wraps <input>, <textarea>, <select> and other interactable elements.
|
||||||
* (checkbox group, radio group)
|
|
||||||
* Also it would follow a nice hierarchy: lion-form -> lion-fieldset -> lion-field
|
* Also it would follow a nice hierarchy: lion-form -> lion-fieldset -> lion-field
|
||||||
*
|
*
|
||||||
|
* Note: We don't support placeholders, because we have a helper text and
|
||||||
|
* placeholders confuse the user with accessibility needs.
|
||||||
|
*
|
||||||
|
* Please see the docs for in depth information.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
* <lion-field name="myName">
|
* <lion-field name="myName">
|
||||||
* <label slot="label">My Input</label>
|
* <label slot="label">My Input</label>
|
||||||
* <input type="text" slot="input">
|
* <input type="text" slot="input">
|
||||||
* </lion-field>
|
* </lion-field>
|
||||||
*
|
*
|
||||||
* Note: We do not support placeholders, because we have a helper text and
|
|
||||||
* placeholders confuse the user with accessibility needs.
|
|
||||||
*
|
|
||||||
* @customElement
|
* @customElement
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO: Consider exporting as FieldMixin
|
|
||||||
// eslint-disable-next-line max-len, no-unused-vars
|
|
||||||
export class LionField extends FormControlMixin(
|
export class LionField extends FormControlMixin(
|
||||||
InteractionStateMixin(
|
InteractionStateMixin(
|
||||||
FocusMixin(
|
FocusMixin(
|
||||||
FormatMixin(
|
FormatMixin(
|
||||||
ValidateMixin(
|
ValidateMixin(
|
||||||
CssClassMixin(ElementMixin(DelegateMixin(SlotMixin(ObserverMixin(LionLitElement))))),
|
CssClassMixin(ElementMixin(DelegateMixin(SlotMixin(ObserverMixin(LitElement))))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -65,14 +69,7 @@ export class LionField extends FormControlMixin(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static get asyncObservers() {
|
// We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret
|
||||||
return {
|
|
||||||
...super.asyncObservers,
|
|
||||||
_setDisabledClass: ['disabled'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't delegate, because we want to 'preprocess' via _setValueAndPreserveCaret
|
|
||||||
set value(value) {
|
set value(value) {
|
||||||
// if not yet connected to dom can't change the value
|
// if not yet connected to dom can't change the value
|
||||||
if (this.inputElement) {
|
if (this.inputElement) {
|
||||||
|
|
@ -85,29 +82,26 @@ export class LionField extends FormControlMixin(
|
||||||
return (this.inputElement && this.inputElement.value) || '';
|
return (this.inputElement && this.inputElement.value) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
_setDisabledClass() {
|
static get asyncObservers() {
|
||||||
this.classList[this.disabled ? 'add' : 'remove']('state-disabled');
|
return {
|
||||||
|
...super.asyncObservers,
|
||||||
|
_setDisabledClass: ['disabled'],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
resetInteractionState() {
|
|
||||||
if (super.resetInteractionState) super.resetInteractionState();
|
|
||||||
// TODO: add submitted prop to InteractionStateMixin ?
|
|
||||||
this.submitted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* * * * * * * *
|
|
||||||
Lifecycle */
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
this._onChange = this._onChange.bind(this);
|
this._onChange = this._onChange.bind(this);
|
||||||
this.inputElement.addEventListener('change', this._onChange);
|
this.inputElement.addEventListener('change', this._onChange);
|
||||||
this._delegateInitialValueAttr(); // TODO: find a better way to do this
|
this._delegateInitialValueAttr();
|
||||||
this._setDisabledClass();
|
this._setDisabledClass();
|
||||||
this.classList.add('form-field');
|
this.classList.add('form-field'); // eslint-disable-line
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
|
||||||
if (this.__parentFormGroup) {
|
if (this.__parentFormGroup) {
|
||||||
const event = new CustomEvent('form-element-unregister', {
|
const event = new CustomEvent('form-element-unregister', {
|
||||||
detail: { element: this },
|
detail: { element: this },
|
||||||
|
|
@ -118,6 +112,10 @@ export class LionField extends FormControlMixin(
|
||||||
this.inputElement.removeEventListener('change', this._onChange);
|
this.inputElement.removeEventListener('change', this._onChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setDisabledClass() {
|
||||||
|
this.classList[this.disabled ? 'add' : 'remove']('state-disabled');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is not done via 'get delegations', because this.inputElement.setAttribute('value')
|
* This is not done via 'get delegations', because this.inputElement.setAttribute('value')
|
||||||
* does not trigger a value change
|
* does not trigger a value change
|
||||||
|
|
@ -129,33 +127,37 @@ export class LionField extends FormControlMixin(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
resetInteractionState() {
|
||||||
Public Methods (also notice delegated methods that are available on host) */
|
if (super.resetInteractionState) {
|
||||||
|
super.resetInteractionState();
|
||||||
|
}
|
||||||
|
this.submitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
// Let validationMixin and interactionStateMixin clear their invalid and dirty/touched states
|
if (super.clear) {
|
||||||
// respectively
|
// Let validationMixin and interactionStateMixin clear their
|
||||||
if (super.clear) super.clear();
|
// invalid and dirty/touched states respectively
|
||||||
|
super.clear();
|
||||||
|
}
|
||||||
this.value = ''; // can't set null here, because IE11 treats it as a string
|
this.value = ''; // can't set null here, because IE11 treats it as a string
|
||||||
}
|
}
|
||||||
|
|
||||||
/* * * * * * * * * *
|
|
||||||
Event Handlers */
|
|
||||||
|
|
||||||
_onChange() {
|
_onChange() {
|
||||||
if (super._onChange) super._onChange();
|
if (super._onChange) {
|
||||||
|
super._onChange();
|
||||||
|
}
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('user-input-changed', {
|
new CustomEvent('user-input-changed', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
this.modelValue = this.parser(this.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* * * * * * * * * * * *
|
|
||||||
Observer Handlers */
|
|
||||||
_onValueChanged({ value }) {
|
_onValueChanged({ value }) {
|
||||||
if (super._onValueChanged) super._onValueChanged();
|
if (super._onValueChanged) {
|
||||||
|
super._onValueChanged();
|
||||||
|
}
|
||||||
// For styling purposes, make it known the input field is not empty
|
// For styling purposes, make it known the input field is not empty
|
||||||
this.classList[value ? 'add' : 'remove']('state-filled');
|
this.classList[value ? 'add' : 'remove']('state-filled');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
unsafeStatic,
|
unsafeStatic,
|
||||||
triggerFocusFor,
|
triggerFocusFor,
|
||||||
triggerBlurFor,
|
triggerBlurFor,
|
||||||
|
aTimeout,
|
||||||
} from '@open-wc/testing';
|
} from '@open-wc/testing';
|
||||||
import { unsafeHTML } from '@lion/core';
|
import { unsafeHTML } from '@lion/core';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
@ -152,25 +153,24 @@ describe('<lion-field>', () => {
|
||||||
expect(lionField.inputElement.selectionEnd).to.equal(2);
|
expect(lionField.inputElement.selectionEnd).to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: add pointerEvents test
|
// TODO: add pointerEvents test for disabled
|
||||||
// TODO: why is this a describe?
|
it('has a class "state-disabled"', async () => {
|
||||||
describe(`<lion-field> with <input disabled>${nameSuffix}`, () => {
|
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
||||||
it('has a class "state-disabled"', async () => {
|
expect(lionField.classList.contains('state-disabled')).to.equal(false);
|
||||||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(false);
|
||||||
expect(lionField.classList.contains('state-disabled')).to.equal(false);
|
|
||||||
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(false);
|
|
||||||
|
|
||||||
lionField.disabled = true;
|
lionField.disabled = true;
|
||||||
await lionField.updateComplete;
|
await lionField.updateComplete;
|
||||||
expect(lionField.classList.contains('state-disabled')).to.equal(true);
|
await aTimeout();
|
||||||
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(true);
|
|
||||||
|
|
||||||
const disabledlionField = await fixture(
|
expect(lionField.classList.contains('state-disabled')).to.equal(true);
|
||||||
`<${tagString} disabled>${inputSlotString}</${tagString}>`,
|
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(true);
|
||||||
);
|
|
||||||
expect(disabledlionField.classList.contains('state-disabled')).to.equal(true);
|
const disabledlionField = await fixture(
|
||||||
expect(disabledlionField.inputElement.hasAttribute('disabled')).to.equal(true);
|
`<${tagString} disabled>${inputSlotString}</${tagString}>`,
|
||||||
});
|
);
|
||||||
|
expect(disabledlionField.classList.contains('state-disabled')).to.equal(true);
|
||||||
|
expect(disabledlionField.inputElement.hasAttribute('disabled')).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`A11y${nameSuffix}`, () => {
|
describe(`A11y${nameSuffix}`, () => {
|
||||||
|
|
@ -355,7 +355,7 @@ describe('<lion-field>', () => {
|
||||||
|
|
||||||
mimicUserInput(lionField, 'foo');
|
mimicUserInput(lionField, 'foo');
|
||||||
expect(formatterSpy.callCount).to.equal(1);
|
expect(formatterSpy.callCount).to.equal(1);
|
||||||
expect(lionField.formattedValue).to.equal('foo');
|
expect(lionField.value).to.equal('foo');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue