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

View file

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