chore(field): docs/annotations FormatMixin
This commit is contained in:
parent
688fcb6690
commit
a5bc33072f
2 changed files with 105 additions and 71 deletions
|
|
@ -4,29 +4,32 @@ It is designed to work in conjunction with `LionField`.
|
|||
|
||||
## Concepts of different values
|
||||
|
||||
### modelValue
|
||||
The model value is the result of the parser function.
|
||||
It should be considered as the internal value used for validation and reasoning/logic.
|
||||
### model value
|
||||
The model value is the result of the parser function. It will be stored as `.modelValue`
|
||||
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
|
||||
or a float). The modelValue can(and is recommended to) be used as both input value and
|
||||
output value of the `<lion-field>`
|
||||
or a float). It can(and is recommended to) be used as both input value and
|
||||
output value of the `LionField`.
|
||||
|
||||
Examples:
|
||||
- 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 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`
|
||||
|
||||
### formattedValue
|
||||
The view value is the result of the formatter function (when available).
|
||||
The result will be stored in the native inputElement (usually an input[type=text]).
|
||||
### view value
|
||||
The view value is the result of the formatter function.
|
||||
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:
|
||||
- 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
|
||||
1234.56)
|
||||
|
||||
### serializedValue
|
||||
The serialized version of the model value.
|
||||
This value exists for maximal compatibility with the platform API.
|
||||
### serialized value
|
||||
This is the serialized version of the model value.
|
||||
It exists for maximal compatibility with the platform API.
|
||||
The serialized value can be an interface in context where data binding is not supported
|
||||
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 { 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:
|
||||
* 1) Application Developer sets `.modelValue`:
|
||||
* Flow: `.modelValue` -> `.formattedValue` -> `.inputElement.value`
|
||||
* -> `.serializedValue`
|
||||
* 2) End user interacts with field:
|
||||
* Flow: `@user-input-changed` -> `.modelValue` -> `.formattedValue` - (debounce till reflect condition (formatOn) is met) -> `.inputElement.value`
|
||||
* -> `.serializedValue`
|
||||
* [1] Application Developer sets `.modelValue`:
|
||||
* Flow: `.modelValue` (formatter) -> `.formattedValue` -> `.inputElement.value`
|
||||
* (serializer) -> `.serializedValue`
|
||||
* [2] End user interacts with field:
|
||||
* Flow: `@user-input-changed` (parser) -> `.modelValue` (formatter) -> `.formattedValue` - (debounce till reflect condition (formatOn) is met) -> `.inputElement.value`
|
||||
* (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(
|
||||
superclass =>
|
||||
|
|
@ -28,9 +58,9 @@ export const FormatMixin = dedupeMixin(
|
|||
/**
|
||||
* 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.
|
||||
* 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
|
||||
* output value of the <lion-field>
|
||||
* 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 output value of the `LionField`.
|
||||
*
|
||||
* Examples:
|
||||
* - 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 number input, this could be '1,234.56' (a String representation of modelValue
|
||||
* 1234.56)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
formattedValue: {
|
||||
type: String,
|
||||
|
|
@ -67,6 +99,7 @@ export const FormatMixin = dedupeMixin(
|
|||
*
|
||||
* When no parser is available, the value is usually the same as the formattedValue
|
||||
* (being inputElement.value)
|
||||
*
|
||||
*/
|
||||
serializedValue: {
|
||||
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
|
||||
* 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)
|
||||
* For instance, a Date object to a localized date
|
||||
* @param {Object} value - modelValue: can be an Object, Number, String depending on the input
|
||||
* type(date, number, email etc)
|
||||
* Converts modelValue to formattedValue (formattedValue will be synced with
|
||||
* `.inputElement.value`)
|
||||
* For instance, a Date object to a localized date.
|
||||
* @param {Object} value - modelValue: can be an Object, Number, String depending on the
|
||||
* input type(date, number, email etc)
|
||||
* @returns {String} formattedValue
|
||||
*/
|
||||
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
|
||||
* @param {Object} value - modelValue: can be an Object, Number, String depending on the input
|
||||
* 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} serializedValue
|
||||
*/
|
||||
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
|
||||
* @param {Object} value - modelValue: can be an Object, Number, String depending on the input
|
||||
* 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 {Object} modelValue
|
||||
*/
|
||||
deserializer(v) {
|
||||
|
|
@ -149,9 +178,8 @@ export const FormatMixin = dedupeMixin(
|
|||
|
||||
/**
|
||||
* Responsible for storing all representations(modelValue, serializedValue, formattedValue
|
||||
* and value) of the input value.
|
||||
* Prevents infinite loops, so all value observers can be treated like they will only be
|
||||
* called once, without indirectly calling other observers.
|
||||
* and value) of the input value. Prevents infinite loops, so all value observers can be
|
||||
* treated like they will only be called once, without indirectly calling other observers.
|
||||
* (in fact, some are called twice, but the __preventRecursiveTrigger lock prevents the
|
||||
* second call from having effect).
|
||||
*
|
||||
|
|
@ -189,7 +217,7 @@ export const FormatMixin = dedupeMixin(
|
|||
// For backwards compatibility we return an empty string:
|
||||
// - it triggers validation for required validators (see ValidateMixin.validate())
|
||||
// - 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 '';
|
||||
}
|
||||
|
||||
|
|
@ -197,8 +225,8 @@ export const FormatMixin = dedupeMixin(
|
|||
// inputElement.value was not available yet
|
||||
if (typeof value !== 'string') {
|
||||
// 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
|
||||
// retrieval.
|
||||
// interest to the Application Developer or needed to store for future
|
||||
// form state retrieval.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -215,24 +243,28 @@ export const FormatMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
__callFormatter() {
|
||||
if (this.modelValue instanceof Unparseable) {
|
||||
return this.modelValue.viewValue;
|
||||
}
|
||||
|
||||
// - Why check for this.errorState?
|
||||
// We only want to format values that are considered valid. For best UX,
|
||||
// we only 'reward' valid inputs.
|
||||
// - Why check for __isHandlingUserInput?
|
||||
// Downwards sync is prevented whenever we are in a `@user-input-changed` flow.
|
||||
// If we are in a 'imperatively set `.modelValue`' flow, we want to reflect back
|
||||
// Downwards sync is prevented whenever we are in an `@user-input-changed` flow, [2].
|
||||
// If we are in a 'imperatively set `.modelValue`' flow, [1], we want to reflect back
|
||||
// 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
|
||||
// input into `.inputElement` with modelValue as input)
|
||||
|
||||
if (this.__isHandlingUserInput && this.errorState && this.inputElement) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -242,19 +274,19 @@ export const FormatMixin = dedupeMixin(
|
|||
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
|
||||
* fired. For instance: when modelValue is an object, a deep comparison is needed first
|
||||
* This is wrapped in a distinct method, so that parents can control when the changed event
|
||||
* is fired. For objects, a deep comparison might be needed.
|
||||
*/
|
||||
_dispatchModelValueChangedEvent() {
|
||||
/** @event model-value-changed */
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('model-value-changed', { bubbles: true, composed: true }),
|
||||
);
|
||||
}
|
||||
|
||||
_onFormattedValueChanged() {
|
||||
/** @deprecated */
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('formatted-value-changed', {
|
||||
bubbles: true,
|
||||
|
|
@ -265,6 +297,7 @@ export const FormatMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
_onSerializedValueChanged() {
|
||||
/** @deprecated */
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('serialized-value-changed', {
|
||||
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() {
|
||||
// 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
|
||||
// parsing/formatting/serializing loop
|
||||
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() {
|
||||
// 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) {
|
||||
// Text 'undefined' should not end up in <input>
|
||||
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
|
||||
// ("input" for <input> or "change" for <select>(mainly for IE)) a different event should be used
|
||||
// as "source" for the "user-input-changed" event (which can be seen as an abstraction layer on
|
||||
// top of other events (input, change, whatever))
|
||||
// ("input" for <input> or "change" for <select>(mainly for IE)) a different event should be
|
||||
// used as source for the "user-input-changed" event (which can be seen as an abstraction
|
||||
// layer on top of other events (input, change, whatever))
|
||||
_proxyInputEvent() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('user-input-changed', {
|
||||
|
|
@ -313,8 +345,7 @@ export const FormatMixin = dedupeMixin(
|
|||
|
||||
_onUserInputChanged() {
|
||||
// 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._syncValueUpwards();
|
||||
this.__isHandlingUserInput = false;
|
||||
|
|
@ -338,9 +369,9 @@ export const FormatMixin = dedupeMixin(
|
|||
this.addEventListener('user-input-changed', this._onUserInputChanged);
|
||||
// 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
|
||||
// <lion-field>(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
|
||||
// input to the <lion-field> (upwards sync).
|
||||
// `LionField`(recommended api) as the api (this is a downwards sync).
|
||||
// However, when no value is specified on `LionField`, have support for sync of the real
|
||||
// input to the `LionField` (upwards sync).
|
||||
if (typeof this.modelValue === 'undefined') {
|
||||
this._syncValueUpwards();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue