@lion/field
Advanced tools
Comparing version 0.1.28 to 0.1.29
@@ -6,2 +6,10 @@ # Change Log | ||
## [0.1.29](https://github.com/ing-bank/lion/compare/@lion/field@0.1.28...@lion/field@0.1.29) (2019-07-16) | ||
**Note:** Version bump only for package @lion/field | ||
## [0.1.28](https://github.com/ing-bank/lion/compare/@lion/field@0.1.27...@lion/field@0.1.28) (2019-07-15) | ||
@@ -8,0 +16,0 @@ |
@@ -7,16 +7,19 @@ # FormatMixin | ||
### 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. | ||
@@ -28,5 +31,5 @@ Examples: | ||
### 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 | ||
@@ -33,0 +36,0 @@ and a serialized string needs to be set. |
{ | ||
"name": "@lion/field", | ||
"version": "0.1.28", | ||
"version": "0.1.29", | ||
"description": "Fields are the most fundamental building block of the Form System", | ||
@@ -44,3 +44,3 @@ "author": "ing-bank", | ||
}, | ||
"gitHead": "482bcbeaeda6aa9c931327883b894f442cb23d05" | ||
"gitHead": "22256eed84f2fa5a6466ee59e9b5c7730f8fd7a2" | ||
} |
@@ -7,14 +7,44 @@ /* eslint-disable class-methods-use-this */ | ||
// 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` | ||
*/ | ||
@@ -32,5 +62,5 @@ export const FormatMixin = dedupeMixin( | ||
* 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`. | ||
* | ||
@@ -54,2 +84,4 @@ * Examples: | ||
* 1234.56) | ||
* | ||
* @private | ||
*/ | ||
@@ -73,2 +105,3 @@ formattedValue: { | ||
* (being inputElement.value) | ||
* | ||
*/ | ||
@@ -107,7 +140,2 @@ serializedValue: { | ||
/** | ||
* === Formatting and parsing ==== | ||
* To understand all concepts below, please consult the flow diagrams in the documentation. | ||
*/ | ||
/** | ||
* Converts formattedValue to modelValue | ||
@@ -123,6 +151,7 @@ * For instance, a localized date to a Date Object | ||
/** | ||
* 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 | ||
@@ -135,6 +164,6 @@ */ | ||
/** | ||
* 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 | ||
@@ -147,6 +176,6 @@ */ | ||
/** | ||
* 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 | ||
@@ -160,5 +189,4 @@ */ | ||
* 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 | ||
@@ -201,3 +229,3 @@ * second call from having effect). | ||
// - 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 ''; | ||
@@ -210,4 +238,4 @@ } | ||
// 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; | ||
@@ -229,6 +257,2 @@ } | ||
__callFormatter() { | ||
if (this.modelValue instanceof Unparseable) { | ||
return this.modelValue.viewValue; | ||
} | ||
// - Why check for this.errorState? | ||
@@ -238,6 +262,6 @@ // We only want to format values that are considered valid. For best UX, | ||
// - 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 | ||
@@ -249,2 +273,10 @@ // input into `.inputElement` with modelValue as input) | ||
} | ||
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); | ||
@@ -259,9 +291,8 @@ } | ||
// 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( | ||
@@ -273,2 +304,3 @@ new CustomEvent('model-value-changed', { bubbles: true, composed: true }), | ||
_onFormattedValueChanged() { | ||
/** @deprecated */ | ||
this.dispatchEvent( | ||
@@ -284,2 +316,3 @@ new CustomEvent('formatted-value-changed', { | ||
_onSerializedValueChanged() { | ||
/** @deprecated */ | ||
this.dispatchEvent( | ||
@@ -295,6 +328,6 @@ new CustomEvent('serialized-value-changed', { | ||
/** | ||
* 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 | ||
@@ -306,8 +339,8 @@ // parsing/formatting/serializing loop | ||
/** | ||
* 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) { | ||
@@ -319,7 +352,6 @@ // Text 'undefined' should not end up in <input> | ||
// 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() { | ||
@@ -336,4 +368,3 @@ this.dispatchEvent( | ||
// 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; | ||
@@ -362,5 +393,5 @@ this._syncValueUpwards(); | ||
// 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') { | ||
@@ -367,0 +398,0 @@ this._syncValueUpwards(); |
@@ -1,3 +0,2 @@ | ||
import { DelegateMixin, SlotMixin } from '@lion/core'; | ||
import { LionLitElement } from '@lion/core/src/LionLitElement.js'; | ||
import { DelegateMixin, SlotMixin, LitElement } from '@lion/core'; | ||
import { ElementMixin } from '@lion/core/src/ElementMixin.js'; | ||
@@ -7,3 +6,2 @@ import { CssClassMixin } from '@lion/core/src/CssClassMixin.js'; | ||
import { ValidateMixin } from '@lion/validate'; | ||
import { FormControlMixin } from './FormControlMixin.js'; | ||
@@ -14,7 +12,19 @@ import { InteractionStateMixin } from './InteractionStateMixin.js'; // applies FocusMixin | ||
/* 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 | ||
* (checkbox group, radio group) | ||
* `LionField`: wraps <input>, <textarea>, <select> and other interactable elements. | ||
* 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"> | ||
@@ -25,10 +35,4 @@ * <label slot="label">My Input</label> | ||
* | ||
* Note: We do not support placeholders, because we have a helper text and | ||
* placeholders confuse the user with accessibility needs. | ||
* | ||
* @customElement | ||
*/ | ||
// TODO: Consider exporting as FieldMixin | ||
// eslint-disable-next-line max-len, no-unused-vars | ||
export class LionField extends FormControlMixin( | ||
@@ -39,3 +43,3 @@ InteractionStateMixin( | ||
ValidateMixin( | ||
CssClassMixin(ElementMixin(DelegateMixin(SlotMixin(ObserverMixin(LionLitElement))))), | ||
CssClassMixin(ElementMixin(DelegateMixin(SlotMixin(ObserverMixin(LitElement))))), | ||
), | ||
@@ -72,10 +76,3 @@ ), | ||
static get asyncObservers() { | ||
return { | ||
...super.asyncObservers, | ||
_setDisabledClass: ['disabled'], | ||
}; | ||
} | ||
// We don't delegate, because we want to 'preprocess' via _setValueAndPreserveCaret | ||
// We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret | ||
set value(value) { | ||
@@ -93,21 +90,17 @@ // if not yet connected to dom can't change the value | ||
_setDisabledClass() { | ||
this.classList[this.disabled ? 'add' : 'remove']('state-disabled'); | ||
static get asyncObservers() { | ||
return { | ||
...super.asyncObservers, | ||
_setDisabledClass: ['disabled'], | ||
}; | ||
} | ||
resetInteractionState() { | ||
if (super.resetInteractionState) super.resetInteractionState(); | ||
// TODO: add submitted prop to InteractionStateMixin ? | ||
this.submitted = false; | ||
} | ||
/* * * * * * * * | ||
Lifecycle */ | ||
connectedCallback() { | ||
super.connectedCallback(); | ||
this._onChange = this._onChange.bind(this); | ||
this.inputElement.addEventListener('change', this._onChange); | ||
this._delegateInitialValueAttr(); // TODO: find a better way to do this | ||
this._delegateInitialValueAttr(); | ||
this._setDisabledClass(); | ||
this.classList.add('form-field'); | ||
this.classList.add('form-field'); // eslint-disable-line | ||
} | ||
@@ -117,2 +110,3 @@ | ||
super.disconnectedCallback(); | ||
if (this.__parentFormGroup) { | ||
@@ -128,2 +122,6 @@ const event = new CustomEvent('form-element-unregister', { | ||
_setDisabledClass() { | ||
this.classList[this.disabled ? 'add' : 'remove']('state-disabled'); | ||
} | ||
/** | ||
@@ -140,17 +138,22 @@ * This is not done via 'get delegations', because this.inputElement.setAttribute('value') | ||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | ||
Public Methods (also notice delegated methods that are available on host) */ | ||
resetInteractionState() { | ||
if (super.resetInteractionState) { | ||
super.resetInteractionState(); | ||
} | ||
this.submitted = false; | ||
} | ||
clear() { | ||
// Let validationMixin and interactionStateMixin clear their invalid and dirty/touched states | ||
// respectively | ||
if (super.clear) super.clear(); | ||
if (super.clear) { | ||
// Let validationMixin and interactionStateMixin clear their | ||
// invalid and dirty/touched states respectively | ||
super.clear(); | ||
} | ||
this.value = ''; // can't set null here, because IE11 treats it as a string | ||
} | ||
/* * * * * * * * * * | ||
Event Handlers */ | ||
_onChange() { | ||
if (super._onChange) super._onChange(); | ||
if (super._onChange) { | ||
super._onChange(); | ||
} | ||
this.dispatchEvent( | ||
@@ -161,9 +164,8 @@ new CustomEvent('user-input-changed', { | ||
); | ||
this.modelValue = this.parser(this.value); | ||
} | ||
/* * * * * * * * * * * * | ||
Observer Handlers */ | ||
_onValueChanged({ value }) { | ||
if (super._onValueChanged) super._onValueChanged(); | ||
if (super._onValueChanged) { | ||
super._onValueChanged(); | ||
} | ||
// For styling purposes, make it known the input field is not empty | ||
@@ -170,0 +172,0 @@ this.classList[value ? 'add' : 'remove']('state-filled'); |
@@ -8,2 +8,3 @@ import { | ||
triggerBlurFor, | ||
aTimeout, | ||
} from '@open-wc/testing'; | ||
@@ -156,21 +157,20 @@ import { unsafeHTML } from '@lion/core'; | ||
// TODO: add pointerEvents test | ||
// TODO: why is this a describe? | ||
describe(`<lion-field> with <input disabled>${nameSuffix}`, () => { | ||
it('has a class "state-disabled"', async () => { | ||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
expect(lionField.classList.contains('state-disabled')).to.equal(false); | ||
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(false); | ||
// TODO: add pointerEvents test for disabled | ||
it('has a class "state-disabled"', async () => { | ||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
expect(lionField.classList.contains('state-disabled')).to.equal(false); | ||
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(false); | ||
lionField.disabled = true; | ||
await lionField.updateComplete; | ||
expect(lionField.classList.contains('state-disabled')).to.equal(true); | ||
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(true); | ||
lionField.disabled = true; | ||
await lionField.updateComplete; | ||
await aTimeout(); | ||
const disabledlionField = await fixture( | ||
`<${tagString} disabled>${inputSlotString}</${tagString}>`, | ||
); | ||
expect(disabledlionField.classList.contains('state-disabled')).to.equal(true); | ||
expect(disabledlionField.inputElement.hasAttribute('disabled')).to.equal(true); | ||
}); | ||
expect(lionField.classList.contains('state-disabled')).to.equal(true); | ||
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(true); | ||
const disabledlionField = await fixture( | ||
`<${tagString} disabled>${inputSlotString}</${tagString}>`, | ||
); | ||
expect(disabledlionField.classList.contains('state-disabled')).to.equal(true); | ||
expect(disabledlionField.inputElement.hasAttribute('disabled')).to.equal(true); | ||
}); | ||
@@ -360,3 +360,3 @@ | ||
expect(formatterSpy.callCount).to.equal(1); | ||
expect(lionField.formattedValue).to.equal('foo'); | ||
expect(lionField.value).to.equal('foo'); | ||
}); | ||
@@ -363,0 +363,0 @@ }); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
178119
2302