lion/packages/choice-input/src/ChoiceGroupMixin.js
Joren Broekema e397f8d68b feat(field): align (pre)filled and empty, fix filled not working
BREAKING: _isPrefilled was removed in favor of _isEmpty. We used to have both, but we decided to align, because they basically do the same thing but opposite. If you were using _isPrefilled, switch to using _isEmpty and just use it in reverse. This change also makes _isEmpty available to all things that implement FormControlMixin. Lastly, filled is available now on all fields that implement InteractionStateMixin
2020-03-25 17:02:10 +01:00

189 lines
6.4 KiB
JavaScript

import { dedupeMixin } from '@lion/core';
import { FormRegistrarMixin, InteractionStateMixin } from '@lion/field';
export const ChoiceGroupMixin = dedupeMixin(
superclass =>
// eslint-disable-next-line
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.
* @type {boolean}
*/
multipleChoice: {
type: Boolean,
attribute: 'multiple-choice',
},
};
}
get modelValue() {
const elems = this._getCheckedElements();
if (this.multipleChoice) {
return elems.map(el => el.modelValue.value);
}
return elems[0] ? elems[0].modelValue.value : '';
}
set modelValue(value) {
this._setCheckedElements(value, (el, val) => el.modelValue.value === val);
}
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) {
this._setCheckedElements(value, (el, val) => el.serializedValue.value === val);
}
constructor() {
super();
this.multipleChoice = false;
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin
}
/**
* @override from FormRegistrarMixin
*/
addFormElement(child, indexToInsertAt) {
this._throwWhenInvalidChildModelValue(child);
this.__delegateNameAttribute(child);
super.addFormElement(child, indexToInsertAt);
}
/**
* @override
*/
_getFromAllFormElements(property, filterCondition = () => true) {
// For modelValue and serializedValue, an exception should be made,
// The reset can be requested from children
if (property === 'modelValue' || property === 'serializedValue') {
return this[property];
}
return this.formElements.filter(filterCondition).map(el => el.property);
}
_throwWhenInvalidChildModelValue(child) {
if (
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;
}
_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 out by default
return this.formElements.filter(el => el.checked && !el.disabled);
}
async _setCheckedElements(value, check) {
if (!this.__readyForRegistration) {
await this.registrationReady;
}
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;
}
}
}
__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;
}
}
__delegateNameAttribute(child) {
if (!child.name || child.name === this.name) {
// eslint-disable-next-line no-param-reassign
child.name = this.name;
} else {
throw new Error(
`The ${this.tagName.toLowerCase()} name="${
this.name
}" does not allow to register ${child.tagName.toLowerCase()} with custom names (name="${
child.name
}" given)`,
);
}
}
/**
* @override FormControlMixin
*/
_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');
}
},
);