diff --git a/packages/field/docs/FormatMixin.md b/packages/field/docs/FormatMixin.md index 5357aea93..32b7673bc 100644 --- a/packages/field/docs/FormatMixin.md +++ b/packages/field/docs/FormatMixin.md @@ -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 `` +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. diff --git a/packages/field/src/FormatMixin.js b/packages/field/src/FormatMixin.js index a63fcef44..1a9b48d0c 100644 --- a/packages/field/src/FormatMixin.js +++ b/packages/field/src/FormatMixin.js @@ -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 + * 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 .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 (.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 .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 .value to .formattedValue + * Synchronization from `.inputElement.value` to `LionField` (flow [2]) */ _syncValueUpwards() { - // Downwards syncing should only happen for .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 .value to .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 .value change was not the 'input' event - // of inputElement if (!this.__isHandlingUserInput) { // Text 'undefined' should not end up in 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 or "change" for or "change" for to the formatting/parsing/serializing loop as a // fallback mechanism. Assume the user uses the value property of the - // (recommended api) as the api (this is a downwards sync). - // However, when no value is specified on , have support for sync of the real - // input to the (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(); }