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'
|
||||
* 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}
|
||||
*/
|
||||
this.focusedVisible = false;
|
||||
|
|
|
|||
|
|
@ -40,83 +40,22 @@ const FormControlMixinImplementation = superclass =>
|
|||
/** @type {any} */
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* The name the element will be registered on to the .formElements collection
|
||||
* 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
|
||||
*/
|
||||
name: { type: String, reflect: true },
|
||||
readOnly: { type: Boolean, attribute: 'readonly', reflect: true },
|
||||
label: String, // FIXME: { attribute: false } breaks a bunch of tests, but shouldn't...
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
helpText: { type: String, attribute: 'help-text' },
|
||||
modelValue: { attribute: false },
|
||||
|
||||
/**
|
||||
* Contains all elements that should end up in aria-labelledby of `._inputNode`
|
||||
*/
|
||||
_ariaLabelledNodes: { attribute: false },
|
||||
/**
|
||||
* Contains all elements that should end up in aria-describedby of `._inputNode`
|
||||
*/
|
||||
_ariaDescribedNodes: { attribute: false },
|
||||
/**
|
||||
* Based on the role, details of handling model-value-changed repropagation differ.
|
||||
*/
|
||||
_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 },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @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() {
|
||||
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() {
|
||||
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() {
|
||||
return this.__fieldName || this.label || this.name || '';
|
||||
|
|
@ -165,7 +107,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* @type {SlotsMap}
|
||||
* @configure SlotMixin
|
||||
*/
|
||||
get slots() {
|
||||
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() {
|
||||
return /** @type {HTMLElementWithValue} */ (this.__getDirectSlotChild('input'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Element where label will be rendered to
|
||||
* @protected
|
||||
*/
|
||||
get _labelNode() {
|
||||
return /** @type {HTMLElement} */ (this.__getDirectSlotChild('label'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Element where help text will be rendered to
|
||||
* @protected
|
||||
*/
|
||||
get _helpTextNode() {
|
||||
return /** @type {HTMLElement} */ (this.__getDirectSlotChild('help-text'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Element where validation feedback will be rendered to
|
||||
* @protected
|
||||
*/
|
||||
get _feedbackNode() {
|
||||
|
|
@ -205,19 +160,91 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
constructor() {
|
||||
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 = '';
|
||||
/** @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);
|
||||
/** @type {HTMLElement[]} */
|
||||
|
||||
/**
|
||||
* Contains all elements that should end up in aria-labelledby of `._inputNode`
|
||||
* @type {HTMLElement[]}
|
||||
*/
|
||||
this._ariaLabelledNodes = [];
|
||||
/** @type {HTMLElement[]} */
|
||||
|
||||
/**
|
||||
* Contains all elements that should end up in aria-describedby of `._inputNode`
|
||||
* @type {HTMLElement[]}
|
||||
*/
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/** @private */
|
||||
this.__label = '';
|
||||
|
||||
this.addEventListener(
|
||||
'model-value-changed',
|
||||
/** @type {EventListenerOrEventListenerObject} */ (this.__repropagateChildrenValues),
|
||||
|
|
@ -274,6 +301,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
if (changedProperties.has('name')) {
|
||||
this.dispatchEvent(
|
||||
/** @privateEvent */
|
||||
new CustomEvent('form-element-name-changed', {
|
||||
detail: { oldName: changedProperties.get('name'), newName: this.name },
|
||||
bubbles: true,
|
||||
|
|
@ -551,6 +579,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Used for Required validation and computation of interaction states
|
||||
* @param {any} modelValue
|
||||
* @return {boolean}
|
||||
* @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 {{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
|
||||
*/
|
||||
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 {{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
|
||||
*/
|
||||
removeFromAriaDescribedBy(element) {
|
||||
|
|
@ -810,6 +839,8 @@ const FormControlMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Hook for Subclassers to add logic before repropagation
|
||||
* @configurable
|
||||
* @param {CustomEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -62,46 +62,8 @@ const FormatMixinImplementation = superclass =>
|
|||
/** @type {any} */
|
||||
static get properties() {
|
||||
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 },
|
||||
|
||||
/**
|
||||
* 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 },
|
||||
|
||||
/**
|
||||
* 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 },
|
||||
};
|
||||
}
|
||||
|
|
@ -124,11 +86,13 @@ const FormatMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The view value. Will be delegated to `._inputNode.value`
|
||||
*/
|
||||
get 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 */
|
||||
set value(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
|
||||
* @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
|
||||
* @param {string} v - formattedValue: the formatted value inside <input>
|
||||
* @param {string} v - viewValue: the formatted value inside <input>
|
||||
* @param {FormatOptions} opts
|
||||
* @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
|
||||
* @param {?} v - modelValue: can be an Object, Number, String depending on the
|
||||
* input type(date, number, email etc)
|
||||
|
|
@ -223,11 +196,9 @@ const FormatMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
if (source !== 'formatted') {
|
||||
/** @type {string} */
|
||||
this.formattedValue = this._callFormatter();
|
||||
}
|
||||
if (source !== 'serialized') {
|
||||
/** @type {string} */
|
||||
this.serializedValue = this.serializer(this.modelValue);
|
||||
}
|
||||
this._reflectBackFormattedValueToUser();
|
||||
|
|
@ -310,7 +281,6 @@ const FormatMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Observer Handlers
|
||||
* @param {{ modelValue: unknown; }[]} args
|
||||
* @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
|
||||
* is fired. For objects, a deep comparison might be needed.
|
||||
* @param {{ modelValue: unknown; }[]} args
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_dispatchModelValueChangedEvent(...args) {
|
||||
/** @event model-value-changed */
|
||||
this.dispatchEvent(
|
||||
/** @privateEvent model-value-changed: FormControl redispatches it as public event */
|
||||
new CustomEvent('model-value-changed', {
|
||||
bubbles: true,
|
||||
detail: /** @type { ModelValueEventDetails } */ ({
|
||||
|
|
@ -396,12 +367,9 @@ const FormatMixinImplementation = superclass =>
|
|||
* @protected
|
||||
*/
|
||||
_proxyInputEvent() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('user-input-changed', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
// TODO: [v1] remove composed (and bubbles as well if possible)
|
||||
/** @protectedEvent user-input-changed meant for usage by Subclassers only */
|
||||
this.dispatchEvent(new Event('user-input-changed', { bubbles: true }));
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
|
|
@ -428,8 +396,52 @@ const FormatMixinImplementation = superclass =>
|
|||
|
||||
constructor() {
|
||||
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';
|
||||
|
||||
/**
|
||||
* Configuration object that will be available inside the formatter function
|
||||
*/
|
||||
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:
|
||||
* @example
|
||||
|
|
@ -439,8 +451,18 @@ const FormatMixinImplementation = superclass =>
|
|||
* }
|
||||
* ```
|
||||
* @protected
|
||||
* @type {boolean}
|
||||
*/
|
||||
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
|
||||
* @type {string}
|
||||
|
|
@ -451,6 +473,20 @@ const FormatMixinImplementation = superclass =>
|
|||
this.addEventListener('user-input-changed', this._onUserInputChanged);
|
||||
// This sets the formatted viewValue after paste
|
||||
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() {
|
||||
|
|
@ -464,13 +500,6 @@ const FormatMixinImplementation = superclass =>
|
|||
|
||||
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
|
||||
// fallback mechanism. Assume the user uses the value property of the
|
||||
|
|
|
|||
|
|
@ -23,47 +23,15 @@ const InteractionStateMixinImplementation = superclass =>
|
|||
/** @type {any} */
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* True when user has focused and left(blurred) the field.
|
||||
*/
|
||||
touched: {
|
||||
type: Boolean,
|
||||
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,
|
||||
},
|
||||
touched: { type: Boolean, reflect: true },
|
||||
dirty: { type: Boolean, reflect: true },
|
||||
filled: { type: Boolean, reflect: true },
|
||||
prefilled: { attribute: false },
|
||||
submitted: { attribute: false },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PropertyKey} name
|
||||
* @param {*} oldVal
|
||||
*/
|
||||
|
|
@ -86,18 +54,65 @@ const InteractionStateMixinImplementation = superclass =>
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
/**
|
||||
* True when user has focused and left(blurred) the field.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.touched = false;
|
||||
|
||||
/**
|
||||
* True when user has changed the value of the field.
|
||||
* @type {boolean}
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* True when the modelValue is non-empty (see _isEmpty in FormControlMixin)
|
||||
* @type {boolean}
|
||||
*/
|
||||
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';
|
||||
/** @type {string} */
|
||||
|
||||
/**
|
||||
* The event that triggers the dirty state
|
||||
* @type {string}
|
||||
* @protected
|
||||
*/
|
||||
this._valueChangedEvent = 'model-value-changed';
|
||||
/** @type {EventHandlerNonNull} */
|
||||
|
||||
/**
|
||||
* @type {EventHandlerNonNull}
|
||||
* @protected
|
||||
*/
|
||||
this._iStateOnLeave = this._iStateOnLeave.bind(this);
|
||||
/** @type {EventHandlerNonNull} */
|
||||
|
||||
/**
|
||||
* @type {EventHandlerNonNull}
|
||||
* @protected
|
||||
*/
|
||||
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
|
||||
* (due to interdependence on light children that can only be processed
|
||||
* after connectedCallback and affect the initial value).
|
||||
* This method is exposed, so it can be called after they are initialized themselves.
|
||||
* 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() {
|
||||
|
|
@ -130,8 +144,7 @@ const InteractionStateMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets touched value to true
|
||||
* Reevaluates prefilled state.
|
||||
* Sets touched value to true and reevaluates prefilled state.
|
||||
* When false, on next interaction, user will start with a clean state.
|
||||
* @protected
|
||||
*/
|
||||
|
|
@ -159,22 +172,25 @@ const InteractionStateMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Dispatches custom event on touched state change
|
||||
* Dispatches event on touched state change
|
||||
* @protected
|
||||
*/
|
||||
_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
|
||||
*/
|
||||
_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:
|
||||
*
|
||||
* - submitted
|
||||
|
|
@ -199,6 +215,9 @@ const InteractionStateMixinImplementation = superclass =>
|
|||
return (meta.touched && meta.dirty) || meta.prefilled || meta.submitted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @enhance ValidateMixin
|
||||
*/
|
||||
get _feedbackConditionMeta() {
|
||||
return {
|
||||
// @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(
|
||||
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
|
||||
*/
|
||||
|
|
@ -85,7 +65,8 @@ export class LionField extends FormControlMixin(
|
|||
* Interaction states are not cleared (use resetInteractionState for this)
|
||||
*/
|
||||
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
|
||||
*/
|
||||
_onChange() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('user-input-changed', {
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
/** @protectedEvent user-input-changed */
|
||||
this.dispatchEvent(new Event('user-input-changed', { bubbles: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,6 +10,23 @@ import { FormatMixin } from './FormatMixin.js';
|
|||
*/
|
||||
const NativeTextFieldMixinImplementation = 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
|
||||
* @type {HTMLInputElement | HTMLTextAreaElement}
|
||||
|
|
|
|||
|
|
@ -25,15 +25,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
/** @type {any} */
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* @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',
|
||||
},
|
||||
multipleChoice: { type: Boolean, attribute: 'multiple-choice' },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -132,11 +124,21 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
|
||||
constructor() {
|
||||
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;
|
||||
/** @type {'child'|'choice-group'|'fieldset'}
|
||||
|
||||
/**
|
||||
* @type {'child'|'choice-group'|'fieldset'}
|
||||
* @configure FormControlMixin event propagation
|
||||
* @protected
|
||||
*/
|
||||
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin
|
||||
this._repropagationRole = 'choice-group';
|
||||
/** @private */
|
||||
this.__isInitialModelValue = true;
|
||||
/** @private */
|
||||
|
|
@ -156,7 +158,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* @enhance FormRegistrarMixin
|
||||
* @enhance FormRegistrarMixin: we need one extra microtask to complete
|
||||
*/
|
||||
_completeRegistration() {
|
||||
// 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 {number} indexToInsertAt
|
||||
*/
|
||||
|
|
@ -353,10 +355,9 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Don't repropagate unchecked single choice choiceInputs
|
||||
* @param {FormControlHost & ChoiceInputHost} target
|
||||
* @protected
|
||||
* @overridable
|
||||
* @configure FormControlMixin: don't repropagate unchecked single choice choiceInputs
|
||||
*/
|
||||
_repropagationCondition(target) {
|
||||
return !(
|
||||
|
|
|
|||
|
|
@ -25,42 +25,18 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
/** @type {any} */
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* Boolean indicating whether or not this element is checked by the end user.
|
||||
*/
|
||||
checked: {
|
||||
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,
|
||||
},
|
||||
checked: { type: Boolean, reflect: true },
|
||||
disabled: { type: Boolean, reflect: true },
|
||||
modelValue: { type: Object, hasChanged },
|
||||
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() {
|
||||
return this.modelValue.value;
|
||||
}
|
||||
|
|
@ -123,8 +99,33 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
|
||||
constructor() {
|
||||
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 };
|
||||
// TODO: maybe disabled is more a concern of FormControl/Field?
|
||||
/**
|
||||
* Boolean indicating whether or not this element is disabled.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.disabled = false;
|
||||
|
||||
/**
|
||||
* The value property of the modelValue. It provides an easy interface for storing
|
||||
* (complex) values in the modelValue
|
||||
*/
|
||||
|
||||
/** @protected */
|
||||
this._preventDuplicateLabelClick = this._preventDuplicateLabelClick.bind(this);
|
||||
/** @protected */
|
||||
|
|
|
|||
|
|
@ -34,54 +34,25 @@ const FormGroupMixinImplementation = superclass =>
|
|||
/** @type {any} */
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* Interaction state that can be used to compute the visibility of
|
||||
* feedback messages
|
||||
*/
|
||||
submitted: {
|
||||
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,
|
||||
},
|
||||
submitted: { type: Boolean, reflect: true },
|
||||
focused: { type: Boolean, reflect: true },
|
||||
dirty: { type: Boolean, reflect: true },
|
||||
touched: { type: Boolean, reflect: true },
|
||||
prefilled: { type: Boolean, reflect: true },
|
||||
};
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
/**
|
||||
* The host element with role group (or radigroup or form) containing neccessary aria attributes
|
||||
* @protected
|
||||
*/
|
||||
get _inputNode() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Object keyed by formElements names, containing formElements' modelValues
|
||||
*/
|
||||
get modelValue() {
|
||||
return this._getFromAllFormElements('modelValue');
|
||||
}
|
||||
|
|
@ -97,6 +68,9 @@ const FormGroupMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Object keyed by formElements names, containing formElements' serializedValues
|
||||
*/
|
||||
get serializedValue() {
|
||||
return this._getFromAllFormElements('serializedValue');
|
||||
}
|
||||
|
|
@ -112,6 +86,9 @@ const FormGroupMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Object keyed by formElements names, containing formElements' formattedValues
|
||||
*/
|
||||
get formattedValue() {
|
||||
return this._getFromAllFormElements('formattedValue');
|
||||
}
|
||||
|
|
@ -120,24 +97,51 @@ const FormGroupMixinImplementation = superclass =>
|
|||
this._setValueMapForAllFormElements('formattedValue', values);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when all of the children are prefilled (see InteractionStateMixin for more details.)
|
||||
*/
|
||||
get prefilled() {
|
||||
return this._everyFormElementHas('prefilled');
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// ._inputNode = this, which always requires a value prop
|
||||
|
||||
// ._inputNode === this, which always requires a value prop
|
||||
this.value = '';
|
||||
|
||||
/**
|
||||
* Disables all formElements in group
|
||||
*/
|
||||
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.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
|
||||
*/
|
||||
submitGroup() {
|
||||
|
|
@ -269,6 +273,9 @@ const FormGroupMixinImplementation = superclass =>
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets to initial/prefilled values and interaction states of all FormControls in group,
|
||||
*/
|
||||
resetGroup() {
|
||||
this.formElements.forEach(child => {
|
||||
if (typeof child.resetGroup === 'function') {
|
||||
|
|
@ -281,6 +288,9 @@ const FormGroupMixinImplementation = superclass =>
|
|||
this.resetInteractionState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all values and resets all interaction states of all FormControls in group,
|
||||
*/
|
||||
clearGroup() {
|
||||
this.formElements.forEach(child => {
|
||||
if (typeof child.clearGroup === 'function') {
|
||||
|
|
@ -293,6 +303,9 @@ const FormGroupMixinImplementation = superclass =>
|
|||
this.resetInteractionState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all interaction states for all formElements
|
||||
*/
|
||||
resetInteractionState() {
|
||||
this.submitted = 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
|
||||
* @returns {{[name:string]: any}}
|
||||
*/
|
||||
_getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) {
|
||||
const result = {};
|
||||
|
|
@ -326,6 +341,7 @@ const FormGroupMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the same value for requested property in all formElements
|
||||
* @param {string | number} property
|
||||
* @param {any} value
|
||||
*/
|
||||
|
|
@ -336,6 +352,7 @@ const FormGroupMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Allows to set formElements values via a keyed object structure
|
||||
* @param {string} property
|
||||
* @param {{ [x: string]: any; }} values
|
||||
*/
|
||||
|
|
@ -360,6 +377,7 @@ const FormGroupMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns true when one of the formElements has requested
|
||||
* @param {string} property
|
||||
*/
|
||||
_anyFormElementHas(property) {
|
||||
|
|
@ -389,6 +407,7 @@ const FormGroupMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns true when all of the formElements have requested property
|
||||
* @param {string} 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
|
||||
* - 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
|
||||
* - children validity states have changed, so fieldset needs to update itself based on that
|
||||
* - 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
|
||||
* - children validity states have changed, so fieldset needs to update itself based on that
|
||||
* @param {Event} 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
|
||||
* first hear the local error, followed by #group-error
|
||||
* @example
|
||||
* ```html
|
||||
* <lion-fieldset name="address">
|
||||
* <lion-input name="street" label="Street" .modelValue="${'Park Avenue'}"></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
|
||||
* </div>
|
||||
* </lion-fieldset>
|
||||
* ```
|
||||
*/
|
||||
__storeAllDescriptionElementsInParentChain() {
|
||||
const unTypedThis = /** @type {unknown} */ (this);
|
||||
|
|
@ -487,8 +510,7 @@ const FormGroupMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* @override of FormRegistrarMixin.
|
||||
* @desc Connects ValidateMixin and DisabledMixin
|
||||
* @enhance FormRegistrarMixin: connects ValidateMixin and DisabledMixin.
|
||||
* On top of this, error messages of children are linked to their parents
|
||||
* @param {FormControl & {serializedValue:string|object}} child
|
||||
* @param {number} indexToInsertAt
|
||||
|
|
@ -520,15 +542,14 @@ const FormGroupMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Gathers initial model values of all children. Used
|
||||
* when resetGroup() is called.
|
||||
* Gathers initial model values of all children. Used when resetGroup() is called.
|
||||
*/
|
||||
get _initialModelValue() {
|
||||
return this._getFromAllFormElements('_initialModelValue');
|
||||
}
|
||||
|
||||
/**
|
||||
* @override of FormRegistrarMixin. Connects ValidateMixin
|
||||
* @override FormRegistrarMixin; Connects ValidateMixin
|
||||
* @param {FormRegisteringHost & FormControl} el
|
||||
*/
|
||||
removeFormElement(el) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* @desc This class closely mimics the natively
|
||||
* This class closely mimics the natively
|
||||
* supported HTMLFormControlsCollection. It can be accessed
|
||||
* both like an array and an object (based on control/element names).
|
||||
* @example
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ const FormRegisteringMixinImplementation = superclass =>
|
|||
class extends superclass {
|
||||
constructor() {
|
||||
super();
|
||||
/** @type {FormRegistrarHost | undefined} */
|
||||
/**
|
||||
* The registrar this FormControl registers to, Usually a descendant of FormGroup or
|
||||
* ChoiceGroup
|
||||
* @type {FormRegistrarHost | undefined}
|
||||
*/
|
||||
this._parentFormGroup = undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,23 +29,31 @@ const FormRegistrarMixinImplementation = superclass =>
|
|||
/** @type {any} */
|
||||
static get properties() {
|
||||
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 },
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
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();
|
||||
|
||||
/**
|
||||
* 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._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
||||
|
|
@ -100,6 +108,10 @@ const FormRegistrarMixinImplementation = superclass =>
|
|||
this._completeRegistration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the registrationComplete promise. Subclassers can delay if needed
|
||||
* @overridable
|
||||
*/
|
||||
_completeRegistration() {
|
||||
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
|
||||
* @protected
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -24,7 +24,12 @@ const FormRegistrarPortalMixinImplementation = superclass =>
|
|||
class extends superclass {
|
||||
constructor() {
|
||||
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.__redispatchEventForFormRegistrarPortalMixin = this.__redispatchEventForFormRegistrarPortalMixin.bind(
|
||||
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:
|
||||
* 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
|
||||
|
|
@ -27,8 +27,9 @@ const SyncUpdatableMixinImplementation = superclass =>
|
|||
class extends superclass {
|
||||
constructor() {
|
||||
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}
|
||||
*/
|
||||
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.
|
||||
* @param {string} name
|
||||
* @param {*} oldValue
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ export class LionValidationFeedback extends LitElement {
|
|||
this.setAttribute('type', this.feedbackData[0].type);
|
||||
this.currentType = this.feedbackData[0].type;
|
||||
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') {
|
||||
this.removeMessage = window.setTimeout(() => {
|
||||
this.removeAttribute('type');
|
||||
|
|
|
|||
|
|
@ -16,7 +16,16 @@
|
|||
export class Unparseable {
|
||||
/** @param {string} value */
|
||||
constructor(value) {
|
||||
/**
|
||||
* Meta info for restoring serialized Unparseable values
|
||||
* @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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ import { Validator } from './Validator.js';
|
|||
import { Required } from './validators/Required.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').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.
|
||||
*
|
||||
* @type {ValidateMixin}
|
||||
|
|
@ -48,11 +51,8 @@ export const ValidateMixinImplementation = superclass =>
|
|||
static get properties() {
|
||||
return {
|
||||
validators: { attribute: false },
|
||||
|
||||
hasFeedbackFor: { attribute: false },
|
||||
|
||||
shouldShowFeedbackFor: { attribute: false },
|
||||
|
||||
showsFeedbackFor: {
|
||||
type: Array,
|
||||
attribute: 'shows-feedback-for',
|
||||
|
|
@ -62,36 +62,22 @@ export const ValidateMixinImplementation = superclass =>
|
|||
toAttribute: /** @param {[]} value */ value => value.join(','),
|
||||
},
|
||||
},
|
||||
|
||||
validationStates: { attribute: false },
|
||||
|
||||
/**
|
||||
* @desc flag that indicates whether async validation is pending
|
||||
*/
|
||||
isPending: {
|
||||
type: Boolean,
|
||||
attribute: 'is-pending',
|
||||
reflect: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc specialized fields (think of input-date and input-email) can have preconfigured
|
||||
* validators.
|
||||
*/
|
||||
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 },
|
||||
|
||||
__childModelValueChanged: { attribute: false },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Types of validation supported by this FormControl (for instance 'error'|'warning'|'info')
|
||||
* @overridable
|
||||
* @type {ValidationType[]}
|
||||
*/
|
||||
static get validationTypes() {
|
||||
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() {
|
||||
return [...this.validators, ...this.defaultValidators];
|
||||
}
|
||||
|
|
@ -131,28 +122,93 @@ export const ValidateMixinImplementation = superclass =>
|
|||
constructor() {
|
||||
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 = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
this.shouldShowFeedbackFor = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
/**
|
||||
* 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"]`
|
||||
* @type {ValidationType[]}
|
||||
* @readOnly
|
||||
* @example
|
||||
* ```css
|
||||
* :host([shows-feedback-for~="success"]) .form-field__feedback {
|
||||
* transform: scaleY(1);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
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 = {};
|
||||
|
||||
/** @protected */
|
||||
this._visibleMessagesAmount = 1;
|
||||
|
||||
/**
|
||||
* Flag indicating whether async validation is pending.
|
||||
* Creates attribute [is-pending] as a styling hook
|
||||
* @type {boolean}
|
||||
*/
|
||||
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 = [];
|
||||
/** @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 = [];
|
||||
|
||||
/**
|
||||
* The amount of feedback messages that will visible in LionValidationFeedback
|
||||
* @protected
|
||||
*/
|
||||
this._visibleMessagesAmount = 1;
|
||||
|
||||
/**
|
||||
* @type {Validator[]}
|
||||
* @private
|
||||
|
|
@ -166,29 +222,35 @@ export const ValidateMixinImplementation = superclass =>
|
|||
this.__asyncValidationResult = [];
|
||||
|
||||
/**
|
||||
* @desc contains results from sync Validators, async Validators and ResultValidators
|
||||
* Aggregated result from sync Validators, async Validators and ResultValidators
|
||||
* @type {Validator[]}
|
||||
* @private
|
||||
*/
|
||||
this.__validationResult = [];
|
||||
|
||||
/**
|
||||
* @type {Validator[]}
|
||||
* @private
|
||||
*/
|
||||
this.__prevValidationResult = [];
|
||||
/** @type {Validator[]} */
|
||||
|
||||
/**
|
||||
* @type {Validator[]}
|
||||
* @private
|
||||
*/
|
||||
this.__prevShownValidationResult = [];
|
||||
|
||||
/**
|
||||
* The updated children validity affects the validity of the parent. Helper to recompute
|
||||
* validatity of parent FormGroup
|
||||
* @private
|
||||
*/
|
||||
this.__childModelValueChanged = false;
|
||||
|
||||
/** @private */
|
||||
this.__onValidatorUpdated = this.__onValidatorUpdated.bind(this);
|
||||
/** @protected */
|
||||
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() {
|
||||
|
|
@ -271,28 +333,28 @@ export const ValidateMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* @desc The main function of this mixin. Triggered by:
|
||||
* - a modelValue change
|
||||
* - a change in the 'validators' array
|
||||
* - a change in the config of an individual Validator
|
||||
* Triggered by:
|
||||
* - modelValue change
|
||||
* - change in the 'validators' array
|
||||
* - change in the config of an individual Validator
|
||||
*
|
||||
* 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
|
||||
* 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
|
||||
* '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
|
||||
* 'sync results'.
|
||||
*
|
||||
* - B. There are ResultValidators. After steps A.1, A.2, or A.3 are finished, the holistic
|
||||
* ResultValidators (evaluating the total result of the 'regular' (A.1, A.2 and A.3) validators)
|
||||
* - 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 A.2 and A.3 are not mutually exclusive and can be triggered within one validate()
|
||||
* call. Situation B will occur after every call.
|
||||
* 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]
|
||||
*/
|
||||
|
|
@ -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() {
|
||||
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 {unknown} value
|
||||
* @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 {?} value
|
||||
* @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
|
||||
* @private
|
||||
*/
|
||||
|
|
@ -541,6 +603,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Helper method for the mutually exclusive Required Validator
|
||||
* @param {?} v
|
||||
* @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.
|
||||
*
|
||||
* For `._feedbackNode` (extension of LionValidationFeedback):
|
||||
|
|
@ -640,8 +703,8 @@ export const ValidateMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* The default feedbackCondition condition that will be used when the
|
||||
* feedbackCondition is not overridden.
|
||||
* 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
|
||||
|
|
@ -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
|
||||
*/
|
||||
get _feedbackConditionMeta() {
|
||||
|
|
@ -664,6 +727,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
/**
|
||||
* Allows the end user to specify when a feedback message should be shown
|
||||
* @example
|
||||
* ```js
|
||||
* feedbackCondition(type, meta, defaultCondition) {
|
||||
* if (type === 'info') {
|
||||
* return return;
|
||||
|
|
@ -672,6 +736,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
* }
|
||||
* return defaultCondition(type, meta);
|
||||
* }
|
||||
* ```
|
||||
* @overridable
|
||||
* @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom
|
||||
* Validator type
|
||||
|
|
@ -690,6 +755,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* Used to translate `.hasFeedbackFor` and `.shouldShowFeedbackFor` to `.showsFeedbackFor`
|
||||
* @param {string} type
|
||||
* @protected
|
||||
*/
|
||||
|
|
@ -767,13 +833,12 @@ export const ValidateMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* @overridable
|
||||
* @desc Orders all active validators in this.__validationResult. Can
|
||||
* Orders all active validators in this.__validationResult. Can
|
||||
* also filter out occurrences (based on interaction states)
|
||||
* @overridable
|
||||
* @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
|
||||
* end user
|
||||
*/
|
||||
_prioritizeAndFilterFeedback({ validationResult }) {
|
||||
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`
|
||||
<${tag}></${tag}>
|
||||
`));
|
||||
// @ts-ignore [allow-private] in test
|
||||
expect(el.shouldShowFeedbackFor).to.deep.equal([]);
|
||||
el.submitted = true;
|
||||
await el.updateComplete;
|
||||
// @ts-ignore [allow-private] in test
|
||||
expect(el.shouldShowFeedbackFor).to.deep.equal(['error']);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -570,7 +570,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
|||
const button = /** @type {HTMLButtonElement} */ (await fixture(`<button>Blur</button>`));
|
||||
|
||||
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');
|
||||
button.focus();
|
||||
expect(el.touched).to.equal(
|
||||
|
|
@ -602,8 +602,8 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
|||
outside.click();
|
||||
expect(el.touched, 'unfocused fieldset should stay untouched').to.be.false;
|
||||
|
||||
el.children[1].focus();
|
||||
el.children[2].focus();
|
||||
el.formElements[0].focus();
|
||||
el.formElements[1].focus();
|
||||
expect(el.touched).to.be.false;
|
||||
|
||||
outside.click(); // blur the group via a click
|
||||
|
|
|
|||
|
|
@ -7,16 +7,19 @@ export declare class FocusHost {
|
|||
* Reflects to attribute '[focused]' as a styling hook
|
||||
*/
|
||||
focused: boolean;
|
||||
|
||||
/**
|
||||
* Whether the focusable element within (`._focusableNode`) matches ':focus-visible'
|
||||
* Reflects to attribute '[focused-visible]' as a styling hook
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible
|
||||
*/
|
||||
focusedVisible: boolean;
|
||||
|
||||
/**
|
||||
* Calls `focus()` on focusable element within
|
||||
*/
|
||||
focus(): void;
|
||||
|
||||
/**
|
||||
* Calls `blur()` on focusable element within
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export type ModelValueEventDetails = {
|
|||
* itself to the beginning of formPath)
|
||||
*/
|
||||
formPath: HTMLElement[];
|
||||
|
||||
/**
|
||||
* Sometimes it can be helpful to detect whether a value change was caused by a user or
|
||||
* via a programmatical change.
|
||||
|
|
@ -24,6 +25,7 @@ export type ModelValueEventDetails = {
|
|||
* like 'input'/'change'/'user-input-changed' etc.)
|
||||
*/
|
||||
isTriggeredByUser: boolean;
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
@ -59,19 +61,23 @@ export declare class FormControlHost {
|
|||
_repropagationRole: { 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
|
||||
* 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)
|
||||
* See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly
|
||||
*/
|
||||
readOnly: boolean;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
@ -86,12 +92,14 @@ export declare class FormControlHost {
|
|||
*/
|
||||
get modelValue(): any | Unparseable;
|
||||
set modelValue(value: any | Unparseable);
|
||||
|
||||
/**
|
||||
* The label text for the input node.
|
||||
* When no light dom defined via [slot=label], this value will be used
|
||||
*/
|
||||
get label(): string;
|
||||
set label(arg: string);
|
||||
|
||||
/**
|
||||
* The helpt text for the input node.
|
||||
* 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;
|
||||
set helpText(arg: string);
|
||||
|
||||
/**
|
||||
* Will be used in validation messages to refer to the current field
|
||||
*/
|
||||
set fieldName(arg: string);
|
||||
get fieldName(): string;
|
||||
|
||||
/**
|
||||
* Allows to add extra element references to aria-labelledby attribute.
|
||||
*/
|
||||
addToAriaLabelledBy(
|
||||
element: HTMLElement,
|
||||
customConfig?: {
|
||||
|
|
@ -109,6 +123,10 @@ export declare class FormControlHost {
|
|||
reorder?: boolean | undefined;
|
||||
},
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Allows to add extra element references to aria-describedby attribute.
|
||||
*/
|
||||
addToAriaDescribedBy(
|
||||
element: HTMLElement,
|
||||
customConfig?: {
|
||||
|
|
@ -116,31 +134,71 @@ export declare class FormControlHost {
|
|||
reorder?: boolean | undefined;
|
||||
},
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Allows to remove element references from aria-labelledby attribute.
|
||||
*/
|
||||
removeFromAriaLabelledBy(
|
||||
element: HTMLElement,
|
||||
customConfig?: {
|
||||
reorder?: boolean | undefined;
|
||||
},
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Allows to remove element references from aria-describedby attribute.
|
||||
*/
|
||||
removeFromAriaDescribedBy(
|
||||
element: HTMLElement,
|
||||
customConfig?: {
|
||||
reorder?: boolean | undefined;
|
||||
},
|
||||
): 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;
|
||||
|
||||
/**
|
||||
* Element where label will be rendered to
|
||||
*/
|
||||
protected get _labelNode(): HTMLElement;
|
||||
|
||||
/**
|
||||
* Element where help text will be rendered to
|
||||
*/
|
||||
protected get _helpTextNode(): HTMLElement;
|
||||
|
||||
/**
|
||||
* Element where validation feedback will be rendered to
|
||||
*/
|
||||
protected get _feedbackNode(): LionValidationFeedback;
|
||||
|
||||
/**
|
||||
* Unique id that can be used in all light dom
|
||||
*/
|
||||
protected _inputId: string;
|
||||
|
||||
/**
|
||||
* Contains all elements that should end up in aria-labelledby of `._inputNode`
|
||||
* @type {HTMLElement[]}
|
||||
*/
|
||||
protected _ariaLabelledNodes: HTMLElement[];
|
||||
|
||||
/**
|
||||
* Contains all elements that should end up in aria-describedby of `._inputNode`
|
||||
*/
|
||||
protected _ariaDescribedNodes: HTMLElement[];
|
||||
|
||||
/**
|
||||
* Based on the role, details of handling model-value-changed repropagation differ.
|
||||
*/
|
||||
protected _repropagationRole: 'child' | 'choice-group' | 'fieldset';
|
||||
|
||||
/**
|
||||
* By default, a field with _repropagationRole 'choice-group' will act as an
|
||||
* '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';
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Configuration object that will be available inside the formatter function
|
||||
*/
|
||||
formatOptions: FormatNumberOptions;
|
||||
|
||||
/**
|
||||
* The view value. Will be delegated to `._inputNode.value`
|
||||
*/
|
||||
get 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;
|
||||
|
||||
/**
|
||||
* Whether the user is pasting content. Allows Subclassers to do this in their subclass:
|
||||
* @example
|
||||
* ```js
|
||||
* _reflectBackFormattedValueToUser() {
|
||||
* return super._reflectBackFormattedValueToUser() || this._isPasting;
|
||||
* _reflectBackOn() {
|
||||
* return super._reflectBackOn() || this._isPasting;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
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 _onModelValueChanged(arg: { modelValue: unknown }): 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 _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;
|
||||
|
||||
/**
|
||||
* 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 _onUserInputChanged(): void;
|
||||
protected _callParser(value: string | undefined): object;
|
||||
|
|
|
|||
|
|
@ -14,22 +14,76 @@ type InteractionStates = {
|
|||
prefilled: boolean;
|
||||
};
|
||||
export declare class InteractionStateHost {
|
||||
prefilled: boolean;
|
||||
filled: boolean;
|
||||
/**
|
||||
* True when user has focused and left(blurred) the field.
|
||||
*/
|
||||
touched: boolean;
|
||||
|
||||
/**
|
||||
* True when user has changed the value of the field.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Resets touched and dirty, and recomputes prefilled
|
||||
*/
|
||||
resetInteractionState(): void;
|
||||
|
||||
connectedCallback(): void;
|
||||
disconnectedCallback(): void;
|
||||
|
||||
/**
|
||||
* The event that triggers the touched state
|
||||
*/
|
||||
protected _leaveEvent: string;
|
||||
|
||||
/**
|
||||
* The event that triggers the dirty state
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Sets dirty value and validates when already touched or invalid
|
||||
*/
|
||||
protected _iStateOnValueChange(): void;
|
||||
|
||||
/**
|
||||
* Dispatches event on touched state change
|
||||
*/
|
||||
protected _onTouchedChanged(): void;
|
||||
|
||||
/**
|
||||
* Dispatches event on touched state change
|
||||
*/
|
||||
protected _onDirtyChanged(): void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,27 @@ import { FormControlHost } from '@lion/form-core/types/FormControlMixinTypes';
|
|||
import { FormatHost } from '@lion/form-core/types/FormatMixinTypes';
|
||||
|
||||
export declare class NativeTextFieldHost {
|
||||
/**
|
||||
* Delegates autocomplete to input/textarea
|
||||
*/
|
||||
autocomplete: string;
|
||||
|
||||
/**
|
||||
* Delegates selectionStart to input/textarea
|
||||
*/
|
||||
get selectionStart(): number;
|
||||
set selectionStart(value: number);
|
||||
|
||||
/**
|
||||
* Delegates selectionEnd to input/textarea
|
||||
*/
|
||||
get selectionEnd(): 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>>(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ export declare class ChoiceInputHost {
|
|||
checked: boolean;
|
||||
get modelValue(): 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;
|
||||
set choiceValue(value: any);
|
||||
static get styles(): CSSResultArray;
|
||||
|
|
|
|||
|
|
@ -7,26 +7,132 @@ import { FormRegistrarHost } from '../registration/FormRegistrarMixinTypes';
|
|||
import { ValidateHost } from '../validate/ValidateMixinTypes';
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* True when the group as a whole is blurred (see InteractionStateMixin for more details.)
|
||||
*/
|
||||
touched: boolean;
|
||||
|
||||
/**
|
||||
* True when any of the children is dirty (see InteractionStateMixin for more details.)
|
||||
*/
|
||||
dirty: boolean;
|
||||
|
||||
/**
|
||||
* True when parent form is submitted
|
||||
*/
|
||||
submitted: boolean;
|
||||
|
||||
/**
|
||||
* Object keyed by formElements names, containing formElements' serializedValues
|
||||
*/
|
||||
serializedValue: { [key: string]: any };
|
||||
|
||||
/**
|
||||
* Object keyed by formElements names, containing formElements' formattedValues
|
||||
*/
|
||||
formattedValue: string;
|
||||
children: Array<HTMLElement & FormControlHost>;
|
||||
|
||||
/**
|
||||
* Object keyed by formElements names, containing formElements' modelValues
|
||||
*/
|
||||
get modelValue(): { [x: string]: any };
|
||||
set modelValue(value: { [x: string]: any });
|
||||
|
||||
/**
|
||||
* Resets all interaction states for all formElements
|
||||
*/
|
||||
resetInteractionState(): void;
|
||||
|
||||
/**
|
||||
* Clears all values and resets all interaction states of all FormControls in group,
|
||||
*/
|
||||
clearGroup(): void;
|
||||
|
||||
/**
|
||||
* Handles interaction state 'submitted'.
|
||||
* This allows children to enable visibility of validation feedback
|
||||
*/
|
||||
submitGroup(): void;
|
||||
|
||||
/**
|
||||
* Resets to initial/prefilled values and interaction states of all FormControls in group,
|
||||
*/
|
||||
resetGroup(): void;
|
||||
|
||||
/**
|
||||
* Gathers initial model values of all children. Used when resetGroup() is called.
|
||||
*/
|
||||
protected _initialModelValue: { [x: string]: any };
|
||||
|
||||
/**
|
||||
* The host element with role group (or radigroup or form) containing neccessary aria attributes
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 __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>>(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@ import { FormRegistrarHost } from './FormRegistrarMixinTypes';
|
|||
import { LitElement } from '@lion/core';
|
||||
|
||||
export declare class FormRegisteringHost {
|
||||
/**
|
||||
* The name the host is registered with to a parent
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The registrar this FormControl registers to, Usually a descendant of FormGroup or
|
||||
* ChoiceGroup
|
||||
*/
|
||||
protected _parentFormGroup: FormRegistrarHost | undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,15 @@ export declare class ElementWithParentFormGroup {
|
|||
}
|
||||
|
||||
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 };
|
||||
|
||||
/**
|
||||
* Adds FormControl to `.formElements`
|
||||
*/
|
||||
addFormElement(
|
||||
child:
|
||||
| (FormControlHost & ElementWithParentFormGroup)
|
||||
|
|
@ -17,12 +25,53 @@ export declare class FormRegistrarHost {
|
|||
| (HTMLElement & ElementWithParentFormGroup),
|
||||
indexToInsertAt?: number,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Removes FormControl from `.formElements`
|
||||
*/
|
||||
removeFormElement(child: FormRegisteringHost): void;
|
||||
|
||||
/**
|
||||
* Whether FormControl is part of `.formElements`
|
||||
*/
|
||||
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>;
|
||||
|
||||
/**
|
||||
* initComplete resolves after all pending initialization logic
|
||||
* (for instance `<form-group .serializedValue=${{ child1: 'a', child2: 'b' }}>`)
|
||||
* is executed.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Hook for Subclassers to perform logic before an element is added
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import { Constructor } from '@open-wc/dedupe-mixin';
|
|||
import { LitElement } from '@lion/core';
|
||||
|
||||
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;
|
||||
private __redispatchEventForFormRegistrarPortalMixin(ev: CustomEvent): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,19 @@ export declare interface SyncUpdatableNamespace {
|
|||
}
|
||||
|
||||
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;
|
||||
private __syncUpdatableInitialize(): void;
|
||||
private __SyncUpdatableNamespace: SyncUpdatableNamespace;
|
||||
|
|
|
|||
|
|
@ -18,13 +18,81 @@ type FeedbackMessage = {
|
|||
validator?: Validator;
|
||||
};
|
||||
|
||||
export type ValidationType = 'error' | 'warning' | 'info' | 'success' | string;
|
||||
|
||||
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[];
|
||||
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 } };
|
||||
|
||||
/**
|
||||
* Flag indicating whether async validation is pending.
|
||||
* Creates attribute [is-pending] as a styling hook
|
||||
*/
|
||||
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[];
|
||||
fieldName: string;
|
||||
validateComplete: Promise<void>;
|
||||
|
|
@ -32,19 +100,97 @@ export declare class ValidateHost {
|
|||
|
||||
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;
|
||||
|
||||
/**
|
||||
* The amount of feedback messages that will visible in LionValidationFeedback
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Used to translate `.hasFeedbackFor` and `.shouldShowFeedbackFor` to `.showsFeedbackFor`
|
||||
*/
|
||||
protected _hasFeedbackVisibleFor(type: string): boolean;
|
||||
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 updateSync(name: string, oldValue: unknown): void;
|
||||
|
||||
private __syncValidationResult: Validator[];
|
||||
private __asyncValidationResult: Validator[];
|
||||
|
|
|
|||
|
|
@ -119,7 +119,9 @@ export class LionInputRange extends LionInput {
|
|||
_inputGroupTemplate() {
|
||||
return html`
|
||||
<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>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ describe('<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');
|
||||
/** @type {LionRadio} */ (el.children[1]).checked = 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';
|
||||
|
||||
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}
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue