feat(form-core): member descriptions for editors and api tables
This commit is contained in:
parent
3b187fa2c9
commit
8cd0142d12
35 changed files with 1253 additions and 429 deletions
9
.changeset/selfish-ears-flash.md
Normal file
9
.changeset/selfish-ears-flash.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
'@lion/form-core': minor
|
||||||
|
'@lion/input-range': minor
|
||||||
|
'@lion/listbox': minor
|
||||||
|
'@lion/radio-group': minor
|
||||||
|
'@lion/select': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
member descriptions for editors and api tables
|
||||||
|
|
@ -41,7 +41,7 @@ const FocusMixinImplementation = superclass =>
|
||||||
/**
|
/**
|
||||||
* Whether the focusable element within (`._focusableNode`) matches ':focus-visible'
|
* Whether the focusable element within (`._focusableNode`) matches ':focus-visible'
|
||||||
* Reflects to attribute '[focused-visible]' as a styling hook
|
* Reflects to attribute '[focused-visible]' as a styling hook
|
||||||
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible
|
* See: https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.focusedVisible = false;
|
this.focusedVisible = false;
|
||||||
|
|
|
||||||
|
|
@ -40,83 +40,22 @@ const FormControlMixinImplementation = superclass =>
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
name: { type: String, reflect: true },
|
||||||
* The name the element will be registered on to the .formElements collection
|
readOnly: { type: Boolean, attribute: 'readonly', reflect: true },
|
||||||
* of the parent.
|
|
||||||
*/
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* A Boolean attribute which, if present, indicates that the user should not be able to edit
|
|
||||||
* the value of the input. The difference between disabled and readonly is that read-only
|
|
||||||
* controls can still function, whereas disabled controls generally do not function as
|
|
||||||
* controls until they are enabled.
|
|
||||||
*
|
|
||||||
* (From: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly)
|
|
||||||
*/
|
|
||||||
readOnly: {
|
|
||||||
type: Boolean,
|
|
||||||
attribute: 'readonly',
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* The label text for the input node.
|
|
||||||
* When no light dom defined via [slot=label], this value will be used
|
|
||||||
*/
|
|
||||||
label: String, // FIXME: { attribute: false } breaks a bunch of tests, but shouldn't...
|
label: String, // FIXME: { attribute: false } breaks a bunch of tests, but shouldn't...
|
||||||
/**
|
helpText: { type: String, attribute: 'help-text' },
|
||||||
* The helpt text for the input node.
|
|
||||||
* When no light dom defined via [slot=help-text], this value will be used
|
|
||||||
*/
|
|
||||||
helpText: {
|
|
||||||
type: String,
|
|
||||||
attribute: 'help-text',
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 `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
|
|
||||||
*/
|
|
||||||
modelValue: { attribute: false },
|
modelValue: { attribute: false },
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains all elements that should end up in aria-labelledby of `._inputNode`
|
|
||||||
*/
|
|
||||||
_ariaLabelledNodes: { attribute: false },
|
_ariaLabelledNodes: { attribute: false },
|
||||||
/**
|
|
||||||
* Contains all elements that should end up in aria-describedby of `._inputNode`
|
|
||||||
*/
|
|
||||||
_ariaDescribedNodes: { attribute: false },
|
_ariaDescribedNodes: { attribute: false },
|
||||||
/**
|
|
||||||
* Based on the role, details of handling model-value-changed repropagation differ.
|
|
||||||
*/
|
|
||||||
_repropagationRole: { attribute: false },
|
_repropagationRole: { attribute: false },
|
||||||
/**
|
|
||||||
* By default, a field with _repropagationRole 'choice-group' will act as an
|
|
||||||
* 'endpoint'. This means it will be considered as an individual field: for
|
|
||||||
* a select, individual options will not be part of the formPath. They
|
|
||||||
* will.
|
|
||||||
* Similarly, components that (a11y wise) need to be fieldsets, but 'interaction wise'
|
|
||||||
* (from Application Developer perspective) need to be more like fields
|
|
||||||
* (think of an amount-input with a currency select box next to it), can set this
|
|
||||||
* to true to hide private internals in the formPath.
|
|
||||||
*/
|
|
||||||
_isRepropagationEndpoint: { attribute: false },
|
_isRepropagationEndpoint: { attribute: false },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {string}
|
* The label text for the input node.
|
||||||
|
* When no light dom defined via [slot=label], this value will be used.
|
||||||
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
get label() {
|
get label() {
|
||||||
return this.__label || (this._labelNode && this._labelNode.textContent) || '';
|
return this.__label || (this._labelNode && this._labelNode.textContent) || '';
|
||||||
|
|
@ -133,7 +72,9 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {string}
|
* The helpt text for the input node.
|
||||||
|
* When no light dom defined via [slot=help-text], this value will be used
|
||||||
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
get helpText() {
|
get helpText() {
|
||||||
return this.__helpText || (this._helpTextNode && this._helpTextNode.textContent) || '';
|
return this.__helpText || (this._helpTextNode && this._helpTextNode.textContent) || '';
|
||||||
|
|
@ -150,7 +91,8 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {string}
|
* Will be used in validation messages to refer to the current field
|
||||||
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
get fieldName() {
|
get fieldName() {
|
||||||
return this.__fieldName || this.label || this.name || '';
|
return this.__fieldName || this.label || this.name || '';
|
||||||
|
|
@ -165,7 +107,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {SlotsMap}
|
* @configure SlotMixin
|
||||||
*/
|
*/
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -183,20 +125,33 @@ const FormControlMixinImplementation = superclass =>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @protected */
|
/**
|
||||||
|
* The interactive (form) element. Can be a native element like input/textarea/select or
|
||||||
|
* an element with tabindex > -1
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
get _inputNode() {
|
get _inputNode() {
|
||||||
return /** @type {HTMLElementWithValue} */ (this.__getDirectSlotChild('input'));
|
return /** @type {HTMLElementWithValue} */ (this.__getDirectSlotChild('input'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element where label will be rendered to
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
get _labelNode() {
|
get _labelNode() {
|
||||||
return /** @type {HTMLElement} */ (this.__getDirectSlotChild('label'));
|
return /** @type {HTMLElement} */ (this.__getDirectSlotChild('label'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element where help text will be rendered to
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
get _helpTextNode() {
|
get _helpTextNode() {
|
||||||
return /** @type {HTMLElement} */ (this.__getDirectSlotChild('help-text'));
|
return /** @type {HTMLElement} */ (this.__getDirectSlotChild('help-text'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Element where validation feedback will be rendered to
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
get _feedbackNode() {
|
get _feedbackNode() {
|
||||||
|
|
@ -205,19 +160,91 @@ const FormControlMixinImplementation = superclass =>
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
/** @type {string} */
|
|
||||||
|
/**
|
||||||
|
* The name the element will be registered with to the .formElements collection
|
||||||
|
* of the parent. Also, it serves as the key of key/value pairs in
|
||||||
|
* modelValue/serializedValue objects
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
this.name = '';
|
this.name = '';
|
||||||
/** @type {string} */
|
|
||||||
|
/**
|
||||||
|
* A Boolean attribute which, if present, indicates that the user should not be able to edit
|
||||||
|
* the value of the input. The difference between disabled and readonly is that read-only
|
||||||
|
* controls can still function, whereas disabled controls generally do not function as
|
||||||
|
* controls until they are enabled.
|
||||||
|
* (From: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly)
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.readOnly = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label text for the input node.
|
||||||
|
* When no value is defined, textContent of [slot=label] will be used
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.label = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The helpt text for the input node.
|
||||||
|
* When no value is defined, textContent of [slot=help-text] will be used
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.helpText = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 `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
|
||||||
|
*/
|
||||||
|
// TODO: we can probably set this up properly once propert effects run from firstUpdated
|
||||||
|
// this.modelValue = undefined;
|
||||||
|
/**
|
||||||
|
* Unique id that can be used in all light dom
|
||||||
|
* @type {string}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
this._inputId = uuid(this.localName);
|
this._inputId = uuid(this.localName);
|
||||||
/** @type {HTMLElement[]} */
|
|
||||||
|
/**
|
||||||
|
* Contains all elements that should end up in aria-labelledby of `._inputNode`
|
||||||
|
* @type {HTMLElement[]}
|
||||||
|
*/
|
||||||
this._ariaLabelledNodes = [];
|
this._ariaLabelledNodes = [];
|
||||||
/** @type {HTMLElement[]} */
|
|
||||||
|
/**
|
||||||
|
* Contains all elements that should end up in aria-describedby of `._inputNode`
|
||||||
|
* @type {HTMLElement[]}
|
||||||
|
*/
|
||||||
this._ariaDescribedNodes = [];
|
this._ariaDescribedNodes = [];
|
||||||
/** @type {'child'|'choice-group'|'fieldset'} */
|
|
||||||
|
/**
|
||||||
|
* Based on the role, details of handling model-value-changed repropagation differ.
|
||||||
|
* @type {'child'|'choice-group'|'fieldset'}
|
||||||
|
*/
|
||||||
this._repropagationRole = 'child';
|
this._repropagationRole = 'child';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, a field with _repropagationRole 'choice-group' will act as an
|
||||||
|
* 'endpoint'. This means it will be considered as an individual field: for
|
||||||
|
* a select, individual options will not be part of the formPath. They
|
||||||
|
* will.
|
||||||
|
* Similarly, components that (a11y wise) need to be fieldsets, but 'interaction wise'
|
||||||
|
* (from Application Developer perspective) need to be more like fields
|
||||||
|
* (think of an amount-input with a currency select box next to it), can set this
|
||||||
|
* to true to hide private internals in the formPath.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
this._isRepropagationEndpoint = false;
|
this._isRepropagationEndpoint = false;
|
||||||
/** @private */
|
|
||||||
this.__label = '';
|
|
||||||
this.addEventListener(
|
this.addEventListener(
|
||||||
'model-value-changed',
|
'model-value-changed',
|
||||||
/** @type {EventListenerOrEventListenerObject} */ (this.__repropagateChildrenValues),
|
/** @type {EventListenerOrEventListenerObject} */ (this.__repropagateChildrenValues),
|
||||||
|
|
@ -274,6 +301,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
|
|
||||||
if (changedProperties.has('name')) {
|
if (changedProperties.has('name')) {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
|
/** @privateEvent */
|
||||||
new CustomEvent('form-element-name-changed', {
|
new CustomEvent('form-element-name-changed', {
|
||||||
detail: { oldName: changedProperties.get('name'), newName: this.name },
|
detail: { oldName: changedProperties.get('name'), newName: this.name },
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
|
|
@ -551,6 +579,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Used for Required validation and computation of interaction states
|
||||||
* @param {any} modelValue
|
* @param {any} modelValue
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
* @protected
|
* @protected
|
||||||
|
|
@ -713,7 +742,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Meant for Application Developers wanting to add to aria-labelledby attribute.
|
* Allows to add extra element references to aria-labelledby attribute.
|
||||||
* @param {HTMLElement} element
|
* @param {HTMLElement} element
|
||||||
* @param {{idPrefix?:string; reorder?: boolean}} customConfig
|
* @param {{idPrefix?:string; reorder?: boolean}} customConfig
|
||||||
*/
|
*/
|
||||||
|
|
@ -729,7 +758,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Meant for Application Developers wanting to delete from aria-labelledby attribute.
|
* Allows to remove element references from aria-labelledby attribute.
|
||||||
* @param {HTMLElement} element
|
* @param {HTMLElement} element
|
||||||
*/
|
*/
|
||||||
removeFromAriaLabelledBy(element) {
|
removeFromAriaLabelledBy(element) {
|
||||||
|
|
@ -744,7 +773,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Meant for Application Developers wanting to add to aria-describedby attribute.
|
* Allows to add element references to aria-describedby attribute.
|
||||||
* @param {HTMLElement} element
|
* @param {HTMLElement} element
|
||||||
* @param {{idPrefix?:string; reorder?: boolean}} customConfig
|
* @param {{idPrefix?:string; reorder?: boolean}} customConfig
|
||||||
*/
|
*/
|
||||||
|
|
@ -760,7 +789,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Meant for Application Developers wanting to delete from aria-describedby attribute.
|
* Allows to remove element references from aria-describedby attribute.
|
||||||
* @param {HTMLElement} element
|
* @param {HTMLElement} element
|
||||||
*/
|
*/
|
||||||
removeFromAriaDescribedBy(element) {
|
removeFromAriaDescribedBy(element) {
|
||||||
|
|
@ -810,6 +839,8 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Hook for Subclassers to add logic before repropagation
|
||||||
|
* @configurable
|
||||||
* @param {CustomEvent} ev
|
* @param {CustomEvent} ev
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -62,46 +62,8 @@ const FormatMixinImplementation = superclass =>
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
|
||||||
* The view value is the result of the formatter function (when available).
|
|
||||||
* The result will be stored in the native _inputNode (usually an input[type=text]).
|
|
||||||
*
|
|
||||||
* 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)
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
formattedValue: { attribute: false },
|
formattedValue: { attribute: false },
|
||||||
|
|
||||||
/**
|
|
||||||
* The serialized version of the model value.
|
|
||||||
* This value 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.
|
|
||||||
*
|
|
||||||
* Examples:
|
|
||||||
* - For a date input, this would be the iso format of a date, e.g. '1999-01-20'.
|
|
||||||
* - For a number input this would be the String representation of a float ('1234.56'
|
|
||||||
* instead of 1234.56)
|
|
||||||
*
|
|
||||||
* When no parser is available, the value is usually the same as the formattedValue
|
|
||||||
* (being _inputNode.value)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
serializedValue: { attribute: false },
|
serializedValue: { attribute: false },
|
||||||
|
|
||||||
/**
|
|
||||||
* Event that will trigger formatting (more precise, visual update of the view, so the
|
|
||||||
* user sees the formatted value)
|
|
||||||
* Default: 'change'
|
|
||||||
*/
|
|
||||||
formatOn: { attribute: false },
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration object that will be available inside the formatter function
|
|
||||||
*/
|
|
||||||
formatOptions: { attribute: false },
|
formatOptions: { attribute: false },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -124,11 +86,13 @@ const FormatMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view value. Will be delegated to `._inputNode.value`
|
||||||
|
*/
|
||||||
get value() {
|
get value() {
|
||||||
return (this._inputNode && this._inputNode.value) || this.__value || '';
|
return (this._inputNode && this._inputNode.value) || this.__value || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret
|
|
||||||
/** @param {string} value */
|
/** @param {string} value */
|
||||||
set value(value) {
|
set value(value) {
|
||||||
// if not yet connected to dom can't change the value
|
// if not yet connected to dom can't change the value
|
||||||
|
|
@ -142,6 +106,15 @@ const FormatMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Preprocesses the viewValue before it's parsed to a modelValue. Can be used to filter
|
||||||
|
* invalid input amongst others.
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* preprocessor(viewValue) {
|
||||||
|
* // only use digits
|
||||||
|
* return viewValue.replace(/\D/g, '');
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
* @param {string} v - the raw value from the <input> after keyUp/Down event
|
* @param {string} v - the raw value from the <input> after keyUp/Down event
|
||||||
* @returns {string} preprocessedValue: the result of preprocessing for invalid input
|
* @returns {string} preprocessedValue: the result of preprocessing for invalid input
|
||||||
*/
|
*/
|
||||||
|
|
@ -150,9 +123,9 @@ const FormatMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts formattedValue to modelValue
|
* Converts viewValue to modelValue
|
||||||
* For instance, a localized date to a Date Object
|
* For instance, a localized date to a Date Object
|
||||||
* @param {string} v - formattedValue: the formatted value inside <input>
|
* @param {string} v - viewValue: the formatted value inside <input>
|
||||||
* @param {FormatOptions} opts
|
* @param {FormatOptions} opts
|
||||||
* @returns {*} modelValue
|
* @returns {*} modelValue
|
||||||
*/
|
*/
|
||||||
|
|
@ -187,7 +160,7 @@ const FormatMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts `LionField.value` to `.modelValue`
|
* Converts `.serializedValue` 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 {?} v - modelValue: can be an Object, Number, String depending on the
|
* @param {?} v - modelValue: can be an Object, Number, String depending on the
|
||||||
* input type(date, number, email etc)
|
* input type(date, number, email etc)
|
||||||
|
|
@ -223,11 +196,9 @@ const FormatMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (source !== 'formatted') {
|
if (source !== 'formatted') {
|
||||||
/** @type {string} */
|
|
||||||
this.formattedValue = this._callFormatter();
|
this.formattedValue = this._callFormatter();
|
||||||
}
|
}
|
||||||
if (source !== 'serialized') {
|
if (source !== 'serialized') {
|
||||||
/** @type {string} */
|
|
||||||
this.serializedValue = this.serializer(this.modelValue);
|
this.serializedValue = this.serializer(this.modelValue);
|
||||||
}
|
}
|
||||||
this._reflectBackFormattedValueToUser();
|
this._reflectBackFormattedValueToUser();
|
||||||
|
|
@ -310,7 +281,6 @@ const FormatMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observer Handlers
|
|
||||||
* @param {{ modelValue: unknown; }[]} args
|
* @param {{ modelValue: unknown; }[]} args
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
|
|
@ -320,15 +290,16 @@ const FormatMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ modelValue: unknown; }[]} args
|
|
||||||
* This is wrapped in a distinct method, so that parents can control when the changed event
|
* 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.
|
* is fired. For objects, a deep comparison might be needed.
|
||||||
|
* @param {{ modelValue: unknown; }[]} args
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
_dispatchModelValueChangedEvent(...args) {
|
_dispatchModelValueChangedEvent(...args) {
|
||||||
/** @event model-value-changed */
|
/** @event model-value-changed */
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
|
/** @privateEvent model-value-changed: FormControl redispatches it as public event */
|
||||||
new CustomEvent('model-value-changed', {
|
new CustomEvent('model-value-changed', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: /** @type { ModelValueEventDetails } */ ({
|
detail: /** @type { ModelValueEventDetails } */ ({
|
||||||
|
|
@ -396,12 +367,9 @@ const FormatMixinImplementation = superclass =>
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_proxyInputEvent() {
|
_proxyInputEvent() {
|
||||||
this.dispatchEvent(
|
// TODO: [v1] remove composed (and bubbles as well if possible)
|
||||||
new CustomEvent('user-input-changed', {
|
/** @protectedEvent user-input-changed meant for usage by Subclassers only */
|
||||||
bubbles: true,
|
this.dispatchEvent(new Event('user-input-changed', { bubbles: true }));
|
||||||
composed: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @protected */
|
/** @protected */
|
||||||
|
|
@ -428,8 +396,52 @@ const FormatMixinImplementation = superclass =>
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
// TODO: [v1] delete; use 'change' event directly within this file
|
||||||
|
/**
|
||||||
|
* Event that will trigger formatting (more precise, visual update of the view, so the
|
||||||
|
* user sees the formatted value)
|
||||||
|
* Default: 'change'
|
||||||
|
* @deprecated use _reflectBackOn()
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
this.formatOn = 'change';
|
this.formatOn = 'change';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration object that will be available inside the formatter function
|
||||||
|
*/
|
||||||
this.formatOptions = /** @type {FormatOptions} */ ({});
|
this.formatOptions = /** @type {FormatOptions} */ ({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view value is the result of the formatter function (when available).
|
||||||
|
* The result will be stored in the native _inputNode (usually an input[type=text]).
|
||||||
|
*
|
||||||
|
* 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)
|
||||||
|
* @type {string|undefined}
|
||||||
|
* @readOnly
|
||||||
|
*/
|
||||||
|
this.formattedValue = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The serialized version of the model value.
|
||||||
|
* This value 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.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - For a date input, this would be the iso format of a date, e.g. '1999-01-20'.
|
||||||
|
* - For a number input this would be the String representation of a float ('1234.56'
|
||||||
|
* instead of 1234.56)
|
||||||
|
*
|
||||||
|
* When no parser is available, the value is usually the same as the formattedValue
|
||||||
|
* (being _inputNode.value)
|
||||||
|
* @type {string|undefined}
|
||||||
|
*/
|
||||||
|
this.serializedValue = undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the user is pasting content. Allows Subclassers to do this in their subclass:
|
* Whether the user is pasting content. Allows Subclassers to do this in their subclass:
|
||||||
* @example
|
* @example
|
||||||
|
|
@ -439,8 +451,18 @@ const FormatMixinImplementation = superclass =>
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
* @protected
|
* @protected
|
||||||
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this._isPasting = false;
|
this._isPasting = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag that will be set when user interaction takes place (for instance after an 'input'
|
||||||
|
* event). Will be added as meta info to the `model-value-changed` event. Depending on
|
||||||
|
* whether a user is interacting, formatting logic will be handled differently.
|
||||||
|
* @protected
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this._isHandlingUserInput = false;
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
|
@ -451,6 +473,20 @@ const FormatMixinImplementation = superclass =>
|
||||||
this.addEventListener('user-input-changed', this._onUserInputChanged);
|
this.addEventListener('user-input-changed', this._onUserInputChanged);
|
||||||
// This sets the formatted viewValue after paste
|
// This sets the formatted viewValue after paste
|
||||||
this.addEventListener('paste', this.__onPaste);
|
this.addEventListener('paste', this.__onPaste);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
this._reflectBackFormattedValueToUser = this._reflectBackFormattedValueToUser.bind(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._reflectBackFormattedValueDebounced = () => {
|
||||||
|
// Make sure this is fired after the change event of _inputNode, so that formattedValue
|
||||||
|
// is guaranteed to be calculated
|
||||||
|
setTimeout(this._reflectBackFormattedValueToUser);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
__onPaste() {
|
__onPaste() {
|
||||||
|
|
@ -464,13 +500,6 @@ const FormatMixinImplementation = superclass =>
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._reflectBackFormattedValueToUser = this._reflectBackFormattedValueToUser.bind(this);
|
|
||||||
|
|
||||||
this._reflectBackFormattedValueDebounced = () => {
|
|
||||||
// Make sure this is fired after the change event of _inputNode, so that formattedValue
|
|
||||||
// is guaranteed to be calculated
|
|
||||||
setTimeout(this._reflectBackFormattedValueToUser);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
||||||
|
|
@ -23,47 +23,15 @@ const InteractionStateMixinImplementation = superclass =>
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
touched: { type: Boolean, reflect: true },
|
||||||
* True when user has focused and left(blurred) the field.
|
dirty: { type: Boolean, reflect: true },
|
||||||
*/
|
filled: { type: Boolean, reflect: true },
|
||||||
touched: {
|
prefilled: { attribute: false },
|
||||||
type: Boolean,
|
submitted: { attribute: false },
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* True when user has changed the value of the field.
|
|
||||||
*/
|
|
||||||
dirty: {
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* True when the modelValue is non-empty (see _isEmpty in FormControlMixin)
|
|
||||||
*/
|
|
||||||
filled: {
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* True when user has left non-empty field or input is prefilled.
|
|
||||||
* The name must be seen from the point of view of the input field:
|
|
||||||
* once the user enters the input field, the value is non-empty.
|
|
||||||
*/
|
|
||||||
prefilled: {
|
|
||||||
attribute: false,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* True when user has attempted to submit the form, e.g. through a button
|
|
||||||
* of type="submit"
|
|
||||||
*/
|
|
||||||
submitted: {
|
|
||||||
attribute: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {PropertyKey} name
|
* @param {PropertyKey} name
|
||||||
* @param {*} oldVal
|
* @param {*} oldVal
|
||||||
*/
|
*/
|
||||||
|
|
@ -86,18 +54,65 @@ const InteractionStateMixinImplementation = superclass =>
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when user has focused and left(blurred) the field.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
this.touched = false;
|
this.touched = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when user has changed the value of the field.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when user has left non-empty field or input is prefilled.
|
||||||
|
* The name must be seen from the point of view of the input field:
|
||||||
|
* once the user enters the input field, the value is non-empty.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
this.prefilled = false;
|
this.prefilled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the modelValue is non-empty (see _isEmpty in FormControlMixin)
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
this.filled = false;
|
this.filled = false;
|
||||||
|
|
||||||
/** @type {string} */
|
/**
|
||||||
|
* True when user has attempted to submit the form, e.g. through a button
|
||||||
|
* of type="submit"
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
// TODO: [v1] this might be fixable by scheduling property effects till firstUpdated
|
||||||
|
// this.submitted = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event that triggers the touched state
|
||||||
|
* @type {string}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
this._leaveEvent = 'blur';
|
this._leaveEvent = 'blur';
|
||||||
/** @type {string} */
|
|
||||||
|
/**
|
||||||
|
* The event that triggers the dirty state
|
||||||
|
* @type {string}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
this._valueChangedEvent = 'model-value-changed';
|
this._valueChangedEvent = 'model-value-changed';
|
||||||
/** @type {EventHandlerNonNull} */
|
|
||||||
|
/**
|
||||||
|
* @type {EventHandlerNonNull}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
this._iStateOnLeave = this._iStateOnLeave.bind(this);
|
this._iStateOnLeave = this._iStateOnLeave.bind(this);
|
||||||
/** @type {EventHandlerNonNull} */
|
|
||||||
|
/**
|
||||||
|
* @type {EventHandlerNonNull}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
this._iStateOnValueChange = this._iStateOnValueChange.bind(this);
|
this._iStateOnValueChange = this._iStateOnValueChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,10 +133,9 @@ const InteractionStateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluations performed on connectedCallback. Since some components can be out of sync
|
* Evaluations performed on connectedCallback.
|
||||||
* (due to interdependence on light children that can only be processed
|
* This method is public, so it can be called at a later moment (when we need to wait for
|
||||||
* after connectedCallback and affect the initial value).
|
* registering children for instance) as well.
|
||||||
* This method is exposed, so it can be called after they are initialized themselves.
|
|
||||||
* Since this method will be called twice in last mentioned scenario, it must stay idempotent.
|
* Since this method will be called twice in last mentioned scenario, it must stay idempotent.
|
||||||
*/
|
*/
|
||||||
initInteractionState() {
|
initInteractionState() {
|
||||||
|
|
@ -130,8 +144,7 @@ const InteractionStateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets touched value to true
|
* Sets touched value to true and reevaluates prefilled state.
|
||||||
* Reevaluates prefilled state.
|
|
||||||
* When false, on next interaction, user will start with a clean state.
|
* When false, on next interaction, user will start with a clean state.
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
|
|
@ -159,22 +172,25 @@ const InteractionStateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches custom event on touched state change
|
* Dispatches event on touched state change
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_onTouchedChanged() {
|
_onTouchedChanged() {
|
||||||
this.dispatchEvent(new CustomEvent('touched-changed', { bubbles: true, composed: true }));
|
/** @protectedEvent touched-changed */
|
||||||
|
this.dispatchEvent(new Event('touched-changed', { bubbles: true, composed: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches custom event on touched state change
|
* Dispatches event on touched state change
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_onDirtyChanged() {
|
_onDirtyChanged() {
|
||||||
this.dispatchEvent(new CustomEvent('dirty-changed', { bubbles: true, composed: true }));
|
/** @protectedEvent dirty-changed */
|
||||||
|
this.dispatchEvent(new Event('dirty-changed', { bubbles: true, composed: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @override ValidateMixin
|
||||||
* Show the validity feedback when one of the following conditions is met:
|
* Show the validity feedback when one of the following conditions is met:
|
||||||
*
|
*
|
||||||
* - submitted
|
* - submitted
|
||||||
|
|
@ -199,6 +215,9 @@ const InteractionStateMixinImplementation = superclass =>
|
||||||
return (meta.touched && meta.dirty) || meta.prefilled || meta.submitted;
|
return (meta.touched && meta.dirty) || meta.prefilled || meta.submitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enhance ValidateMixin
|
||||||
|
*/
|
||||||
get _feedbackConditionMeta() {
|
get _feedbackConditionMeta() {
|
||||||
return {
|
return {
|
||||||
// @ts-ignore to fix, InteractionStateMixin needs to depend on ValidateMixin
|
// @ts-ignore to fix, InteractionStateMixin needs to depend on ValidateMixin
|
||||||
|
|
|
||||||
|
|
@ -25,26 +25,6 @@ import { InteractionStateMixin } from './InteractionStateMixin.js'; // applies F
|
||||||
export class LionField extends FormControlMixin(
|
export class LionField extends FormControlMixin(
|
||||||
InteractionStateMixin(FocusMixin(FormatMixin(ValidateMixin(SlotMixin(LitElement))))),
|
InteractionStateMixin(FocusMixin(FormatMixin(ValidateMixin(SlotMixin(LitElement))))),
|
||||||
) {
|
) {
|
||||||
/** @type {any} */
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
autocomplete: {
|
|
||||||
type: String,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.name = '';
|
|
||||||
/** @type {string | undefined} */
|
|
||||||
this.autocomplete = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@lion/core').PropertyValues } changedProperties
|
* @param {import('@lion/core').PropertyValues } changedProperties
|
||||||
*/
|
*/
|
||||||
|
|
@ -85,7 +65,8 @@ export class LionField extends FormControlMixin(
|
||||||
* Interaction states are not cleared (use resetInteractionState for this)
|
* Interaction states are not cleared (use resetInteractionState for this)
|
||||||
*/
|
*/
|
||||||
clear() {
|
clear() {
|
||||||
this.modelValue = ''; // can't set null here, because IE11 treats it as a string
|
// TODO: [v1] set to undefined
|
||||||
|
this.modelValue = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -93,11 +74,8 @@ export class LionField extends FormControlMixin(
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_onChange() {
|
_onChange() {
|
||||||
this.dispatchEvent(
|
/** @protectedEvent user-input-changed */
|
||||||
new CustomEvent('user-input-changed', {
|
this.dispatchEvent(new Event('user-input-changed', { bubbles: true }));
|
||||||
bubbles: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,23 @@ import { FormatMixin } from './FormatMixin.js';
|
||||||
*/
|
*/
|
||||||
const NativeTextFieldMixinImplementation = superclass =>
|
const NativeTextFieldMixinImplementation = superclass =>
|
||||||
class NativeTextFieldMixin extends FormatMixin(FocusMixin(FormControlMixin(superclass))) {
|
class NativeTextFieldMixin extends FormatMixin(FocusMixin(FormControlMixin(superclass))) {
|
||||||
|
/** @type {any} */
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
autocomplete: { type: String, reflect: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates this property to input/textarea/select.
|
||||||
|
* @type {string | undefined}
|
||||||
|
*/
|
||||||
|
this.autocomplete = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @protected
|
* @protected
|
||||||
* @type {HTMLInputElement | HTMLTextAreaElement}
|
* @type {HTMLInputElement | HTMLTextAreaElement}
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
multipleChoice: { type: Boolean, attribute: 'multiple-choice' },
|
||||||
* @desc When false (default), modelValue and serializedValue will reflect the
|
|
||||||
* currently selected choice (usually a string). When true, modelValue will and
|
|
||||||
* serializedValue will be an array of strings.
|
|
||||||
*/
|
|
||||||
multipleChoice: {
|
|
||||||
type: Boolean,
|
|
||||||
attribute: 'multiple-choice',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,11 +124,21 @@ const ChoiceGroupMixinImplementation = superclass =>
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When false (default), modelValue and serializedValue will reflect the
|
||||||
|
* currently selected choice (usually a string). When true, modelValue will and
|
||||||
|
* serializedValue will be an array of strings.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
this.multipleChoice = false;
|
this.multipleChoice = false;
|
||||||
/** @type {'child'|'choice-group'|'fieldset'}
|
|
||||||
|
/**
|
||||||
|
* @type {'child'|'choice-group'|'fieldset'}
|
||||||
|
* @configure FormControlMixin event propagation
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin
|
this._repropagationRole = 'choice-group';
|
||||||
/** @private */
|
/** @private */
|
||||||
this.__isInitialModelValue = true;
|
this.__isInitialModelValue = true;
|
||||||
/** @private */
|
/** @private */
|
||||||
|
|
@ -156,7 +158,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enhance FormRegistrarMixin
|
* @enhance FormRegistrarMixin: we need one extra microtask to complete
|
||||||
*/
|
*/
|
||||||
_completeRegistration() {
|
_completeRegistration() {
|
||||||
// Double microtask queue to account for Webkit race condition
|
// Double microtask queue to account for Webkit race condition
|
||||||
|
|
@ -175,7 +177,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override from FormRegistrarMixin
|
* @enhance FormRegistrarMixin
|
||||||
* @param {FormControl} child
|
* @param {FormControl} child
|
||||||
* @param {number} indexToInsertAt
|
* @param {number} indexToInsertAt
|
||||||
*/
|
*/
|
||||||
|
|
@ -353,10 +355,9 @@ const ChoiceGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Don't repropagate unchecked single choice choiceInputs
|
|
||||||
* @param {FormControlHost & ChoiceInputHost} target
|
* @param {FormControlHost & ChoiceInputHost} target
|
||||||
* @protected
|
* @protected
|
||||||
* @overridable
|
* @configure FormControlMixin: don't repropagate unchecked single choice choiceInputs
|
||||||
*/
|
*/
|
||||||
_repropagationCondition(target) {
|
_repropagationCondition(target) {
|
||||||
return !(
|
return !(
|
||||||
|
|
|
||||||
|
|
@ -25,42 +25,18 @@ const ChoiceInputMixinImplementation = superclass =>
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
checked: { type: Boolean, reflect: true },
|
||||||
* Boolean indicating whether or not this element is checked by the end user.
|
disabled: { type: Boolean, reflect: true },
|
||||||
*/
|
modelValue: { type: Object, hasChanged },
|
||||||
checked: {
|
choiceValue: { type: Object },
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Boolean indicating whether or not this element is disabled.
|
|
||||||
*/
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Whereas 'normal' `.modelValue`s usually store a complex/typed version
|
|
||||||
* of a view value, choice inputs have a slightly different approach.
|
|
||||||
* In order to remain their Single Source of Truth characteristic, choice inputs
|
|
||||||
* store both the value and 'checkedness', in the format { value: 'x', checked: true }
|
|
||||||
* Different from the platform, this also allows to serialize the 'non checkedness',
|
|
||||||
* allowing to restore form state easily and inform the server about unchecked options.
|
|
||||||
*/
|
|
||||||
modelValue: {
|
|
||||||
type: Object,
|
|
||||||
hasChanged,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* The value property of the modelValue. It provides an easy interface for storing
|
|
||||||
* (complex) values in the modelValue
|
|
||||||
*/
|
|
||||||
choiceValue: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value that will be registered to the modelValue of the parent ChoiceGroup. Recommended
|
||||||
|
* to be a string
|
||||||
|
* @type {string|any}
|
||||||
|
*/
|
||||||
get choiceValue() {
|
get choiceValue() {
|
||||||
return this.modelValue.value;
|
return this.modelValue.value;
|
||||||
}
|
}
|
||||||
|
|
@ -123,8 +99,33 @@ const ChoiceInputMixinImplementation = superclass =>
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
/**
|
||||||
|
* Boolean indicating whether or not this element is checked by the end user.
|
||||||
|
*/
|
||||||
|
// TODO: [v1] this can be solved when property effects are scheduled until firstUpdated
|
||||||
|
// this.checked = false;
|
||||||
|
/**
|
||||||
|
* Whereas 'normal' `.modelValue`s usually store a complex/typed version
|
||||||
|
* of a view value, choice inputs have a slightly different approach.
|
||||||
|
* In order to remain their Single Source of Truth characteristic, choice inputs
|
||||||
|
* store both the value and 'checkedness', in the format { value: 'x', checked: true }
|
||||||
|
* Different from the platform, this also allows to serialize the 'non checkedness',
|
||||||
|
* allowing to restore form state easily and inform the server about unchecked options.
|
||||||
|
* @type {{value:string|any,checked:boolean}}
|
||||||
|
*/
|
||||||
this.modelValue = { value: '', checked: false };
|
this.modelValue = { value: '', checked: false };
|
||||||
|
// TODO: maybe disabled is more a concern of FormControl/Field?
|
||||||
|
/**
|
||||||
|
* Boolean indicating whether or not this element is disabled.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
this.disabled = false;
|
this.disabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value property of the modelValue. It provides an easy interface for storing
|
||||||
|
* (complex) values in the modelValue
|
||||||
|
*/
|
||||||
|
|
||||||
/** @protected */
|
/** @protected */
|
||||||
this._preventDuplicateLabelClick = this._preventDuplicateLabelClick.bind(this);
|
this._preventDuplicateLabelClick = this._preventDuplicateLabelClick.bind(this);
|
||||||
/** @protected */
|
/** @protected */
|
||||||
|
|
|
||||||
|
|
@ -34,54 +34,25 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
submitted: { type: Boolean, reflect: true },
|
||||||
* Interaction state that can be used to compute the visibility of
|
focused: { type: Boolean, reflect: true },
|
||||||
* feedback messages
|
dirty: { type: Boolean, reflect: true },
|
||||||
*/
|
touched: { type: Boolean, reflect: true },
|
||||||
submitted: {
|
prefilled: { type: Boolean, reflect: true },
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Interaction state that will be active when any of the children
|
|
||||||
* is focused.
|
|
||||||
*/
|
|
||||||
focused: {
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Interaction state that will be active when any of the children
|
|
||||||
* is dirty (see InteractionStateMixin for more details.)
|
|
||||||
*/
|
|
||||||
dirty: {
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Interaction state that will be active when the group as a whole is
|
|
||||||
* blurred
|
|
||||||
*/
|
|
||||||
touched: {
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Interaction state that will be active when all of the children
|
|
||||||
* are prefilled (see InteractionStateMixin for more details.)
|
|
||||||
*/
|
|
||||||
prefilled: {
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @protected */
|
/**
|
||||||
|
* The host element with role group (or radigroup or form) containing neccessary aria attributes
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
get _inputNode() {
|
get _inputNode() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object keyed by formElements names, containing formElements' modelValues
|
||||||
|
*/
|
||||||
get modelValue() {
|
get modelValue() {
|
||||||
return this._getFromAllFormElements('modelValue');
|
return this._getFromAllFormElements('modelValue');
|
||||||
}
|
}
|
||||||
|
|
@ -97,6 +68,9 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object keyed by formElements names, containing formElements' serializedValues
|
||||||
|
*/
|
||||||
get serializedValue() {
|
get serializedValue() {
|
||||||
return this._getFromAllFormElements('serializedValue');
|
return this._getFromAllFormElements('serializedValue');
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +86,9 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object keyed by formElements names, containing formElements' formattedValues
|
||||||
|
*/
|
||||||
get formattedValue() {
|
get formattedValue() {
|
||||||
return this._getFromAllFormElements('formattedValue');
|
return this._getFromAllFormElements('formattedValue');
|
||||||
}
|
}
|
||||||
|
|
@ -120,24 +97,51 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
this._setValueMapForAllFormElements('formattedValue', values);
|
this._setValueMapForAllFormElements('formattedValue', values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when all of the children are prefilled (see InteractionStateMixin for more details.)
|
||||||
|
*/
|
||||||
get prefilled() {
|
get prefilled() {
|
||||||
return this._everyFormElementHas('prefilled');
|
return this._everyFormElementHas('prefilled');
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
// ._inputNode = this, which always requires a value prop
|
|
||||||
|
// ._inputNode === this, which always requires a value prop
|
||||||
this.value = '';
|
this.value = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables all formElements in group
|
||||||
|
*/
|
||||||
this.disabled = false;
|
this.disabled = false;
|
||||||
this.submitted = false;
|
|
||||||
this.dirty = false;
|
|
||||||
this.touched = false;
|
|
||||||
this.focused = false;
|
|
||||||
this.__addedSubValidators = false;
|
|
||||||
this.__isInitialModelValue = true;
|
|
||||||
this.__isInitialSerializedValue = true;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when parent form is submitted
|
||||||
|
*/
|
||||||
|
this.submitted = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when any of the children is dirty (see InteractionStateMixin for more details.)
|
||||||
|
*/
|
||||||
|
this.dirty = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the group as a whole is blurred (see InteractionStateMixin for more details.)
|
||||||
|
*/
|
||||||
|
this.touched = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when any of the children is focused.
|
||||||
|
*/
|
||||||
|
this.focused = false;
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
this.__addedSubValidators = false;
|
||||||
|
/** @private */
|
||||||
|
this.__isInitialModelValue = true;
|
||||||
|
/** @private */
|
||||||
|
this.__isInitialSerializedValue = true;
|
||||||
|
/** @private */
|
||||||
this._checkForOutsideClick = this._checkForOutsideClick.bind(this);
|
this._checkForOutsideClick = this._checkForOutsideClick.bind(this);
|
||||||
|
|
||||||
this.addEventListener('focusin', this._syncFocused);
|
this.addEventListener('focusin', this._syncFocused);
|
||||||
|
|
@ -255,7 +259,7 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc Handles interaction state 'submitted'.
|
* Handles interaction state 'submitted'.
|
||||||
* This allows children to enable visibility of validation feedback
|
* This allows children to enable visibility of validation feedback
|
||||||
*/
|
*/
|
||||||
submitGroup() {
|
submitGroup() {
|
||||||
|
|
@ -269,6 +273,9 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets to initial/prefilled values and interaction states of all FormControls in group,
|
||||||
|
*/
|
||||||
resetGroup() {
|
resetGroup() {
|
||||||
this.formElements.forEach(child => {
|
this.formElements.forEach(child => {
|
||||||
if (typeof child.resetGroup === 'function') {
|
if (typeof child.resetGroup === 'function') {
|
||||||
|
|
@ -281,6 +288,9 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
this.resetInteractionState();
|
this.resetInteractionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all values and resets all interaction states of all FormControls in group,
|
||||||
|
*/
|
||||||
clearGroup() {
|
clearGroup() {
|
||||||
this.formElements.forEach(child => {
|
this.formElements.forEach(child => {
|
||||||
if (typeof child.clearGroup === 'function') {
|
if (typeof child.clearGroup === 'function') {
|
||||||
|
|
@ -293,6 +303,9 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
this.resetInteractionState();
|
this.resetInteractionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all interaction states for all formElements
|
||||||
|
*/
|
||||||
resetInteractionState() {
|
resetInteractionState() {
|
||||||
this.submitted = false;
|
this.submitted = false;
|
||||||
this.touched = false;
|
this.touched = false;
|
||||||
|
|
@ -305,7 +318,9 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Gets a keyed be name object for requested property (like modelValue/serializedValue)
|
||||||
* @param {string} property
|
* @param {string} property
|
||||||
|
* @returns {{[name:string]: any}}
|
||||||
*/
|
*/
|
||||||
_getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) {
|
_getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) {
|
||||||
const result = {};
|
const result = {};
|
||||||
|
|
@ -326,6 +341,7 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Sets the same value for requested property in all formElements
|
||||||
* @param {string | number} property
|
* @param {string | number} property
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
*/
|
*/
|
||||||
|
|
@ -336,6 +352,7 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Allows to set formElements values via a keyed object structure
|
||||||
* @param {string} property
|
* @param {string} property
|
||||||
* @param {{ [x: string]: any; }} values
|
* @param {{ [x: string]: any; }} values
|
||||||
*/
|
*/
|
||||||
|
|
@ -360,6 +377,7 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Returns true when one of the formElements has requested
|
||||||
* @param {string} property
|
* @param {string} property
|
||||||
*/
|
*/
|
||||||
_anyFormElementHas(property) {
|
_anyFormElementHas(property) {
|
||||||
|
|
@ -389,6 +407,7 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Returns true when all of the formElements have requested property
|
||||||
* @param {string} property
|
* @param {string} property
|
||||||
*/
|
*/
|
||||||
_everyFormElementHas(property) {
|
_everyFormElementHas(property) {
|
||||||
|
|
@ -400,11 +419,13 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: the same functionality has been implemented with model-value-changed event, which
|
||||||
|
// covers the same and works with FormRegistrarPortalMixin
|
||||||
/**
|
/**
|
||||||
* Gets triggered by event 'validate-performed' which enabled us to handle 2 different situations
|
* Gets triggered by event 'validate-performed' which enabled us to handle 2 different situations
|
||||||
* - react on modelValue change, which says something about the validity as a whole
|
* - react on modelValue change, which says something about the validity as a whole
|
||||||
* (at least two checkboxes for instance) and nothing about the children's values
|
* (at least two checkboxes for instance) and nothing about the children's values
|
||||||
* - children validity states have changed, so fieldset needs to update itself based on that
|
* - children validity states have changed, so fieldset needs to update itself based on that
|
||||||
* @param {Event} ev
|
* @param {Event} ev
|
||||||
*/
|
*/
|
||||||
__onChildValidatePerformed(ev) {
|
__onChildValidatePerformed(ev) {
|
||||||
|
|
@ -441,6 +462,7 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
* In case one of the inputs was in error state as well, the SR user would
|
* In case one of the inputs was in error state as well, the SR user would
|
||||||
* first hear the local error, followed by #group-error
|
* first hear the local error, followed by #group-error
|
||||||
* @example
|
* @example
|
||||||
|
* ```html
|
||||||
* <lion-fieldset name="address">
|
* <lion-fieldset name="address">
|
||||||
* <lion-input name="street" label="Street" .modelValue="${'Park Avenue'}"></lion-input>
|
* <lion-input name="street" label="Street" .modelValue="${'Park Avenue'}"></lion-input>
|
||||||
* <lion-input name="number" label="Number" .modelValue="${100}">...</lion-input>
|
* <lion-input name="number" label="Number" .modelValue="${100}">...</lion-input>
|
||||||
|
|
@ -448,6 +470,7 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
* Park Avenue only has numbers up to 80
|
* Park Avenue only has numbers up to 80
|
||||||
* </div>
|
* </div>
|
||||||
* </lion-fieldset>
|
* </lion-fieldset>
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
__storeAllDescriptionElementsInParentChain() {
|
__storeAllDescriptionElementsInParentChain() {
|
||||||
const unTypedThis = /** @type {unknown} */ (this);
|
const unTypedThis = /** @type {unknown} */ (this);
|
||||||
|
|
@ -487,8 +510,7 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override of FormRegistrarMixin.
|
* @enhance FormRegistrarMixin: connects ValidateMixin and DisabledMixin.
|
||||||
* @desc Connects ValidateMixin and DisabledMixin
|
|
||||||
* On top of this, error messages of children are linked to their parents
|
* On top of this, error messages of children are linked to their parents
|
||||||
* @param {FormControl & {serializedValue:string|object}} child
|
* @param {FormControl & {serializedValue:string|object}} child
|
||||||
* @param {number} indexToInsertAt
|
* @param {number} indexToInsertAt
|
||||||
|
|
@ -520,15 +542,14 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gathers initial model values of all children. Used
|
* Gathers initial model values of all children. Used when resetGroup() is called.
|
||||||
* when resetGroup() is called.
|
|
||||||
*/
|
*/
|
||||||
get _initialModelValue() {
|
get _initialModelValue() {
|
||||||
return this._getFromAllFormElements('_initialModelValue');
|
return this._getFromAllFormElements('_initialModelValue');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override of FormRegistrarMixin. Connects ValidateMixin
|
* @override FormRegistrarMixin; Connects ValidateMixin
|
||||||
* @param {FormRegisteringHost & FormControl} el
|
* @param {FormRegisteringHost & FormControl} el
|
||||||
*/
|
*/
|
||||||
removeFormElement(el) {
|
removeFormElement(el) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc This class closely mimics the natively
|
* This class closely mimics the natively
|
||||||
* supported HTMLFormControlsCollection. It can be accessed
|
* supported HTMLFormControlsCollection. It can be accessed
|
||||||
* both like an array and an object (based on control/element names).
|
* both like an array and an object (based on control/element names).
|
||||||
* @example
|
* @example
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,11 @@ const FormRegisteringMixinImplementation = superclass =>
|
||||||
class extends superclass {
|
class extends superclass {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
/** @type {FormRegistrarHost | undefined} */
|
/**
|
||||||
|
* The registrar this FormControl registers to, Usually a descendant of FormGroup or
|
||||||
|
* ChoiceGroup
|
||||||
|
* @type {FormRegistrarHost | undefined}
|
||||||
|
*/
|
||||||
this._parentFormGroup = undefined;
|
this._parentFormGroup = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,23 +29,31 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
|
||||||
* @desc Flag that determines how ".formElements" should behave.
|
|
||||||
* For a regular fieldset (see LionFieldset) we expect ".formElements"
|
|
||||||
* to be accessible as an object.
|
|
||||||
* In case of a radio-group, a checkbox-group or a select/listbox,
|
|
||||||
* it should act like an array (see ChoiceGroupMixin).
|
|
||||||
* Usually, when false, we deal with a choice-group (radio-group, checkbox-group,
|
|
||||||
* (multi)select)
|
|
||||||
*/
|
|
||||||
_isFormOrFieldset: { type: Boolean },
|
_isFormOrFieldset: { type: Boolean },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closely mimics the natively supported HTMLFormControlsCollection. It can be accessed
|
||||||
|
* both like an array and an object (based on control/element names).
|
||||||
|
* @type {FormControlsCollection}
|
||||||
|
*/
|
||||||
this.formElements = new FormControlsCollection();
|
this.formElements = new FormControlsCollection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag that determines how ".formElements" should behave.
|
||||||
|
* For a regular fieldset (see LionFieldset) we expect ".formElements"
|
||||||
|
* to be accessible as an object.
|
||||||
|
* In case of a radio-group, a checkbox-group or a select/listbox,
|
||||||
|
* it should act like an array (see ChoiceGroupMixin).
|
||||||
|
* Usually, when false, we deal with a choice-group (radio-group, checkbox-group,
|
||||||
|
* (multi)select)
|
||||||
|
* @type {boolean}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
this._isFormOrFieldset = false;
|
this._isFormOrFieldset = false;
|
||||||
|
|
||||||
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
||||||
|
|
@ -100,6 +108,10 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
this._completeRegistration();
|
this._completeRegistration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the registrationComplete promise. Subclassers can delay if needed
|
||||||
|
* @overridable
|
||||||
|
*/
|
||||||
_completeRegistration() {
|
_completeRegistration() {
|
||||||
Promise.resolve().then(() => this.__resolveRegistrationComplete(undefined));
|
Promise.resolve().then(() => this.__resolveRegistrationComplete(undefined));
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +209,7 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Hook for Subclassers to perform logic before an element is added
|
||||||
* @param {CustomEvent} ev
|
* @param {CustomEvent} ev
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,12 @@ const FormRegistrarPortalMixinImplementation = superclass =>
|
||||||
class extends superclass {
|
class extends superclass {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
/** @type {(FormRegistrarPortalHost & HTMLElement) | undefined} */
|
|
||||||
|
/**
|
||||||
|
* Registration target: an element, usually in the body of the dom, that captures events
|
||||||
|
* and redispatches them on host
|
||||||
|
* @type {(FormRegistrarPortalHost & HTMLElement) | undefined}
|
||||||
|
*/
|
||||||
this.registrationTarget = undefined;
|
this.registrationTarget = undefined;
|
||||||
this.__redispatchEventForFormRegistrarPortalMixin = this.__redispatchEventForFormRegistrarPortalMixin.bind(
|
this.__redispatchEventForFormRegistrarPortalMixin = this.__redispatchEventForFormRegistrarPortalMixin.bind(
|
||||||
this,
|
this,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { dedupeMixin } from '@lion/core';
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc Why this mixin?
|
* Why this mixin?
|
||||||
* - it adheres to the "Member Order Independence" web components standard:
|
* - it adheres to the "Member Order Independence" web components standard:
|
||||||
* https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence
|
* https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence
|
||||||
* - sync observers can be dependent on the outcome of the render function (or, more generically
|
* - sync observers can be dependent on the outcome of the render function (or, more generically
|
||||||
|
|
@ -27,8 +27,9 @@ const SyncUpdatableMixinImplementation = superclass =>
|
||||||
class extends superclass {
|
class extends superclass {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
// Namespace for this mixin that guarantees naming clashes will not occur...
|
|
||||||
/**
|
/**
|
||||||
|
* Namespace for this mixin that guarantees naming clashes will not occur...
|
||||||
* @type {SyncUpdatableNamespace}
|
* @type {SyncUpdatableNamespace}
|
||||||
*/
|
*/
|
||||||
this.__SyncUpdatableNamespace = {};
|
this.__SyncUpdatableNamespace = {};
|
||||||
|
|
@ -113,7 +114,14 @@ const SyncUpdatableMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc A public abstraction that has the exact same api as `requestUpdateInternal`.
|
* An abstraction that has the exact same api as `requestUpdateInternal`, but taking
|
||||||
|
* into account:
|
||||||
|
* - [member order independence](https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence)
|
||||||
|
* - property effects start when all (light) dom has initialized (on firstUpdated)
|
||||||
|
* - property effects don't interrupt the first meaningful paint
|
||||||
|
* - compatible with propertyAccessor.`hasChanged`: no manual checks needed or accidentally
|
||||||
|
* run property effects / events when no change happened
|
||||||
|
* effects when values didn't change
|
||||||
* All code previously present in requestUpdateInternal can be placed in this method.
|
* All code previously present in requestUpdateInternal can be placed in this method.
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {*} oldValue
|
* @param {*} oldValue
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ export class LionValidationFeedback extends LitElement {
|
||||||
this.setAttribute('type', this.feedbackData[0].type);
|
this.setAttribute('type', this.feedbackData[0].type);
|
||||||
this.currentType = this.feedbackData[0].type;
|
this.currentType = this.feedbackData[0].type;
|
||||||
window.clearTimeout(this.removeMessage);
|
window.clearTimeout(this.removeMessage);
|
||||||
|
// TODO: this logic should be in ValidateMixin, so that [show-feedback-for] is in sync,
|
||||||
|
// plus duration should be configurable
|
||||||
if (this.currentType === 'success') {
|
if (this.currentType === 'success') {
|
||||||
this.removeMessage = window.setTimeout(() => {
|
this.removeMessage = window.setTimeout(() => {
|
||||||
this.removeAttribute('type');
|
this.removeAttribute('type');
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,16 @@
|
||||||
export class Unparseable {
|
export class Unparseable {
|
||||||
/** @param {string} value */
|
/** @param {string} value */
|
||||||
constructor(value) {
|
constructor(value) {
|
||||||
|
/**
|
||||||
|
* Meta info for restoring serialized Unparseable values
|
||||||
|
* @type {'unparseable'}
|
||||||
|
*/
|
||||||
this.type = 'unparseable';
|
this.type = 'unparseable';
|
||||||
|
/**
|
||||||
|
* Stores current view value. For instance, value '09-' is an unparseable Date.
|
||||||
|
* This info can be used to restore previous form states.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
this.viewValue = value;
|
this.viewValue = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ import { Validator } from './Validator.js';
|
||||||
import { Required } from './validators/Required.js';
|
import { Required } from './validators/Required.js';
|
||||||
import { FormControlMixin } from '../FormControlMixin.js';
|
import { FormControlMixin } from '../FormControlMixin.js';
|
||||||
|
|
||||||
|
// TODO: [v1] make all @readOnly => @readonly and actually make sure those values cannot be set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidateMixin} ValidateMixin
|
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidateMixin} ValidateMixin
|
||||||
|
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidationType} ValidationType
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -25,7 +28,7 @@ function arrayDiff(array1 = [], array2 = []) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc Handles all validation, based on modelValue changes. It has no knowledge about dom and
|
* Handles all validation, based on modelValue changes. It has no knowledge about dom and
|
||||||
* UI. All error visibility, dom interaction and accessibility are handled in FeedbackMixin.
|
* UI. All error visibility, dom interaction and accessibility are handled in FeedbackMixin.
|
||||||
*
|
*
|
||||||
* @type {ValidateMixin}
|
* @type {ValidateMixin}
|
||||||
|
|
@ -48,11 +51,8 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
validators: { attribute: false },
|
validators: { attribute: false },
|
||||||
|
|
||||||
hasFeedbackFor: { attribute: false },
|
hasFeedbackFor: { attribute: false },
|
||||||
|
|
||||||
shouldShowFeedbackFor: { attribute: false },
|
shouldShowFeedbackFor: { attribute: false },
|
||||||
|
|
||||||
showsFeedbackFor: {
|
showsFeedbackFor: {
|
||||||
type: Array,
|
type: Array,
|
||||||
attribute: 'shows-feedback-for',
|
attribute: 'shows-feedback-for',
|
||||||
|
|
@ -62,36 +62,22 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
toAttribute: /** @param {[]} value */ value => value.join(','),
|
toAttribute: /** @param {[]} value */ value => value.join(','),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
validationStates: { attribute: false },
|
validationStates: { attribute: false },
|
||||||
|
|
||||||
/**
|
|
||||||
* @desc flag that indicates whether async validation is pending
|
|
||||||
*/
|
|
||||||
isPending: {
|
isPending: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
attribute: 'is-pending',
|
attribute: 'is-pending',
|
||||||
reflect: true,
|
reflect: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* @desc specialized fields (think of input-date and input-email) can have preconfigured
|
|
||||||
* validators.
|
|
||||||
*/
|
|
||||||
defaultValidators: { attribute: false },
|
defaultValidators: { attribute: false },
|
||||||
|
|
||||||
/**
|
|
||||||
* Subclassers can enable this to show multiple feedback messages at the same time
|
|
||||||
* By default, just like the platform, only one message (with highest prio) is visible.
|
|
||||||
*/
|
|
||||||
_visibleMessagesAmount: { attribute: false },
|
_visibleMessagesAmount: { attribute: false },
|
||||||
|
|
||||||
__childModelValueChanged: { attribute: false },
|
__childModelValueChanged: { attribute: false },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Types of validation supported by this FormControl (for instance 'error'|'warning'|'info')
|
||||||
* @overridable
|
* @overridable
|
||||||
|
* @type {ValidationType[]}
|
||||||
*/
|
*/
|
||||||
static get validationTypes() {
|
static get validationTypes() {
|
||||||
return ['error'];
|
return ['error'];
|
||||||
|
|
@ -124,6 +110,11 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combination of validators provided by Application Developer and the default validators
|
||||||
|
* @type {Validator[]}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
get _allValidators() {
|
get _allValidators() {
|
||||||
return [...this.validators, ...this.defaultValidators];
|
return [...this.validators, ...this.defaultValidators];
|
||||||
}
|
}
|
||||||
|
|
@ -131,28 +122,93 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
/** @type {string[]} */
|
/**
|
||||||
|
* As soon as validation happens (after modelValue/validators/validator param change), this
|
||||||
|
* array is updated with the active ValidationTypes ('error'|'warning'|'success'|'info' etc.).
|
||||||
|
* Notice the difference with `.showsFeedbackFor`, which filters `.hasFeedbackFor` based on
|
||||||
|
* `.feedbackCondition()`.
|
||||||
|
*
|
||||||
|
* For styling purposes, will be reflected to [has-feedback-for="error warning"]. This can
|
||||||
|
* be useful for subtle visual feedback on keyup, like a red/green border around an input.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```css
|
||||||
|
* :host([has-feedback-for~="error"]) .input-group__container {
|
||||||
|
* border: 1px solid red;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* @type {ValidationType[]}
|
||||||
|
* @readOnly
|
||||||
|
*/
|
||||||
this.hasFeedbackFor = [];
|
this.hasFeedbackFor = [];
|
||||||
|
|
||||||
/** @type {string[]} */
|
/**
|
||||||
this.shouldShowFeedbackFor = [];
|
* Based on outcome of feedbackCondition, this array decides what ValidationTypes should be
|
||||||
|
* shown in validationFeedback, based on meta data like interaction states.
|
||||||
/** @type {string[]} */
|
*
|
||||||
|
* For styling purposes, it reflects it `[shows-feedback-for="error warning"]`
|
||||||
|
* @type {ValidationType[]}
|
||||||
|
* @readOnly
|
||||||
|
* @example
|
||||||
|
* ```css
|
||||||
|
* :host([shows-feedback-for~="success"]) .form-field__feedback {
|
||||||
|
* transform: scaleY(1);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
this.showsFeedbackFor = [];
|
this.showsFeedbackFor = [];
|
||||||
|
|
||||||
/** @type {Object.<string, Object.<string, boolean>>} */
|
// TODO: [v1] make this fully private (preifix __)?
|
||||||
|
/**
|
||||||
|
* A temporary storage to transition from hasFeedbackFor to showsFeedbackFor
|
||||||
|
* @type {ValidationType[]}
|
||||||
|
* @readOnly
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.shouldShowFeedbackFor = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The outcome of a validation 'round'. Keyed by ValidationType and Validator name
|
||||||
|
* @readOnly
|
||||||
|
* @type {Object.<string, Object.<string, boolean>>}
|
||||||
|
*/
|
||||||
this.validationStates = {};
|
this.validationStates = {};
|
||||||
|
|
||||||
/** @protected */
|
/**
|
||||||
this._visibleMessagesAmount = 1;
|
* Flag indicating whether async validation is pending.
|
||||||
|
* Creates attribute [is-pending] as a styling hook
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
this.isPending = false;
|
this.isPending = false;
|
||||||
|
|
||||||
/** @type {Validator[]} */
|
/**
|
||||||
|
* Used by Application Developers to add Validators to a FormControl.
|
||||||
|
* @example
|
||||||
|
* ```html
|
||||||
|
* <form-control .validators="${[new Required(), new MinLength(4, {type: 'warning'})]}">
|
||||||
|
* </form-control>
|
||||||
|
* ```
|
||||||
|
* @type {Validator[]}
|
||||||
|
*/
|
||||||
this.validators = [];
|
this.validators = [];
|
||||||
/** @type {Validator[]} */
|
|
||||||
|
/**
|
||||||
|
* Used by Subclassers to add default Validators to a particular FormControl.
|
||||||
|
* A date input for instance, always needs the isDate validator.
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* this.defaultValidators.push(new IsDate());
|
||||||
|
* ```
|
||||||
|
* @type {Validator[]}
|
||||||
|
*/
|
||||||
this.defaultValidators = [];
|
this.defaultValidators = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of feedback messages that will visible in LionValidationFeedback
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
this._visibleMessagesAmount = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Validator[]}
|
* @type {Validator[]}
|
||||||
* @private
|
* @private
|
||||||
|
|
@ -166,29 +222,35 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
this.__asyncValidationResult = [];
|
this.__asyncValidationResult = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc contains results from sync Validators, async Validators and ResultValidators
|
* Aggregated result from sync Validators, async Validators and ResultValidators
|
||||||
* @type {Validator[]}
|
* @type {Validator[]}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.__validationResult = [];
|
this.__validationResult = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Validator[]}
|
* @type {Validator[]}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.__prevValidationResult = [];
|
this.__prevValidationResult = [];
|
||||||
/** @type {Validator[]} */
|
|
||||||
|
/**
|
||||||
|
* @type {Validator[]}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
this.__prevShownValidationResult = [];
|
this.__prevShownValidationResult = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated children validity affects the validity of the parent. Helper to recompute
|
||||||
|
* validatity of parent FormGroup
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.__childModelValueChanged = false;
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
this.__onValidatorUpdated = this.__onValidatorUpdated.bind(this);
|
this.__onValidatorUpdated = this.__onValidatorUpdated.bind(this);
|
||||||
/** @protected */
|
/** @protected */
|
||||||
this._updateFeedbackComponent = this._updateFeedbackComponent.bind(this);
|
this._updateFeedbackComponent = this._updateFeedbackComponent.bind(this);
|
||||||
|
|
||||||
/**
|
|
||||||
* This will be used for FormGroups that listen for `model-value-changed` of children
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this.__childModelValueChanged = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
|
@ -271,28 +333,28 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc The main function of this mixin. Triggered by:
|
* Triggered by:
|
||||||
* - a modelValue change
|
* - modelValue change
|
||||||
* - a change in the 'validators' array
|
* - change in the 'validators' array
|
||||||
* - a change in the config of an individual Validator
|
* - change in the config of an individual Validator
|
||||||
*
|
*
|
||||||
* Three situations are handled:
|
* Three situations are handled:
|
||||||
* - A.1 The FormControl is empty: further execution is halted. When the Required Validator
|
* - a1) the FormControl is empty: further execution is halted. When the Required Validator
|
||||||
* (being mutually exclusive to the other Validators) is applied, it will end up in the
|
* (being mutually exclusive to the other Validators) is applied, it will end up in the
|
||||||
* validation result (as the only Validator, since further execution was halted).
|
* validation result (as the only Validator, since further execution was halted).
|
||||||
* - A.2 There are synchronous Validators: this is the most common flow. When modelValue hasn't
|
* - a2) there are synchronous Validators: this is the most common flow. When modelValue hasn't
|
||||||
* changed since last async results were generated, 'sync results' are merged with the
|
* changed since last async results were generated, 'sync results' are merged with the
|
||||||
* 'async results'.
|
* 'async results'.
|
||||||
* - A.3 There are asynchronous Validators: for instance when server side evaluation is needed.
|
* - a3) there are asynchronous Validators: for instance when server side evaluation is needed.
|
||||||
* Executions are scheduled and awaited and the 'async results' are merged with the
|
* Executions are scheduled and awaited and the 'async results' are merged with the
|
||||||
* 'sync results'.
|
* 'sync results'.
|
||||||
*
|
*
|
||||||
* - B. There are ResultValidators. After steps A.1, A.2, or A.3 are finished, the holistic
|
* - b) there are ResultValidators. After steps a1, a2, or a3 are finished, the holistic
|
||||||
* ResultValidators (evaluating the total result of the 'regular' (A.1, A.2 and A.3) validators)
|
* ResultValidators (evaluating the total result of the 'regular' (a1, a2 and a3) validators)
|
||||||
* will be run...
|
* will be run...
|
||||||
*
|
*
|
||||||
* Situations A.2 and A.3 are not mutually exclusive and can be triggered within one validate()
|
* Situations a2 and a3 are not mutually exclusive and can be triggered within one `validate()`
|
||||||
* call. Situation B will occur after every call.
|
* call. Situation b will occur after every call.
|
||||||
*
|
*
|
||||||
* @param {{ clearCurrentResult?: boolean }} [opts]
|
* @param {{ clearCurrentResult?: boolean }} [opts]
|
||||||
*/
|
*/
|
||||||
|
|
@ -318,7 +380,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc step A1-3 + B (as explained in 'validate')
|
* @desc step a1-3 + b (as explained in `validate()`)
|
||||||
*/
|
*/
|
||||||
async __executeValidators() {
|
async __executeValidators() {
|
||||||
this.validateComplete = new Promise(resolve => {
|
this.validateComplete = new Promise(resolve => {
|
||||||
|
|
@ -380,7 +442,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc step A2, calls __finishValidation
|
* step a2 (as explained in `validate()`): calls `__finishValidation`
|
||||||
* @param {Validator[]} syncValidators
|
* @param {Validator[]} syncValidators
|
||||||
* @param {unknown} value
|
* @param {unknown} value
|
||||||
* @param {{ hasAsync: boolean }} opts
|
* @param {{ hasAsync: boolean }} opts
|
||||||
|
|
@ -396,7 +458,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc step A3, calls __finishValidation
|
* step a3 (as explained in `validate()`), calls __finishValidation
|
||||||
* @param {Validator[]} asyncValidators all Validators except required and ResultValidators
|
* @param {Validator[]} asyncValidators all Validators except required and ResultValidators
|
||||||
* @param {?} value
|
* @param {?} value
|
||||||
* @private
|
* @private
|
||||||
|
|
@ -415,7 +477,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc step B, called by __finishValidation
|
* step b (as explained in `validate()`), called by __finishValidation
|
||||||
* @param {Validator[]} regularValidationResult result of steps 1-3
|
* @param {Validator[]} regularValidationResult result of steps 1-3
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
@ -541,6 +603,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Helper method for the mutually exclusive Required Validator
|
||||||
* @param {?} v
|
* @param {?} v
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
@ -593,7 +656,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc Responsible for retrieving messages from Validators and
|
* Responsible for retrieving messages from Validators and
|
||||||
* (delegation of) rendering them.
|
* (delegation of) rendering them.
|
||||||
*
|
*
|
||||||
* For `._feedbackNode` (extension of LionValidationFeedback):
|
* For `._feedbackNode` (extension of LionValidationFeedback):
|
||||||
|
|
@ -640,8 +703,8 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default feedbackCondition condition that will be used when the
|
* Default feedbackCondition condition, used by Subclassers, that will be used when
|
||||||
* feedbackCondition is not overridden.
|
* `feedbackCondition()` is not overridden by Application Developer.
|
||||||
* Show the validity feedback when returning true, don't show when false
|
* Show the validity feedback when returning true, don't show when false
|
||||||
* @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom
|
* @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom
|
||||||
* Validator type
|
* Validator type
|
||||||
|
|
@ -654,7 +717,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows super classes to add meta info for feedbackCondition
|
* Allows Subclassers to add meta info for feedbackCondition
|
||||||
* @configurable
|
* @configurable
|
||||||
*/
|
*/
|
||||||
get _feedbackConditionMeta() {
|
get _feedbackConditionMeta() {
|
||||||
|
|
@ -664,6 +727,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
/**
|
/**
|
||||||
* Allows the end user to specify when a feedback message should be shown
|
* Allows the end user to specify when a feedback message should be shown
|
||||||
* @example
|
* @example
|
||||||
|
* ```js
|
||||||
* feedbackCondition(type, meta, defaultCondition) {
|
* feedbackCondition(type, meta, defaultCondition) {
|
||||||
* if (type === 'info') {
|
* if (type === 'info') {
|
||||||
* return return;
|
* return return;
|
||||||
|
|
@ -672,6 +736,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
* }
|
* }
|
||||||
* return defaultCondition(type, meta);
|
* return defaultCondition(type, meta);
|
||||||
* }
|
* }
|
||||||
|
* ```
|
||||||
* @overridable
|
* @overridable
|
||||||
* @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom
|
* @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom
|
||||||
* Validator type
|
* Validator type
|
||||||
|
|
@ -690,6 +755,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Used to translate `.hasFeedbackFor` and `.shouldShowFeedbackFor` to `.showsFeedbackFor`
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
|
|
@ -767,13 +833,12 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @overridable
|
* Orders all active validators in this.__validationResult. Can
|
||||||
* @desc Orders all active validators in this.__validationResult. Can
|
|
||||||
* also filter out occurrences (based on interaction states)
|
* also filter out occurrences (based on interaction states)
|
||||||
|
* @overridable
|
||||||
* @param {{ validationResult: Validator[] }} opts
|
* @param {{ validationResult: Validator[] }} opts
|
||||||
* @return {Validator[]} ordered list of Validators with feedback messages visible to the
|
* @return {Validator[]} ordered list of Validators with feedback messages visible to the end user
|
||||||
* @protected
|
* @protected
|
||||||
* end user
|
|
||||||
*/
|
*/
|
||||||
_prioritizeAndFilterFeedback({ validationResult }) {
|
_prioritizeAndFilterFeedback({ validationResult }) {
|
||||||
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this
|
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this
|
||||||
|
|
|
||||||
|
|
@ -216,9 +216,11 @@ export function runInteractionStateMixinSuite(customConfig) {
|
||||||
const el = /** @type {IState} */ (await fixture(html`
|
const el = /** @type {IState} */ (await fixture(html`
|
||||||
<${tag}></${tag}>
|
<${tag}></${tag}>
|
||||||
`));
|
`));
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
expect(el.shouldShowFeedbackFor).to.deep.equal([]);
|
expect(el.shouldShowFeedbackFor).to.deep.equal([]);
|
||||||
el.submitted = true;
|
el.submitted = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
expect(el.shouldShowFeedbackFor).to.deep.equal(['error']);
|
expect(el.shouldShowFeedbackFor).to.deep.equal(['error']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -570,7 +570,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
const button = /** @type {HTMLButtonElement} */ (await fixture(`<button>Blur</button>`));
|
const button = /** @type {HTMLButtonElement} */ (await fixture(`<button>Blur</button>`));
|
||||||
|
|
||||||
expect(el.touched).to.equal(false, 'initially, touched state is false');
|
expect(el.touched).to.equal(false, 'initially, touched state is false');
|
||||||
el.children[2].focus();
|
el.formElements[1].focus();
|
||||||
expect(el.touched).to.equal(false, 'focus is on second checkbox');
|
expect(el.touched).to.equal(false, 'focus is on second checkbox');
|
||||||
button.focus();
|
button.focus();
|
||||||
expect(el.touched).to.equal(
|
expect(el.touched).to.equal(
|
||||||
|
|
@ -602,8 +602,8 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
outside.click();
|
outside.click();
|
||||||
expect(el.touched, 'unfocused fieldset should stay untouched').to.be.false;
|
expect(el.touched, 'unfocused fieldset should stay untouched').to.be.false;
|
||||||
|
|
||||||
el.children[1].focus();
|
el.formElements[0].focus();
|
||||||
el.children[2].focus();
|
el.formElements[1].focus();
|
||||||
expect(el.touched).to.be.false;
|
expect(el.touched).to.be.false;
|
||||||
|
|
||||||
outside.click(); // blur the group via a click
|
outside.click(); // blur the group via a click
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,19 @@ export declare class FocusHost {
|
||||||
* Reflects to attribute '[focused]' as a styling hook
|
* Reflects to attribute '[focused]' as a styling hook
|
||||||
*/
|
*/
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the focusable element within (`._focusableNode`) matches ':focus-visible'
|
* Whether the focusable element within (`._focusableNode`) matches ':focus-visible'
|
||||||
* Reflects to attribute '[focused-visible]' as a styling hook
|
* Reflects to attribute '[focused-visible]' as a styling hook
|
||||||
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible
|
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible
|
||||||
*/
|
*/
|
||||||
focusedVisible: boolean;
|
focusedVisible: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls `focus()` on focusable element within
|
* Calls `focus()` on focusable element within
|
||||||
*/
|
*/
|
||||||
focus(): void;
|
focus(): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls `blur()` on focusable element within
|
* Calls `blur()` on focusable element within
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export type ModelValueEventDetails = {
|
||||||
* itself to the beginning of formPath)
|
* itself to the beginning of formPath)
|
||||||
*/
|
*/
|
||||||
formPath: HTMLElement[];
|
formPath: HTMLElement[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sometimes it can be helpful to detect whether a value change was caused by a user or
|
* Sometimes it can be helpful to detect whether a value change was caused by a user or
|
||||||
* via a programmatical change.
|
* via a programmatical change.
|
||||||
|
|
@ -24,6 +25,7 @@ export type ModelValueEventDetails = {
|
||||||
* like 'input'/'change'/'user-input-changed' etc.)
|
* like 'input'/'change'/'user-input-changed' etc.)
|
||||||
*/
|
*/
|
||||||
isTriggeredByUser: boolean;
|
isTriggeredByUser: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether it is the first event sent on initialization of the form (other
|
* Whether it is the first event sent on initialization of the form (other
|
||||||
* model-value-changed events are triggered imperatively or via user input (in the latter
|
* model-value-changed events are triggered imperatively or via user input (in the latter
|
||||||
|
|
@ -59,19 +61,23 @@ export declare class FormControlHost {
|
||||||
_repropagationRole: { attribute: boolean };
|
_repropagationRole: { attribute: boolean };
|
||||||
_isRepropagationEndpoint: { attribute: boolean };
|
_isRepropagationEndpoint: { attribute: boolean };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Boolean attribute which, if present, indicates that the user should not be able to edit
|
* A boolean attribute which, if present, indicates that the user should not be able to edit
|
||||||
* the value of the input. The difference between disabled and readonly is that read-only
|
* the value of the input. The difference between disabled and readonly is that read-only
|
||||||
* controls can still function, whereas disabled controls generally do not function as
|
* controls can still function, whereas disabled controls generally do not function as
|
||||||
* controls until they are enabled.
|
* controls until they are enabled.
|
||||||
* (From: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly)
|
* See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly
|
||||||
*/
|
*/
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name the element will be registered with to the .formElements collection
|
* The name the element will be registered with to the .formElements collection
|
||||||
* of the parent.
|
* of the parent. Also, it serves as the key of key/value pairs in
|
||||||
|
* modelValue/serializedValue objects
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
|
@ -86,12 +92,14 @@ export declare class FormControlHost {
|
||||||
*/
|
*/
|
||||||
get modelValue(): any | Unparseable;
|
get modelValue(): any | Unparseable;
|
||||||
set modelValue(value: any | Unparseable);
|
set modelValue(value: any | Unparseable);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The label text for the input node.
|
* The label text for the input node.
|
||||||
* When no light dom defined via [slot=label], this value will be used
|
* When no light dom defined via [slot=label], this value will be used
|
||||||
*/
|
*/
|
||||||
get label(): string;
|
get label(): string;
|
||||||
set label(arg: string);
|
set label(arg: string);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The helpt text for the input node.
|
* The helpt text for the input node.
|
||||||
* When no light dom defined via [slot=help-text], this value will be used
|
* When no light dom defined via [slot=help-text], this value will be used
|
||||||
|
|
@ -99,9 +107,15 @@ export declare class FormControlHost {
|
||||||
get helpText(): string;
|
get helpText(): string;
|
||||||
set helpText(arg: string);
|
set helpText(arg: string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will be used in validation messages to refer to the current field
|
||||||
|
*/
|
||||||
set fieldName(arg: string);
|
set fieldName(arg: string);
|
||||||
get fieldName(): string;
|
get fieldName(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to add extra element references to aria-labelledby attribute.
|
||||||
|
*/
|
||||||
addToAriaLabelledBy(
|
addToAriaLabelledBy(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
customConfig?: {
|
customConfig?: {
|
||||||
|
|
@ -109,6 +123,10 @@ export declare class FormControlHost {
|
||||||
reorder?: boolean | undefined;
|
reorder?: boolean | undefined;
|
||||||
},
|
},
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to add extra element references to aria-describedby attribute.
|
||||||
|
*/
|
||||||
addToAriaDescribedBy(
|
addToAriaDescribedBy(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
customConfig?: {
|
customConfig?: {
|
||||||
|
|
@ -116,31 +134,71 @@ export declare class FormControlHost {
|
||||||
reorder?: boolean | undefined;
|
reorder?: boolean | undefined;
|
||||||
},
|
},
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to remove element references from aria-labelledby attribute.
|
||||||
|
*/
|
||||||
removeFromAriaLabelledBy(
|
removeFromAriaLabelledBy(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
customConfig?: {
|
customConfig?: {
|
||||||
reorder?: boolean | undefined;
|
reorder?: boolean | undefined;
|
||||||
},
|
},
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to remove element references from aria-describedby attribute.
|
||||||
|
*/
|
||||||
removeFromAriaDescribedBy(
|
removeFromAriaDescribedBy(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
customConfig?: {
|
customConfig?: {
|
||||||
reorder?: boolean | undefined;
|
reorder?: boolean | undefined;
|
||||||
},
|
},
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
updated(changedProperties: import('@lion/core').PropertyValues): void;
|
updated(changedProperties: import('@lion/core').PropertyValues): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interactive (form) element. Can be a native element like input/textarea/select or
|
||||||
|
* an element with tabindex > -1
|
||||||
|
*/
|
||||||
protected get _inputNode(): HTMLElementWithValue | HTMLInputElement | HTMLTextAreaElement;
|
protected get _inputNode(): HTMLElementWithValue | HTMLInputElement | HTMLTextAreaElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element where label will be rendered to
|
||||||
|
*/
|
||||||
protected get _labelNode(): HTMLElement;
|
protected get _labelNode(): HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element where help text will be rendered to
|
||||||
|
*/
|
||||||
protected get _helpTextNode(): HTMLElement;
|
protected get _helpTextNode(): HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element where validation feedback will be rendered to
|
||||||
|
*/
|
||||||
protected get _feedbackNode(): LionValidationFeedback;
|
protected get _feedbackNode(): LionValidationFeedback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique id that can be used in all light dom
|
||||||
|
*/
|
||||||
protected _inputId: string;
|
protected _inputId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all elements that should end up in aria-labelledby of `._inputNode`
|
||||||
|
* @type {HTMLElement[]}
|
||||||
|
*/
|
||||||
protected _ariaLabelledNodes: HTMLElement[];
|
protected _ariaLabelledNodes: HTMLElement[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all elements that should end up in aria-describedby of `._inputNode`
|
||||||
|
*/
|
||||||
protected _ariaDescribedNodes: HTMLElement[];
|
protected _ariaDescribedNodes: HTMLElement[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on the role, details of handling model-value-changed repropagation differ.
|
* Based on the role, details of handling model-value-changed repropagation differ.
|
||||||
*/
|
*/
|
||||||
protected _repropagationRole: 'child' | 'choice-group' | 'fieldset';
|
protected _repropagationRole: 'child' | 'choice-group' | 'fieldset';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* By default, a field with _repropagationRole 'choice-group' will act as an
|
* By default, a field with _repropagationRole 'choice-group' will act as an
|
||||||
* 'endpoint'. This means it will be considered as an individual field: for
|
* 'endpoint'. This means it will be considered as an individual field: for
|
||||||
|
|
|
||||||
141
packages/form-core/types/FormatMixinTypes.d.ts
vendored
141
packages/form-core/types/FormatMixinTypes.d.ts
vendored
|
|
@ -5,36 +5,167 @@ import { ValidateHost } from './validate/ValidateMixinTypes';
|
||||||
import { FormControlHost } from './FormControlMixinTypes';
|
import { FormControlHost } from './FormControlMixinTypes';
|
||||||
|
|
||||||
export declare class FormatHost {
|
export declare class FormatHost {
|
||||||
|
/**
|
||||||
|
* Converts viewValue to modelValue
|
||||||
|
* For instance, a localized date to a Date Object
|
||||||
|
* @param {string} v - viewValue: the formatted value inside <input>
|
||||||
|
* @param {FormatOptions} opts
|
||||||
|
* @returns {*} modelValue
|
||||||
|
*/
|
||||||
parser(v: string, opts: FormatNumberOptions): unknown;
|
parser(v: string, opts: FormatNumberOptions): unknown;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts modelValue to formattedValue (formattedValue will be synced with
|
||||||
|
* `._inputNode.value`)
|
||||||
|
* For instance, a Date object to a localized date.
|
||||||
|
* @param {*} v - modelValue: can be an Object, Number, String depending on the
|
||||||
|
* input type(date, number, email etc)
|
||||||
|
* @param {FormatOptions} opts
|
||||||
|
* @returns {string} formattedValue
|
||||||
|
*/
|
||||||
formatter(v: unknown, opts?: FormatNumberOptions): string;
|
formatter(v: unknown, opts?: FormatNumberOptions): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts `.modelValue` to `.serializedValue`
|
||||||
|
* For instance, a Date object to an iso formatted date string
|
||||||
|
* @param {?} v - modelValue: can be an Object, Number, String depending on the
|
||||||
|
* input type(date, number, email etc)
|
||||||
|
* @returns {string} serializedValue
|
||||||
|
*/
|
||||||
serializer(v: unknown): string;
|
serializer(v: unknown): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts `.serializedValue` to `.modelValue`
|
||||||
|
* For instance, an iso formatted date string to a Date object
|
||||||
|
* @param {?} v - modelValue: can be an Object, Number, String depending on the
|
||||||
|
* input type(date, number, email etc)
|
||||||
|
* @returns {?} modelValue
|
||||||
|
*/
|
||||||
deserializer(v: string): unknown;
|
deserializer(v: string): unknown;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preprocesses the viewValue before it's parsed to a modelValue. Can be used to filter
|
||||||
|
* invalid input amongst others.
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* preprocessor(viewValue) {
|
||||||
|
* // only use digits
|
||||||
|
* return viewValue.replace(/\D/g, '');
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* @param {string} v - the raw value from the <input> after keyUp/Down event
|
||||||
|
* @returns {string} preprocessedValue: the result of preprocessing for invalid input
|
||||||
|
*/
|
||||||
preprocessor(v: string): string;
|
preprocessor(v: string): string;
|
||||||
formattedValue: string;
|
|
||||||
serializedValue: string;
|
/**
|
||||||
|
* The view value is the result of the formatter function (when available).
|
||||||
|
* The result will be stored in the native _inputNode (usually an input[type=text]).
|
||||||
|
*
|
||||||
|
* 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)
|
||||||
|
* @type {string|undefined}
|
||||||
|
* @readOnly
|
||||||
|
*/
|
||||||
|
formattedValue: string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The serialized version of the model value.
|
||||||
|
* This value 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.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - For a date input, this would be the iso format of a date, e.g. '1999-01-20'.
|
||||||
|
* - For a number input this would be the String representation of a float ('1234.56'
|
||||||
|
* instead of 1234.56)
|
||||||
|
*
|
||||||
|
* When no parser is available, the value is usually the same as the formattedValue
|
||||||
|
* (being _inputNode.value)
|
||||||
|
*/
|
||||||
|
serializedValue: string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event that will trigger formatting (more precise, visual update of the view, so the
|
||||||
|
* user sees the formatted value)
|
||||||
|
* Default: 'change'
|
||||||
|
* @deprecated use _reflectBackOn()
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
formatOn: string;
|
formatOn: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration object that will be available inside the formatter function
|
||||||
|
*/
|
||||||
formatOptions: FormatNumberOptions;
|
formatOptions: FormatNumberOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view value. Will be delegated to `._inputNode.value`
|
||||||
|
*/
|
||||||
get value(): string;
|
get value(): string;
|
||||||
set value(value: string);
|
set value(value: string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag that will be set when user interaction takes place (for instance after an 'input'
|
||||||
|
* event). Will be added as meta info to the `model-value-changed` event. Depending on
|
||||||
|
* whether a user is interacting, formatting logic will be handled differently.
|
||||||
|
*/
|
||||||
protected _isHandlingUserInput: boolean;
|
protected _isHandlingUserInput: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the user is pasting content. Allows Subclassers to do this in their subclass:
|
* Whether the user is pasting content. Allows Subclassers to do this in their subclass:
|
||||||
* @example
|
* @example
|
||||||
* ```js
|
* ```js
|
||||||
* _reflectBackFormattedValueToUser() {
|
* _reflectBackOn() {
|
||||||
* return super._reflectBackFormattedValueToUser() || this._isPasting;
|
* return super._reflectBackOn() || this._isPasting;
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
protected _isPasting: boolean;
|
protected _isPasting: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* (in fact, some are called twice, but the __preventRecursiveTrigger lock prevents the
|
||||||
|
* second call from having effect).
|
||||||
|
* @param {{source:'model'|'serialized'|'formatted'|null}} config
|
||||||
|
* the type of value that triggered this method. It should not be set again, so that its
|
||||||
|
* observer won't be triggered. Can be: 'model'|'formatted'|'serialized'.
|
||||||
|
*/
|
||||||
protected _calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void;
|
protected _calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void;
|
||||||
protected _onModelValueChanged(arg: { modelValue: unknown }): void;
|
protected _onModelValueChanged(arg: { modelValue: unknown }): void;
|
||||||
protected _dispatchModelValueChangedEvent(): void;
|
protected _dispatchModelValueChangedEvent(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronization from `._inputNode.value` to `LionField` (flow [2])
|
||||||
|
* Downwards syncing should only happen for `LionField`.value changes from 'above'.
|
||||||
|
* This triggers _onModelValueChanged and connects user input
|
||||||
|
* to the parsing/formatting/serializing loop.
|
||||||
|
*/
|
||||||
protected _syncValueUpwards(): void;
|
protected _syncValueUpwards(): void;
|
||||||
protected _reflectBackFormattedValueToUser(): void;
|
protected _reflectBackFormattedValueToUser(): void;
|
||||||
protected _reflectBackFormattedValueDebounced(): void;
|
private _reflectBackFormattedValueDebounced(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every time .formattedValue is attempted to sync to the view value (on change/blur and on
|
||||||
|
* modelValue change), this condition is checked. When enhancing it, it's recommended to
|
||||||
|
* call `super._reflectBackOn()`
|
||||||
|
* @overridable
|
||||||
|
* @return {boolean}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected _reflectBackOn(): boolean;
|
protected _reflectBackOn(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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))
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected _proxyInputEvent(): void;
|
protected _proxyInputEvent(): void;
|
||||||
protected _onUserInputChanged(): void;
|
protected _onUserInputChanged(): void;
|
||||||
protected _callParser(value: string | undefined): object;
|
protected _callParser(value: string | undefined): object;
|
||||||
|
|
|
||||||
|
|
@ -14,22 +14,76 @@ type InteractionStates = {
|
||||||
prefilled: boolean;
|
prefilled: boolean;
|
||||||
};
|
};
|
||||||
export declare class InteractionStateHost {
|
export declare class InteractionStateHost {
|
||||||
prefilled: boolean;
|
/**
|
||||||
filled: boolean;
|
* True when user has focused and left(blurred) the field.
|
||||||
|
*/
|
||||||
touched: boolean;
|
touched: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when user has changed the value of the field.
|
||||||
|
*/
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when user has left non-empty field or input is prefilled.
|
||||||
|
* The name must be seen from the point of view of the input field:
|
||||||
|
* once the user enters the input field, the value is non-empty.
|
||||||
|
*/
|
||||||
|
prefilled: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the modelValue is non-empty (see _isEmpty in FormControlMixin)
|
||||||
|
*/
|
||||||
|
filled: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when user has attempted to submit the form, e.g. through a button
|
||||||
|
* of type="submit"
|
||||||
|
*/
|
||||||
submitted: boolean;
|
submitted: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluations performed on connectedCallback.
|
||||||
|
* This method is public, so it can be called at a later moment (when we need to wait for
|
||||||
|
* registering children for instance) as well.
|
||||||
|
* Since this method will be called twice in last mentioned scenario, it must stay idempotent.
|
||||||
|
*/
|
||||||
initInteractionState(): void;
|
initInteractionState(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets touched and dirty, and recomputes prefilled
|
||||||
|
*/
|
||||||
resetInteractionState(): void;
|
resetInteractionState(): void;
|
||||||
|
|
||||||
connectedCallback(): void;
|
/**
|
||||||
disconnectedCallback(): void;
|
* The event that triggers the touched state
|
||||||
|
*/
|
||||||
protected _leaveEvent: string;
|
protected _leaveEvent: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event that triggers the dirty state
|
||||||
|
*/
|
||||||
protected _valueChangedEvent: string;
|
protected _valueChangedEvent: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets touched value to true and reevaluates prefilled state.
|
||||||
|
* When false, on next interaction, user will start with a clean state.
|
||||||
|
*/
|
||||||
protected _iStateOnLeave(): void;
|
protected _iStateOnLeave(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets dirty value and validates when already touched or invalid
|
||||||
|
*/
|
||||||
protected _iStateOnValueChange(): void;
|
protected _iStateOnValueChange(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches event on touched state change
|
||||||
|
*/
|
||||||
protected _onTouchedChanged(): void;
|
protected _onTouchedChanged(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches event on touched state change
|
||||||
|
*/
|
||||||
protected _onDirtyChanged(): void;
|
protected _onDirtyChanged(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,27 @@ import { FormControlHost } from '@lion/form-core/types/FormControlMixinTypes';
|
||||||
import { FormatHost } from '@lion/form-core/types/FormatMixinTypes';
|
import { FormatHost } from '@lion/form-core/types/FormatMixinTypes';
|
||||||
|
|
||||||
export declare class NativeTextFieldHost {
|
export declare class NativeTextFieldHost {
|
||||||
|
/**
|
||||||
|
* Delegates autocomplete to input/textarea
|
||||||
|
*/
|
||||||
|
autocomplete: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates selectionStart to input/textarea
|
||||||
|
*/
|
||||||
get selectionStart(): number;
|
get selectionStart(): number;
|
||||||
set selectionStart(value: number);
|
set selectionStart(value: number);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates selectionEnd to input/textarea
|
||||||
|
*/
|
||||||
get selectionEnd(): number;
|
get selectionEnd(): number;
|
||||||
set selectionEnd(value: number);
|
set selectionEnd(value: number);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the cursor to its original position after updating the value.
|
||||||
|
*/
|
||||||
|
protected _setValueAndPreserveCaret(value: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function NativeTextFieldImplementation<T extends Constructor<LitElement>>(
|
export declare function NativeTextFieldImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ export declare class ChoiceInputHost {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
get modelValue(): ChoiceInputModelValue;
|
get modelValue(): ChoiceInputModelValue;
|
||||||
set modelValue(value: ChoiceInputModelValue);
|
set modelValue(value: ChoiceInputModelValue);
|
||||||
|
/**
|
||||||
|
* The value that will be registered to the modelValue of the parent ChoiceGroup. Recommended
|
||||||
|
* to be a string
|
||||||
|
*/
|
||||||
get choiceValue(): any;
|
get choiceValue(): any;
|
||||||
set choiceValue(value: any);
|
set choiceValue(value: any);
|
||||||
static get styles(): CSSResultArray;
|
static get styles(): CSSResultArray;
|
||||||
|
|
|
||||||
|
|
@ -7,26 +7,132 @@ import { FormRegistrarHost } from '../registration/FormRegistrarMixinTypes';
|
||||||
import { ValidateHost } from '../validate/ValidateMixinTypes';
|
import { ValidateHost } from '../validate/ValidateMixinTypes';
|
||||||
|
|
||||||
export declare class FormGroupHost {
|
export declare class FormGroupHost {
|
||||||
|
/**
|
||||||
|
* Disables all formElements in group
|
||||||
|
*/
|
||||||
|
disabled: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when all of the children are prefilled (see InteractionStateMixin for more details.)
|
||||||
|
*/
|
||||||
prefilled: boolean;
|
prefilled: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the group as a whole is blurred (see InteractionStateMixin for more details.)
|
||||||
|
*/
|
||||||
touched: boolean;
|
touched: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when any of the children is dirty (see InteractionStateMixin for more details.)
|
||||||
|
*/
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when parent form is submitted
|
||||||
|
*/
|
||||||
submitted: boolean;
|
submitted: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object keyed by formElements names, containing formElements' serializedValues
|
||||||
|
*/
|
||||||
serializedValue: { [key: string]: any };
|
serializedValue: { [key: string]: any };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object keyed by formElements names, containing formElements' formattedValues
|
||||||
|
*/
|
||||||
formattedValue: string;
|
formattedValue: string;
|
||||||
children: Array<HTMLElement & FormControlHost>;
|
|
||||||
|
/**
|
||||||
|
* Object keyed by formElements names, containing formElements' modelValues
|
||||||
|
*/
|
||||||
get modelValue(): { [x: string]: any };
|
get modelValue(): { [x: string]: any };
|
||||||
set modelValue(value: { [x: string]: any });
|
set modelValue(value: { [x: string]: any });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all interaction states for all formElements
|
||||||
|
*/
|
||||||
resetInteractionState(): void;
|
resetInteractionState(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all values and resets all interaction states of all FormControls in group,
|
||||||
|
*/
|
||||||
clearGroup(): void;
|
clearGroup(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles interaction state 'submitted'.
|
||||||
|
* This allows children to enable visibility of validation feedback
|
||||||
|
*/
|
||||||
submitGroup(): void;
|
submitGroup(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets to initial/prefilled values and interaction states of all FormControls in group,
|
||||||
|
*/
|
||||||
resetGroup(): void;
|
resetGroup(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gathers initial model values of all children. Used when resetGroup() is called.
|
||||||
|
*/
|
||||||
protected _initialModelValue: { [x: string]: any };
|
protected _initialModelValue: { [x: string]: any };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The host element with role group (or radigroup or form) containing neccessary aria attributes
|
||||||
|
*/
|
||||||
protected get _inputNode(): HTMLElement;
|
protected get _inputNode(): HTMLElement;
|
||||||
protected static _addDescriptionElementIdsToField(): void;
|
|
||||||
|
/**
|
||||||
|
* Gets a keyed be name object for requested property (like modelValue/serializedValue)
|
||||||
|
*/
|
||||||
|
protected _getFromAllFormElements(
|
||||||
|
property: string,
|
||||||
|
filterFn: (el: FormControlHost) => boolean,
|
||||||
|
): { [name: string]: any };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to set formElements values via a keyed object structure
|
||||||
|
*/
|
||||||
|
protected _setValueMapForAllFormElements(property: string, values: { [x: string]: any }): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the same value for requested property in all formElements
|
||||||
|
*/
|
||||||
protected _setValueForAllFormElements(property: string, value: any): void;
|
protected _setValueForAllFormElements(property: string, value: any): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when one of the formElements has requested property
|
||||||
|
*/
|
||||||
|
protected _anyFormElementHas(prop: string): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when all of the formElements have requested property
|
||||||
|
*/
|
||||||
|
protected _everyFormElementHas(prop: string): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when all of the formElements have requested property
|
||||||
|
*/
|
||||||
|
protected _anyFormElementHasFeedbackFor(prop: string): boolean;
|
||||||
|
protected _checkForOutsideClick(): void;
|
||||||
|
protected _triggerInitialModelValueChangedEvent(): void;
|
||||||
|
protected _syncDirty(): void;
|
||||||
|
protected _onFocusOut(): void;
|
||||||
|
protected _syncFocused(): void;
|
||||||
|
|
||||||
private __descriptionElementsInParentChain: Set<HTMLElement>;
|
private __descriptionElementsInParentChain: Set<HTMLElement>;
|
||||||
|
private __addedSubValidators: boolean;
|
||||||
|
private __isInitialModelValue: boolean;
|
||||||
|
private __isInitialSerializedValue: boolean;
|
||||||
|
private __pendingValues: {
|
||||||
|
modelValue?: { [key: string]: any };
|
||||||
|
serializedValue?: { [key: string]: any };
|
||||||
|
};
|
||||||
|
private __initInteractionStates(): void;
|
||||||
|
private __setupOutsideClickHandling(): void;
|
||||||
|
private __requestChildrenToBeDisabled(): void;
|
||||||
|
private __retractRequestChildrenToBeDisabled(): void;
|
||||||
|
private __linkParentMessages(): void;
|
||||||
|
private __unlinkParentMessages(): void;
|
||||||
|
private __storeAllDescriptionElementsInParentChain(): void;
|
||||||
|
private __onChildValidatePerformed(e: Event): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function FormGroupImplementation<T extends Constructor<LitElement>>(
|
export declare function FormGroupImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,15 @@ import { FormRegistrarHost } from './FormRegistrarMixinTypes';
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
|
|
||||||
export declare class FormRegisteringHost {
|
export declare class FormRegisteringHost {
|
||||||
|
/**
|
||||||
|
* The name the host is registered with to a parent
|
||||||
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The registrar this FormControl registers to, Usually a descendant of FormGroup or
|
||||||
|
* ChoiceGroup
|
||||||
|
*/
|
||||||
protected _parentFormGroup: FormRegistrarHost | undefined;
|
protected _parentFormGroup: FormRegistrarHost | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,15 @@ export declare class ElementWithParentFormGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class FormRegistrarHost {
|
export declare class FormRegistrarHost {
|
||||||
|
/**
|
||||||
|
* Closely mimics the natively supported HTMLFormControlsCollection. It can be accessed
|
||||||
|
* both like an array and an object (based on control/element names).
|
||||||
|
*/
|
||||||
formElements: FormControlsCollection & { [x: string]: any };
|
formElements: FormControlsCollection & { [x: string]: any };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds FormControl to `.formElements`
|
||||||
|
*/
|
||||||
addFormElement(
|
addFormElement(
|
||||||
child:
|
child:
|
||||||
| (FormControlHost & ElementWithParentFormGroup)
|
| (FormControlHost & ElementWithParentFormGroup)
|
||||||
|
|
@ -17,12 +25,53 @@ export declare class FormRegistrarHost {
|
||||||
| (HTMLElement & ElementWithParentFormGroup),
|
| (HTMLElement & ElementWithParentFormGroup),
|
||||||
indexToInsertAt?: number,
|
indexToInsertAt?: number,
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes FormControl from `.formElements`
|
||||||
|
*/
|
||||||
removeFormElement(child: FormRegisteringHost): void;
|
removeFormElement(child: FormRegisteringHost): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether FormControl is part of `.formElements`
|
||||||
|
*/
|
||||||
isRegisteredFormElement(el: FormControlHost): boolean;
|
isRegisteredFormElement(el: FormControlHost): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise that is resolved by `._completeRegistration`. By default after one microtask,
|
||||||
|
* so children get the chance to register themselves
|
||||||
|
*/
|
||||||
registrationComplete: Promise<boolean>;
|
registrationComplete: Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initComplete resolves after all pending initialization logic
|
||||||
|
* (for instance `<form-group .serializedValue=${{ child1: 'a', child2: 'b' }}>`)
|
||||||
|
* is executed.
|
||||||
|
*/
|
||||||
initComplete: Promise<boolean>;
|
initComplete: Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag that determines how ".formElements" should behave.
|
||||||
|
* For a regular fieldset (see LionFieldset) we expect ".formElements"
|
||||||
|
* to be accessible as an object.
|
||||||
|
* In case of a radio-group, a checkbox-group or a select/listbox,
|
||||||
|
* it should act like an array (see ChoiceGroupMixin).
|
||||||
|
* Usually, when false, we deal with a choice-group (radio-group, checkbox-group,
|
||||||
|
* (multi)select)
|
||||||
|
*/
|
||||||
protected _isFormOrFieldset: boolean;
|
protected _isFormOrFieldset: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for Subclassers to perform logic before an element is added
|
||||||
|
*/
|
||||||
protected _onRequestToAddFormElement(e: CustomEvent): void;
|
protected _onRequestToAddFormElement(e: CustomEvent): void;
|
||||||
|
|
||||||
|
protected _onRequestToChangeFormElementName(e: CustomEvent): void;
|
||||||
|
|
||||||
|
protected _onRequestToRemoveFormElement(e: CustomEvent): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the registrationComplete promise. Subclassers can delay if needed
|
||||||
|
*/
|
||||||
protected _completeRegistration(): void;
|
protected _completeRegistration(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
|
|
||||||
export declare class FormRegistrarPortalHost {
|
export declare class FormRegistrarPortalHost {
|
||||||
|
/**
|
||||||
|
* Registration target: an element, usually in the body of the dom, that captures events
|
||||||
|
* and redispatches them on host
|
||||||
|
*/
|
||||||
registrationTarget: HTMLElement;
|
registrationTarget: HTMLElement;
|
||||||
private __redispatchEventForFormRegistrarPortalMixin(ev: CustomEvent): void;
|
private __redispatchEventForFormRegistrarPortalMixin(ev: CustomEvent): void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,19 @@ export declare interface SyncUpdatableNamespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class SyncUpdatableHost {
|
export declare class SyncUpdatableHost {
|
||||||
|
/**
|
||||||
|
* An abstraction that has the exact same api as `requestUpdateInternal`, but taking
|
||||||
|
* into account:
|
||||||
|
* - [member order independence](https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence)
|
||||||
|
* - property effects start when all (light) dom has initialized (on firstUpdated)
|
||||||
|
* - property effects don't interrupt the first meaningful paint
|
||||||
|
* - compatible with propertyAccessor.`hasChanged`: no manual checks needed or accidentally
|
||||||
|
* run property effects / events when no change happened
|
||||||
|
* effects when values didn't change
|
||||||
|
* All code previously present in requestUpdateInternal can be placed in this method.
|
||||||
|
* @param {string} name
|
||||||
|
* @param {*} oldValue
|
||||||
|
*/
|
||||||
protected updateSync(name: string, oldValue: any): void;
|
protected updateSync(name: string, oldValue: any): void;
|
||||||
private __syncUpdatableInitialize(): void;
|
private __syncUpdatableInitialize(): void;
|
||||||
private __SyncUpdatableNamespace: SyncUpdatableNamespace;
|
private __SyncUpdatableNamespace: SyncUpdatableNamespace;
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,81 @@ type FeedbackMessage = {
|
||||||
validator?: Validator;
|
validator?: Validator;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ValidationType = 'error' | 'warning' | 'info' | 'success' | string;
|
||||||
|
|
||||||
export declare class ValidateHost {
|
export declare class ValidateHost {
|
||||||
|
/**
|
||||||
|
* Used by Application Developers to add Validators to a FormControl.
|
||||||
|
* @example
|
||||||
|
* ```html
|
||||||
|
* <form-control .validators="${[new Required(), new MinLength(4, {type: 'warning'})]}">
|
||||||
|
* </form-control>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
validators: Validator[];
|
validators: Validator[];
|
||||||
hasFeedbackFor: string[];
|
|
||||||
shouldShowFeedbackFor: string[];
|
/**
|
||||||
showsFeedbackFor: string[];
|
* As soon as validation happens (after modelValue/validators/validator param change), this
|
||||||
|
* array is updated with the active ValidationTypes ('error'|'warning'|'success'|'info' etc.).
|
||||||
|
* Notice the difference with `.showsFeedbackFor`, which filters `.hasFeedbackFor` based on
|
||||||
|
* `.feedbackCondition()`.
|
||||||
|
*
|
||||||
|
* For styling purposes, will be reflected to [has-feedback-for="error warning"]. This can
|
||||||
|
* be useful for subtle visual feedback on keyup, like a red/green border around an input.
|
||||||
|
*
|
||||||
|
* @readOnly
|
||||||
|
* @example
|
||||||
|
* ```css
|
||||||
|
* :host([has-feedback-for~="error"]) .input-group__container {
|
||||||
|
* border: 1px solid red;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
hasFeedbackFor: ValidationType[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on outcome of feedbackCondition, this array decides what ValidationTypes should be
|
||||||
|
* shown in validationFeedback, based on meta data like interaction states.
|
||||||
|
*
|
||||||
|
* For styling purposes, it reflects it `[shows-feedback-for="error warning"]`
|
||||||
|
* @readOnly
|
||||||
|
* @example
|
||||||
|
* ```css
|
||||||
|
* :host([shows-feedback-for~="success"]) .form-field__feedback {
|
||||||
|
* transform: scaleY(1);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
showsFeedbackFor: ValidationType[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A temporary storage to transition from hasFeedbackFor to showsFeedbackFor
|
||||||
|
* @type {ValidationType[]}
|
||||||
|
* @readOnly
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private shouldShowFeedbackFor: ValidationType[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The outcome of a validation 'round'. Keyed by ValidationType and Validator name
|
||||||
|
* @readOnly
|
||||||
|
*/
|
||||||
validationStates: { [key: string]: { [key: string]: Object } };
|
validationStates: { [key: string]: { [key: string]: Object } };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag indicating whether async validation is pending.
|
||||||
|
* Creates attribute [is-pending] as a styling hook
|
||||||
|
*/
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by Subclassers to add default Validators to a particular FormControl.
|
||||||
|
* A date input for instance, always needs the isDate validator.
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* this.defaultValidators.push(new IsDate());
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
defaultValidators: Validator[];
|
defaultValidators: Validator[];
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
validateComplete: Promise<void>;
|
validateComplete: Promise<void>;
|
||||||
|
|
@ -32,19 +100,97 @@ export declare class ValidateHost {
|
||||||
|
|
||||||
static validationTypes: string[];
|
static validationTypes: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered by:
|
||||||
|
* - modelValue change
|
||||||
|
* - change in the 'validators' array
|
||||||
|
* - change in the config of an individual Validator
|
||||||
|
*
|
||||||
|
* Three situations are handled:
|
||||||
|
* - a1) the FormControl is empty: further execution is halted. When the Required Validator
|
||||||
|
* (being mutually exclusive to the other Validators) is applied, it will end up in the
|
||||||
|
* validation result (as the only Validator, since further execution was halted).
|
||||||
|
* - a2) there are synchronous Validators: this is the most common flow. When modelValue hasn't
|
||||||
|
* changed since last async results were generated, 'sync results' are merged with the
|
||||||
|
* 'async results'.
|
||||||
|
* - a3) there are asynchronous Validators: for instance when server side evaluation is needed.
|
||||||
|
* Executions are scheduled and awaited and the 'async results' are merged with the
|
||||||
|
* 'sync results'.
|
||||||
|
*
|
||||||
|
* - b) there are ResultValidators. After steps a1, a2, or a3 are finished, the holistic
|
||||||
|
* ResultValidators (evaluating the total result of the 'regular' (a1, a2 and a3) validators)
|
||||||
|
* will be run...
|
||||||
|
*
|
||||||
|
* Situations a2 and a3 are not mutually exclusive and can be triggered within one `validate()`
|
||||||
|
* call. Situation b will occur after every call.
|
||||||
|
*
|
||||||
|
* @param {{ clearCurrentResult?: boolean }} [opts]
|
||||||
|
*/
|
||||||
validate(opts?: { clearCurrentResult?: boolean }): void;
|
validate(opts?: { clearCurrentResult?: boolean }): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of feedback messages that will visible in LionValidationFeedback
|
||||||
|
*/
|
||||||
protected _visibleMessagesAmount: number;
|
protected _visibleMessagesAmount: number;
|
||||||
protected _allValidators: Validator[];
|
|
||||||
protected get _feedbackConditionMeta(): object;
|
|
||||||
protected get _feedbackNode(): LionValidationFeedback;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combination of validators provided by Application Developer and the default validators
|
||||||
|
*/
|
||||||
|
protected _allValidators: Validator[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows Subclassers to add meta info for feedbackCondition
|
||||||
|
*/
|
||||||
|
protected get _feedbackConditionMeta(): object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for retrieving messages from Validators and
|
||||||
|
* (delegation of) rendering them.
|
||||||
|
*
|
||||||
|
* For `._feedbackNode` (extension of LionValidationFeedback):
|
||||||
|
* - retrieve messages from highest prio Validators
|
||||||
|
* - provide the result to custom feedback node and let the
|
||||||
|
* custom node decide on their renderings
|
||||||
|
*
|
||||||
|
* In both cases:
|
||||||
|
* - we compute the 'show' flag (like 'hasErrorVisible') for all types
|
||||||
|
* - we set the customValidity message of the highest prio Validator
|
||||||
|
* - we set aria-invalid="true" in case hasErrorVisible is true
|
||||||
|
*/
|
||||||
protected _updateFeedbackComponent(): void;
|
protected _updateFeedbackComponent(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default feedbackCondition condition, used by Subclassers, that will be used when
|
||||||
|
* `feedbackCondition()` is not overridden by Application Developer.
|
||||||
|
* Show the validity feedback when returning true, don't show when false
|
||||||
|
* @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom
|
||||||
|
* Validator type
|
||||||
|
* @param {object} meta meta info (interaction states etc)
|
||||||
|
*/
|
||||||
protected _showFeedbackConditionFor(type: string, meta: object): boolean;
|
protected _showFeedbackConditionFor(type: string, meta: object): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to translate `.hasFeedbackFor` and `.shouldShowFeedbackFor` to `.showsFeedbackFor`
|
||||||
|
*/
|
||||||
protected _hasFeedbackVisibleFor(type: string): boolean;
|
protected _hasFeedbackVisibleFor(type: string): boolean;
|
||||||
protected _updateShouldShowFeedbackFor(): void;
|
protected _updateShouldShowFeedbackFor(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orders all active validators in this.__validationResult. Can also filter out occurrences
|
||||||
|
* (based on interaction states).
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* _prioritizeAndFilterFeedback({ validationResult }) {
|
||||||
|
* // Put info messages on top; no limitation by `._visibleMessagesAmount`
|
||||||
|
* const meta = this._feedbackConditionMeta;
|
||||||
|
* return validationResult.filter(v =>
|
||||||
|
* this.feedbackCondition(v.type, meta, this._showFeedbackConditionFor.bind(this))
|
||||||
|
* ).sort((a, b) => a.type === 'info' ? 1 : 0);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
protected _prioritizeAndFilterFeedback(opts: { validationResult: Validator[] }): Validator[];
|
protected _prioritizeAndFilterFeedback(opts: { validationResult: Validator[] }): Validator[];
|
||||||
protected updateSync(name: string, oldValue: unknown): void;
|
|
||||||
|
|
||||||
private __syncValidationResult: Validator[];
|
private __syncValidationResult: Validator[];
|
||||||
private __asyncValidationResult: Validator[];
|
private __asyncValidationResult: Validator[];
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,9 @@ export class LionInputRange extends LionInput {
|
||||||
_inputGroupTemplate() {
|
_inputGroupTemplate() {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<span class="input-range__value">${formatNumber(parseFloat(this.formattedValue))}</span>
|
<span class="input-range__value"
|
||||||
|
>${formatNumber(parseFloat(/** @type {string} */ (this.formattedValue)))}</span
|
||||||
|
>
|
||||||
<span class="input-range__unit">${this.unit}</span>
|
<span class="input-range__unit">${this.unit}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ describe('<lion-radio-group>', () => {
|
||||||
</lion-radio-group>
|
</lion-radio-group>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
el.children[1].focus();
|
el.formElements[1].focus();
|
||||||
expect(el.touched).to.equal(false, 'initially, touched state is false');
|
expect(el.touched).to.equal(false, 'initially, touched state is false');
|
||||||
/** @type {LionRadio} */ (el.children[1]).checked = true;
|
/** @type {LionRadio} */ (el.children[1]).checked = true;
|
||||||
expect(el.touched, `focused via a mouse click, group should be touched`).to.be.true;
|
expect(el.touched, `focused via a mouse click, group should be touched`).to.be.true;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,21 @@
|
||||||
import { LionField } from '@lion/form-core';
|
import { LionField } from '@lion/form-core';
|
||||||
|
|
||||||
class LionFieldWithSelect extends LionField {
|
class LionFieldWithSelect extends LionField {
|
||||||
|
/** @type {any} */
|
||||||
|
static get properties() {
|
||||||
|
return { autocomplete: { type: String } };
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates autocomplete to select
|
||||||
|
* @type {string|undefined}
|
||||||
|
*/
|
||||||
|
this.autocomplete = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {HTMLSelectElement}
|
* @returns {HTMLSelectElement}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue