chore(field): docs/annotations FormatMixin

This commit is contained in:
Thijs Louisse 2019-07-15 14:59:09 +02:00
parent 688fcb6690
commit a5bc33072f
2 changed files with 105 additions and 71 deletions

View file

@ -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.

View file

@ -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();
}