325 lines
11 KiB
JavaScript
325 lines
11 KiB
JavaScript
import { dedupeMixin } from '@lion/core';
|
|
import { FormRegistrarMixin } from '../registration/FormRegistrarMixin.js';
|
|
import { InteractionStateMixin } from '../InteractionStateMixin.js';
|
|
|
|
/**
|
|
* @typedef {import('../../types/choice-group/ChoiceGroupMixinTypes').ChoiceGroupMixin} ChoiceGroupMixin
|
|
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
|
* @typedef {FormControlHost & HTMLElement & {_parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
|
* @typedef {import('../../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} ChoiceInputHost
|
|
*/
|
|
|
|
/**
|
|
* @type {ChoiceGroupMixin}
|
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
|
*/
|
|
const ChoiceGroupMixinImplementation = superclass =>
|
|
// @ts-expect-error false positive for incompatible static get properties. Lit-element merges super properties already for you.
|
|
class ChoiceGroupMixin extends FormRegistrarMixin(InteractionStateMixin(superclass)) {
|
|
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',
|
|
},
|
|
};
|
|
}
|
|
|
|
get modelValue() {
|
|
const elems = this._getCheckedElements();
|
|
if (this.multipleChoice) {
|
|
return elems.map(el => el.choiceValue);
|
|
}
|
|
return elems[0] ? elems[0].choiceValue : '';
|
|
}
|
|
|
|
set modelValue(value) {
|
|
/**
|
|
* @param {ChoiceInputHost} el
|
|
* @param {any} val
|
|
*/
|
|
const checkCondition = (el, val) => el.choiceValue === val;
|
|
|
|
if (this.__isInitialModelValue) {
|
|
this.__isInitialModelValue = false;
|
|
this.registrationComplete.then(() => {
|
|
this._setCheckedElements(value, checkCondition);
|
|
this.requestUpdate('modelValue', this.__oldModelValue);
|
|
});
|
|
} else {
|
|
this._setCheckedElements(value, checkCondition);
|
|
this.requestUpdate('modelValue', this.__oldModelValue);
|
|
}
|
|
this.__oldModelValue = this.modelValue;
|
|
}
|
|
|
|
get serializedValue() {
|
|
// We want to filter out disabled values out by default:
|
|
// The goal of serializing values could either be submitting state to a backend
|
|
// ot storing state in a backend. For this, only values that are entered by the end
|
|
// user are relevant, choice values are always defined by the Application Developer
|
|
// and known by the backend.
|
|
|
|
// Assuming values are always defined as strings, modelValues and serializedValues
|
|
// are the same.
|
|
const elems = this._getCheckedElements();
|
|
if (this.multipleChoice) {
|
|
return elems.map(el => el.serializedValue.value);
|
|
}
|
|
return elems[0] ? elems[0].serializedValue.value : '';
|
|
}
|
|
|
|
set serializedValue(value) {
|
|
/**
|
|
* @param {ChoiceInputHost} el
|
|
* @param {string} val
|
|
*/
|
|
const checkCondition = (el, val) => el.serializedValue.value === val;
|
|
|
|
if (this.__isInitialSerializedValue) {
|
|
this.__isInitialSerializedValue = false;
|
|
this.registrationComplete.then(() => {
|
|
this._setCheckedElements(value, checkCondition);
|
|
this.requestUpdate('serializedValue');
|
|
});
|
|
} else {
|
|
this._setCheckedElements(value, checkCondition);
|
|
this.requestUpdate('serializedValue');
|
|
}
|
|
}
|
|
|
|
get formattedValue() {
|
|
const elems = this._getCheckedElements();
|
|
if (this.multipleChoice) {
|
|
return elems.map(el => el.formattedValue);
|
|
}
|
|
return elems[0] ? elems[0].formattedValue : '';
|
|
}
|
|
|
|
set formattedValue(value) {
|
|
/**
|
|
* @param {{ formattedValue: string }} el
|
|
* @param {string} val
|
|
*/
|
|
const checkCondition = (el, val) => el.formattedValue === val;
|
|
|
|
if (this.__isInitialFormattedValue) {
|
|
this.__isInitialFormattedValue = false;
|
|
this.registrationComplete.then(() => {
|
|
this._setCheckedElements(value, checkCondition);
|
|
});
|
|
} else {
|
|
this._setCheckedElements(value, checkCondition);
|
|
}
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.multipleChoice = false;
|
|
/** @type {'child'|'choice-group'|'fieldset'} */
|
|
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin
|
|
|
|
this.__isInitialModelValue = true;
|
|
this.__isInitialSerializedValue = true;
|
|
this.__isInitialFormattedValue = true;
|
|
/** @type {Promise<any> & {done?:boolean}} */
|
|
this.registrationComplete = new Promise((resolve, reject) => {
|
|
this.__resolveRegistrationComplete = resolve;
|
|
this.__rejectRegistrationComplete = reject;
|
|
});
|
|
this.registrationComplete.done = false;
|
|
this.registrationComplete.then(
|
|
() => {
|
|
this.registrationComplete.done = true;
|
|
},
|
|
() => {
|
|
this.registrationComplete.done = true;
|
|
throw new Error(
|
|
'Registration could not finish. Please use await el.registrationComplete;',
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
// Double microtask queue to account for Webkit race condition
|
|
Promise.resolve().then(() =>
|
|
Promise.resolve().then(() => this.__resolveRegistrationComplete()),
|
|
);
|
|
|
|
this.registrationComplete.then(() => {
|
|
this.__isInitialModelValue = false;
|
|
this.__isInitialSerializedValue = false;
|
|
this.__isInitialFormattedValue = false;
|
|
});
|
|
}
|
|
|
|
/** @param {import('@lion/core').PropertyValues} changedProperties */
|
|
updated(changedProperties) {
|
|
super.updated(changedProperties);
|
|
if (changedProperties.has('name') && this.name !== changedProperties.get('name')) {
|
|
this.formElements.forEach(child => {
|
|
// eslint-disable-next-line no-param-reassign
|
|
child.name = this.name;
|
|
});
|
|
}
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
super.disconnectedCallback();
|
|
|
|
if (this.registrationComplete.done === false) {
|
|
this.__rejectRegistrationComplete();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override from FormRegistrarMixin
|
|
* @param {FormControl} child
|
|
* @param {number} indexToInsertAt
|
|
*/
|
|
addFormElement(child, indexToInsertAt) {
|
|
this._throwWhenInvalidChildModelValue(child);
|
|
// eslint-disable-next-line no-param-reassign
|
|
child.name = this.name;
|
|
super.addFormElement(child, indexToInsertAt);
|
|
}
|
|
|
|
/**
|
|
* @override from FormControlMixin
|
|
*/
|
|
_triggerInitialModelValueChangedEvent() {
|
|
this.registrationComplete.then(() => {
|
|
this.__dispatchInitialModelValueChangedEvent();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {string} property
|
|
*/
|
|
_getFromAllFormElements(property, filterCondition = () => true) {
|
|
// For modelValue, serializedValue and formattedValue, an exception should be made,
|
|
// The reset can be requested from children
|
|
if (
|
|
property === 'modelValue' ||
|
|
property === 'serializedValue' ||
|
|
property === 'formattedValue'
|
|
) {
|
|
return this[property];
|
|
}
|
|
return this.formElements.filter(filterCondition).map(el => el.property);
|
|
}
|
|
|
|
/**
|
|
* @param {FormControl} child
|
|
*/
|
|
_throwWhenInvalidChildModelValue(child) {
|
|
if (
|
|
// @ts-expect-error
|
|
typeof child.modelValue.checked !== 'boolean' ||
|
|
!Object.prototype.hasOwnProperty.call(child.modelValue, 'value')
|
|
) {
|
|
throw new Error(
|
|
`The ${this.tagName.toLowerCase()} name="${
|
|
this.name
|
|
}" does not allow to register ${child.tagName.toLowerCase()} with .modelValue="${
|
|
child.modelValue
|
|
}" - The modelValue should represent an Object { value: "foo", checked: false }`,
|
|
);
|
|
}
|
|
}
|
|
|
|
_isEmpty() {
|
|
if (this.multipleChoice) {
|
|
return this.modelValue.length === 0;
|
|
}
|
|
|
|
if (typeof this.modelValue === 'string' && this.modelValue === '') {
|
|
return true;
|
|
}
|
|
if (this.modelValue === undefined || this.modelValue === null) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {CustomEvent & {target:FormControl}} ev
|
|
*/
|
|
_checkSingleChoiceElements(ev) {
|
|
const { target } = ev;
|
|
if (target.checked === false) return;
|
|
|
|
const groupName = target.name;
|
|
this.formElements
|
|
.filter(i => i.name === groupName)
|
|
.forEach(choice => {
|
|
if (choice !== target) {
|
|
choice.checked = false; // eslint-disable-line no-param-reassign
|
|
}
|
|
});
|
|
// this.__triggerCheckedValueChanged();
|
|
}
|
|
|
|
_getCheckedElements() {
|
|
// We want to filter out disabled values by default
|
|
return this.formElements.filter(el => el.checked && !el.disabled);
|
|
}
|
|
|
|
/**
|
|
* @param {string | any[]} value
|
|
* @param {Function} check
|
|
*/
|
|
_setCheckedElements(value, check) {
|
|
for (let i = 0; i < this.formElements.length; i += 1) {
|
|
if (this.multipleChoice) {
|
|
this.formElements[i].checked = value.includes(this.formElements[i].value);
|
|
} else if (check(this.formElements[i], value)) {
|
|
// Allows checking against custom values e.g. formattedValue or serializedValue
|
|
this.formElements[i].checked = true;
|
|
} else {
|
|
this.formElements[i].checked = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
__setChoiceGroupTouched() {
|
|
const value = this.modelValue;
|
|
if (value != null && value !== this.__previousCheckedValue) {
|
|
// TODO: what happens here exactly? Needs to be based on user interaction (?)
|
|
this.touched = true;
|
|
this.__previousCheckedValue = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override FormControlMixin
|
|
* @param {CustomEvent} ev
|
|
*/
|
|
_onBeforeRepropagateChildrenValues(ev) {
|
|
// Normalize target, since we might receive 'portal events' (from children in a modal,
|
|
// see select-rich)
|
|
const target = (ev.detail && ev.detail.element) || ev.target;
|
|
if (this.multipleChoice || !target.checked) {
|
|
return;
|
|
}
|
|
this.formElements.forEach(option => {
|
|
if (target.choiceValue !== option.choiceValue) {
|
|
option.checked = false; // eslint-disable-line no-param-reassign
|
|
}
|
|
});
|
|
this.__setChoiceGroupTouched();
|
|
this.requestUpdate('modelValue', this.__oldModelValue);
|
|
this.__oldModelValue = this.modelValue;
|
|
}
|
|
};
|
|
|
|
export const ChoiceGroupMixin = dedupeMixin(ChoiceGroupMixinImplementation);
|