feat: api normalisation formElements and values
This commit is contained in:
parent
417b37a616
commit
9b905c492a
22 changed files with 1120 additions and 767 deletions
|
|
@ -1,12 +1,13 @@
|
|||
import { LitElement } from '@lion/core';
|
||||
import { ChoiceGroupMixin } from '@lion/choice-input';
|
||||
import { LionFieldset } from '@lion/fieldset';
|
||||
import { FormGroupMixin } from '@lion/fieldset';
|
||||
|
||||
/**
|
||||
* A wrapper around multiple checkboxes
|
||||
*
|
||||
* @extends {LionFieldset}
|
||||
*/
|
||||
export class LionCheckboxGroup extends ChoiceGroupMixin(LionFieldset) {
|
||||
export class LionCheckboxGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {
|
||||
constructor() {
|
||||
super();
|
||||
this.multipleChoice = true;
|
||||
|
|
|
|||
|
|
@ -5,12 +5,30 @@ 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) {
|
||||
// TODO: holds for both modelValue and serializedValue of choiceInput:
|
||||
// consider only allowing strings as values, in which case 'el.value' would suffice
|
||||
// and choice-input could be simplified
|
||||
return elems.map(el => el.modelValue.value);
|
||||
}
|
||||
return elems ? elems.modelValue.value : '';
|
||||
return elems[0] ? elems[0].modelValue.value : '';
|
||||
}
|
||||
|
||||
set modelValue(value) {
|
||||
|
|
@ -18,11 +36,19 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
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 this.modelValue;
|
||||
return elems.map(el => el.serializedValue.value);
|
||||
}
|
||||
return elems ? elems.serializedValue : '';
|
||||
return elems[0] ? elems[0].serializedValue.value : '';
|
||||
}
|
||||
|
||||
set serializedValue(value) {
|
||||
|
|
@ -53,24 +79,22 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
*/
|
||||
addFormElement(child, indexToInsertAt) {
|
||||
this._throwWhenInvalidChildModelValue(child);
|
||||
// TODO: nice to have or does it have a function (since names are meant as keys for
|
||||
// formElements)?
|
||||
this.__delegateNameAttribute(child);
|
||||
super.addFormElement(child, indexToInsertAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override from LionFieldset
|
||||
* @override
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get _childrenCanHaveSameName() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override from LionFieldset
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get _childNamesCanBeDuplicate() {
|
||||
return true;
|
||||
_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) {
|
||||
|
|
@ -108,7 +132,7 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
if (target.checked === false) return;
|
||||
|
||||
const groupName = target.name;
|
||||
this.formElementsArray
|
||||
this.formElements
|
||||
.filter(i => i.name === groupName)
|
||||
.forEach(choice => {
|
||||
if (choice !== target) {
|
||||
|
|
@ -119,11 +143,8 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
_getCheckedElements() {
|
||||
const filtered = this.formElementsArray.filter(el => el.checked === true);
|
||||
if (this.multipleChoice) {
|
||||
return filtered;
|
||||
}
|
||||
return filtered.length > 0 ? filtered[0] : undefined;
|
||||
// We want to filter out disabled values out by default
|
||||
return this.formElements.filter(el => el.checked && !el.disabled);
|
||||
}
|
||||
|
||||
async _setCheckedElements(value, check) {
|
||||
|
|
@ -131,12 +152,12 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
await this.registrationReady;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.formElementsArray.length; i += 1) {
|
||||
for (let i = 0; i < this.formElements.length; i += 1) {
|
||||
if (this.multipleChoice) {
|
||||
this.formElementsArray[i].checked = value.includes(this.formElementsArray[i].value);
|
||||
} else if (check(this.formElementsArray[i], value)) {
|
||||
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.formElementsArray[i].checked = true;
|
||||
this.formElements[i].checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
import { html } from '@lion/core';
|
||||
import { LionFieldset } from '@lion/fieldset';
|
||||
import { html, LitElement } from '@lion/core';
|
||||
import { FormGroupMixin } from '@lion/fieldset';
|
||||
import { LionInput } from '@lion/input';
|
||||
import { Required } from '@lion/validate';
|
||||
import { expect, fixture, nextFrame } from '@open-wc/testing';
|
||||
import { ChoiceGroupMixin } from '../src/ChoiceGroupMixin.js';
|
||||
import { ChoiceInputMixin } from '../src/ChoiceInputMixin.js';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
|
||||
describe('ChoiceGroupMixin', () => {
|
||||
before(() => {
|
||||
class ChoiceInput extends ChoiceInputMixin(LionInput) {}
|
||||
customElements.define('choice-group-input', ChoiceInput);
|
||||
|
||||
class ChoiceGroup extends ChoiceGroupMixin(LionFieldset) {}
|
||||
class ChoiceGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {}
|
||||
customElements.define('choice-group', ChoiceGroup);
|
||||
|
||||
class ChoiceGroupMultiple extends ChoiceGroupMixin(LionFieldset) {
|
||||
class ChoiceGroupMultiple extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {
|
||||
constructor() {
|
||||
super();
|
||||
this.multipleChoice = true;
|
||||
|
|
@ -33,9 +34,9 @@ describe('ChoiceGroupMixin', () => {
|
|||
`);
|
||||
await nextFrame();
|
||||
expect(el.modelValue).to.equal('female');
|
||||
el.formElementsArray[0].checked = true;
|
||||
el.formElements[0].checked = true;
|
||||
expect(el.modelValue).to.equal('male');
|
||||
el.formElementsArray[2].checked = true;
|
||||
el.formElements[2].checked = true;
|
||||
expect(el.modelValue).to.equal('other');
|
||||
});
|
||||
|
||||
|
|
@ -68,15 +69,15 @@ describe('ChoiceGroupMixin', () => {
|
|||
`);
|
||||
await nextFrame();
|
||||
|
||||
expect(el.formElementsArray[0].name).to.equal('gender');
|
||||
expect(el.formElementsArray[1].name).to.equal('gender');
|
||||
expect(el.formElements[0].name).to.equal('gender');
|
||||
expect(el.formElements[1].name).to.equal('gender');
|
||||
|
||||
const validChild = await fixture(html`
|
||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||
`);
|
||||
el.appendChild(validChild);
|
||||
|
||||
expect(el.formElementsArray[2].name).to.equal('gender');
|
||||
expect(el.formElements[2].name).to.equal('gender');
|
||||
});
|
||||
|
||||
it('throws if a child element with a different name than the group tries to register', async () => {
|
||||
|
|
@ -112,7 +113,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
await el.updateComplete;
|
||||
|
||||
expect(el.modelValue).to.equal('other');
|
||||
expect(el.formElementsArray[2].checked).to.be.true;
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
});
|
||||
|
||||
it('can handle complex data via choiceValue', async () => {
|
||||
|
|
@ -127,7 +128,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
await nextFrame();
|
||||
|
||||
expect(el.modelValue).to.equal(date);
|
||||
el.formElementsArray[0].checked = true;
|
||||
el.formElements[0].checked = true;
|
||||
expect(el.modelValue).to.deep.equal({ some: 'data' });
|
||||
});
|
||||
|
||||
|
|
@ -141,7 +142,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
await nextFrame();
|
||||
|
||||
expect(el.modelValue).to.equal(0);
|
||||
el.formElementsArray[1].checked = true;
|
||||
el.formElements[1].checked = true;
|
||||
expect(el.modelValue).to.equal('');
|
||||
});
|
||||
|
||||
|
|
@ -160,7 +161,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
await nextFrame();
|
||||
expect(el.modelValue).to.equal('female');
|
||||
el.modelValue = 'other';
|
||||
expect(el.formElementsArray[2].checked).to.be.true;
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
});
|
||||
|
||||
it('expect child nodes to only fire one model-value-changed event per instance', async () => {
|
||||
|
|
@ -180,14 +181,14 @@ describe('ChoiceGroupMixin', () => {
|
|||
await nextFrame();
|
||||
counter = 0; // reset after setup which may result in different results
|
||||
|
||||
el.formElementsArray[0].checked = true;
|
||||
el.formElements[0].checked = true;
|
||||
expect(counter).to.equal(2); // male becomes checked, female becomes unchecked
|
||||
|
||||
// not changed values trigger no event
|
||||
el.formElementsArray[0].checked = true;
|
||||
el.formElements[0].checked = true;
|
||||
expect(counter).to.equal(2);
|
||||
|
||||
el.formElementsArray[2].checked = true;
|
||||
el.formElements[2].checked = true;
|
||||
expect(counter).to.equal(4); // other becomes checked, male becomes unchecked
|
||||
|
||||
// not found values trigger no event
|
||||
|
|
@ -211,12 +212,12 @@ describe('ChoiceGroupMixin', () => {
|
|||
expect(el.validationStates).to.have.a.property('error');
|
||||
expect(el.validationStates.error).to.have.a.property('Required');
|
||||
|
||||
el.formElementsArray[0].checked = true;
|
||||
el.formElements[0].checked = true;
|
||||
expect(el.hasFeedbackFor).not.to.include('error');
|
||||
expect(el.validationStates).to.have.a.property('error');
|
||||
expect(el.validationStates.error).not.to.have.a.property('Required');
|
||||
|
||||
el.formElementsArray[1].checked = true;
|
||||
el.formElements[1].checked = true;
|
||||
expect(el.hasFeedbackFor).not.to.include('error');
|
||||
expect(el.validationStates).to.have.a.property('error');
|
||||
expect(el.validationStates.error).not.to.have.a.property('Required');
|
||||
|
|
@ -229,8 +230,8 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
el.formElementsArray[0].checked = true;
|
||||
expect(el.serializedValue).to.deep.equal({ checked: true, value: 'male' });
|
||||
el.formElements[0].checked = true;
|
||||
expect(el.serializedValue).to.deep.equal('male');
|
||||
});
|
||||
|
||||
it('returns serialized value on unchecked state', async () => {
|
||||
|
|
@ -256,9 +257,9 @@ describe('ChoiceGroupMixin', () => {
|
|||
`);
|
||||
await nextFrame();
|
||||
expect(el.modelValue).to.eql(['female']);
|
||||
el.formElementsArray[0].checked = true;
|
||||
el.formElements[0].checked = true;
|
||||
expect(el.modelValue).to.eql(['male', 'female']);
|
||||
el.formElementsArray[2].checked = true;
|
||||
el.formElements[2].checked = true;
|
||||
expect(el.modelValue).to.eql(['male', 'female', 'other']);
|
||||
});
|
||||
|
||||
|
|
@ -276,8 +277,8 @@ describe('ChoiceGroupMixin', () => {
|
|||
await el.updateComplete;
|
||||
el.modelValue = ['male', 'other'];
|
||||
expect(el.modelValue).to.eql(['male', 'other']);
|
||||
expect(el.formElementsArray[0].checked).to.be.true;
|
||||
expect(el.formElementsArray[2].checked).to.be.true;
|
||||
expect(el.formElements[0].checked).to.be.true;
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
});
|
||||
|
||||
it('unchecks non-matching checkboxes when setting the modelValue', async () => {
|
||||
|
|
@ -293,13 +294,40 @@ describe('ChoiceGroupMixin', () => {
|
|||
await el.registrationReady;
|
||||
await el.updateComplete;
|
||||
expect(el.modelValue).to.eql(['male', 'other']);
|
||||
expect(el.formElementsArray[0].checked).to.be.true;
|
||||
expect(el.formElementsArray[2].checked).to.be.true;
|
||||
expect(el.formElements[0].checked).to.be.true;
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
|
||||
el.modelValue = ['female'];
|
||||
expect(el.formElementsArray[0].checked).to.be.false;
|
||||
expect(el.formElementsArray[1].checked).to.be.true;
|
||||
expect(el.formElementsArray[2].checked).to.be.false;
|
||||
expect(el.formElements[0].checked).to.be.false;
|
||||
expect(el.formElements[1].checked).to.be.true;
|
||||
expect(el.formElements[2].checked).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration with a parent form/fieldset', () => {
|
||||
it('will serialize all children with their serializedValue', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-fieldset>
|
||||
<choice-group name="gender">
|
||||
<choice-group-input .choiceValue=${'male'} checked disabled></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
</lion-fieldset>
|
||||
`);
|
||||
|
||||
await nextFrame();
|
||||
await el.registrationReady;
|
||||
await el.updateComplete;
|
||||
expect(el.serializedValue).to.eql({
|
||||
gender: 'female',
|
||||
});
|
||||
|
||||
const choiceGroupEl = el.querySelector('[name="gender"]');
|
||||
choiceGroupEl.multipleChoice = true;
|
||||
expect(el.serializedValue).to.eql({
|
||||
gender: ['female'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
export { FieldCustomMixin } from './src/FieldCustomMixin.js';
|
||||
export { FocusMixin } from './src/FocusMixin.js';
|
||||
export { FormatMixin } from './src/FormatMixin.js';
|
||||
export { FieldCustomMixin } from './src/FieldCustomMixin.js';
|
||||
export { FormControlMixin } from './src/FormControlMixin.js';
|
||||
export { InteractionStateMixin } from './src/InteractionStateMixin.js'; // applies FocusMixin
|
||||
export { LionField } from './src/LionField.js';
|
||||
export { FormRegisteringMixin } from './src/FormRegisteringMixin.js';
|
||||
export { FormRegistrarMixin } from './src/FormRegistrarMixin.js';
|
||||
export { FormRegistrarPortalMixin } from './src/FormRegistrarPortalMixin.js';
|
||||
export { FormRegisteringMixin } from './src/registration/FormRegisteringMixin.js';
|
||||
export { FormRegistrarMixin } from './src/registration/FormRegistrarMixin.js';
|
||||
export { FormRegistrarPortalMixin } from './src/registration/FormRegistrarPortalMixin.js';
|
||||
export { FormControlsCollection } from './src/registration/FormControlsCollection.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { html, css, nothing, dedupeMixin, SlotMixin } from '@lion/core';
|
||||
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
||||
import { FormRegisteringMixin } from './registration/FormRegisteringMixin.js';
|
||||
import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js';
|
||||
|
||||
/**
|
||||
|
|
@ -28,11 +28,18 @@ export const FormControlMixin = dedupeMixin(
|
|||
class FormControlMixin extends FormRegisteringMixin(SlotMixin(superclass)) {
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* The name the element will be registered on to the .formElements collection
|
||||
* of the parent.
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
reflect: true,
|
||||
},
|
||||
/**
|
||||
* When no light dom defined and prop set
|
||||
*/
|
||||
label: String,
|
||||
|
||||
/**
|
||||
* When no light dom defined and prop set
|
||||
*/
|
||||
|
|
@ -40,12 +47,10 @@ export const FormControlMixin = dedupeMixin(
|
|||
type: String,
|
||||
attribute: 'help-text',
|
||||
},
|
||||
|
||||
/**
|
||||
* Contains all elements that should end up in aria-labelledby of `._inputNode`
|
||||
*/
|
||||
_ariaLabelledNodes: Array,
|
||||
|
||||
/**
|
||||
* Contains all elements that should end up in aria-describedby of `._inputNode`
|
||||
*/
|
||||
|
|
@ -73,6 +78,14 @@ export const FormControlMixin = dedupeMixin(
|
|||
this.requestUpdate('helpText', oldValue);
|
||||
}
|
||||
|
||||
set fieldName(value) {
|
||||
this.__fieldName = value;
|
||||
}
|
||||
|
||||
get fieldName() {
|
||||
return this.__fieldName || this.label || this.name;
|
||||
}
|
||||
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
|
|
@ -146,9 +159,6 @@ export const FormControlMixin = dedupeMixin(
|
|||
this._enhanceLightDomA11y();
|
||||
}
|
||||
|
||||
/**
|
||||
* Public methods
|
||||
*/
|
||||
_enhanceLightDomClasses() {
|
||||
if (this._inputNode) {
|
||||
this._inputNode.classList.add('form-control');
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
import { dedupeMixin } from '@lion/core';
|
||||
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
||||
import { formRegistrarManager } from './formRegistrarManager.js';
|
||||
|
||||
/**
|
||||
* This allows an element to become the manager of a register
|
||||
*/
|
||||
export const FormRegistrarMixin = dedupeMixin(
|
||||
superclass =>
|
||||
// eslint-disable-next-line no-shadow, no-unused-vars
|
||||
class FormRegistrarMixin extends FormRegisteringMixin(superclass) {
|
||||
get formElements() {
|
||||
return this.__formElements;
|
||||
}
|
||||
|
||||
set formElements(value) {
|
||||
this.__formElements = value;
|
||||
}
|
||||
|
||||
get formElementsArray() {
|
||||
return this.__formElements;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.formElements = [];
|
||||
this.__readyForRegistration = false;
|
||||
this.__hasBeenRendered = false;
|
||||
this.registrationReady = new Promise(resolve => {
|
||||
this.__resolveRegistrationReady = resolve;
|
||||
});
|
||||
|
||||
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
||||
this.addEventListener('form-element-register', this._onRequestToAddFormElement);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
formRegistrarManager.add(this);
|
||||
if (this.__hasBeenRendered) {
|
||||
formRegistrarManager.becomesReady();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
formRegistrarManager.remove(this);
|
||||
}
|
||||
|
||||
isRegisteredFormElement(el) {
|
||||
return this.formElementsArray.some(exitingEl => exitingEl === el);
|
||||
}
|
||||
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.__resolveRegistrationReady();
|
||||
this.__readyForRegistration = true;
|
||||
formRegistrarManager.becomesReady();
|
||||
this.__hasBeenRendered = true;
|
||||
}
|
||||
|
||||
addFormElement(child, index) {
|
||||
// This is a way to let the child element (a lion-fieldset or lion-field) know, about its parent
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
child.__parentFormGroup = this;
|
||||
|
||||
if (index > 0) {
|
||||
this.formElements.splice(index, 0, child);
|
||||
} else {
|
||||
this.formElements.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
removeFormElement(child) {
|
||||
const index = this.formElements.indexOf(child);
|
||||
if (index > -1) {
|
||||
this.formElements.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
_onRequestToAddFormElement(ev) {
|
||||
const child = ev.detail.element;
|
||||
if (child === this) {
|
||||
// as we fire and listen - don't add ourselves
|
||||
return;
|
||||
}
|
||||
if (this.isRegisteredFormElement(child)) {
|
||||
// do not readd already existing elements
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
|
||||
// Check for siblings to determine the right order to insert into formElements
|
||||
// If there is no next sibling, index is -1
|
||||
let indexToInsertAt = -1;
|
||||
if (this.formElements && Array.isArray(this.formElements)) {
|
||||
indexToInsertAt = this.formElements.indexOf(child.nextElementSibling);
|
||||
}
|
||||
this.addFormElement(child, indexToInsertAt);
|
||||
}
|
||||
|
||||
_onRequestToRemoveFormElement(ev) {
|
||||
const child = ev.detail.element;
|
||||
if (child === this) {
|
||||
// as we fire and listen - don't add ourselves
|
||||
return;
|
||||
}
|
||||
if (!this.isRegisteredFormElement(child)) {
|
||||
// do not readd already existing elements
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
|
||||
this.removeFormElement(child);
|
||||
}
|
||||
},
|
||||
);
|
||||
98
packages/field/src/registration/FormControlsCollection.js
Normal file
98
packages/field/src/registration/FormControlsCollection.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* @desc 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
|
||||
* // This is how a native form works:
|
||||
* <form>
|
||||
* <input id="a" name="a">
|
||||
* <fieldset>
|
||||
* <input id="b1" name="b[]">
|
||||
* <input id="b2" name="b[]">
|
||||
* <input id="c" name="c">
|
||||
* </fieldset>
|
||||
* <select id="d" name="d">
|
||||
* <option></option>
|
||||
* </select>
|
||||
* <fieldset>
|
||||
* <input type="radio" id="e1" name="e">
|
||||
* <input type="radio" id="e2" name="e">
|
||||
* </fieldset>
|
||||
* <select id="f" name="f" multiple>
|
||||
* <option></option>
|
||||
* </select>
|
||||
* <fieldset>
|
||||
* <input type="checkbox" id="g1" name="g">
|
||||
* <input type="checkbox" id="g2" name="g">
|
||||
* </fieldset>
|
||||
* </form>
|
||||
*
|
||||
* form.elements[0]; // Element input#a
|
||||
* form.elements[1]; // Element input#b1
|
||||
* form.elements[2]; // Element input#b2
|
||||
* form.elements[3]; // Element input#c
|
||||
* form.elements.a; // Element input#a
|
||||
* form.elements.b; // RadioNodeList<Element> [input#b1, input#b2]
|
||||
* form.elements.c; // input#c
|
||||
*
|
||||
* // This is how a Lion form works (for simplicity Lion components have the 'l'-prefix):
|
||||
* <l-form>
|
||||
* <form>
|
||||
*
|
||||
* <!-- fields -->
|
||||
*
|
||||
* <l-input id="a" name="a"></l-input>
|
||||
*
|
||||
*
|
||||
* <!-- field sets ('sub forms') -->
|
||||
*
|
||||
* <l-fieldset>
|
||||
* <l-input id="b1" name="b"</l-input>
|
||||
* <l-input id="b2" name="b"></l-input>
|
||||
* <l-input id="c" name="c"></l-input>
|
||||
* </l-fieldset>
|
||||
*
|
||||
*
|
||||
* <!-- choice groups (children are 'end points') -->
|
||||
*
|
||||
* <!-- single selection choice groups -->
|
||||
* <l-select id="d" name="d">
|
||||
* <l-option></l-option>
|
||||
* </l-select>
|
||||
* <l-radio-group id="e" name="e">
|
||||
* <l-radio></l-radio>
|
||||
* <l-radio></l-radio>
|
||||
* </l-radio-group>
|
||||
*
|
||||
* <!-- multi selection choice groups -->
|
||||
* <l-select id="f" name="f" multiple>
|
||||
* <l-option></l-option>
|
||||
* </l-select>
|
||||
* <l-checkbox-group id="g" name="g">
|
||||
* <l-checkbox></l-checkbox>
|
||||
* <l-checkbox></l-checkbox>
|
||||
* </l-checkbox-group>
|
||||
*
|
||||
* </form>
|
||||
* </l-form>
|
||||
*
|
||||
* lionForm.formElements[0]; // Element l-input#a
|
||||
* lionForm.formElements[1]; // Element l-input#b1
|
||||
* lionForm.formElements[2]; // Element l-input#b2
|
||||
* lionForm.formElements.a; // Element l-input#a
|
||||
* lionForm.formElements['b[]']; // Array<Element> [l-input#b1, l-input#b2]
|
||||
* lionForm.formElements.c; // Element l-input#c
|
||||
*
|
||||
* lionForm.formElements[d-g].formElements; // Array<Element>
|
||||
*
|
||||
* lionForm.formElements[d-e].value; // String
|
||||
* lionForm.formElements[f-g].value; // Array<String>
|
||||
*/
|
||||
export class FormControlsCollection extends Array {
|
||||
/**
|
||||
* @desc Gives back the named keys and filters out array indexes
|
||||
*/
|
||||
keys() {
|
||||
return Object.keys(this).filter(k => Number.isNaN(Number(k)));
|
||||
}
|
||||
}
|
||||
184
packages/field/src/registration/FormRegistrarMixin.js
Normal file
184
packages/field/src/registration/FormRegistrarMixin.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
// eslint-disable-next-line max-classes-per-file
|
||||
import { dedupeMixin } from '@lion/core';
|
||||
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
||||
import { formRegistrarManager } from './formRegistrarManager.js';
|
||||
import { FormControlsCollection } from './FormControlsCollection.js';
|
||||
|
||||
// TODO: rename .formElements to .formControls? (or .$controls ?)
|
||||
|
||||
/**
|
||||
* @desc This allows an element to become the manager of a register.
|
||||
* It basically keeps track of a FormControlsCollection that it stores in .formElements
|
||||
* This will always be an array of all elements.
|
||||
* In case of a form or fieldset(sub form), it will also act as a key based object with FormControl
|
||||
* (fields, choice groups or fieldsets)as keys.
|
||||
* For choice groups, the value will only stay an array.
|
||||
* See FormControlsCollection for more information
|
||||
*/
|
||||
export const FormRegistrarMixin = dedupeMixin(
|
||||
superclass =>
|
||||
// eslint-disable-next-line no-shadow, no-unused-vars
|
||||
class FormRegistrarMixin extends FormRegisteringMixin(superclass) {
|
||||
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)
|
||||
* @type {boolean}
|
||||
*/
|
||||
_isFormOrFieldset: Boolean,
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.formElements = new FormControlsCollection();
|
||||
|
||||
this._isFormOrFieldset = false;
|
||||
|
||||
this.__readyForRegistration = false;
|
||||
this.__hasBeenRendered = false;
|
||||
this.registrationReady = new Promise(resolve => {
|
||||
this.__resolveRegistrationReady = resolve;
|
||||
});
|
||||
|
||||
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
||||
this.addEventListener('form-element-register', this._onRequestToAddFormElement);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
formRegistrarManager.add(this);
|
||||
if (this.__hasBeenRendered) {
|
||||
formRegistrarManager.becomesReady();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
formRegistrarManager.remove(this);
|
||||
}
|
||||
|
||||
isRegisteredFormElement(el) {
|
||||
return this.formElements.some(exitingEl => exitingEl === el);
|
||||
}
|
||||
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.__resolveRegistrationReady();
|
||||
this.__readyForRegistration = true;
|
||||
formRegistrarManager.becomesReady();
|
||||
this.__hasBeenRendered = true;
|
||||
}
|
||||
|
||||
addFormElement(child, indexToInsertAt) {
|
||||
// This is a way to let the child element (a lion-fieldset or lion-field) know, about its parent
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
child.__parentFormGroup = this;
|
||||
|
||||
// 1. Add children as array element
|
||||
if (indexToInsertAt > 0) {
|
||||
this.formElements.splice(indexToInsertAt, 0, child);
|
||||
} else {
|
||||
this.formElements.push(child);
|
||||
}
|
||||
|
||||
// 2. Add children as object key
|
||||
if (this._isFormOrFieldset) {
|
||||
const { name } = child;
|
||||
if (!name) {
|
||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||
throw new TypeError('You need to define a name');
|
||||
}
|
||||
if (name === this.name) {
|
||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||
throw new TypeError(`You can not have the same name "${name}" as your parent`);
|
||||
}
|
||||
|
||||
if (name.substr(-2) === '[]') {
|
||||
if (!Array.isArray(this.formElements[name])) {
|
||||
this.formElements[name] = new FormControlsCollection();
|
||||
}
|
||||
if (indexToInsertAt > 0) {
|
||||
this.formElements[name].splice(indexToInsertAt, 0, child);
|
||||
} else {
|
||||
this.formElements[name].push(child);
|
||||
}
|
||||
} else if (!this.formElements[name]) {
|
||||
this.formElements[name] = child;
|
||||
} else {
|
||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||
throw new TypeError(
|
||||
`Name "${name}" is already registered - if you want an array add [] to the end`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeFormElement(child) {
|
||||
// 1. Handle array based children
|
||||
const index = this.formElements.indexOf(child);
|
||||
if (index > -1) {
|
||||
this.formElements.splice(index, 1);
|
||||
}
|
||||
|
||||
// 2. Handle name based object keys
|
||||
if (this._isFormOrFieldset) {
|
||||
const { name } = child;
|
||||
if (name.substr(-2) === '[]' && this.formElements[name]) {
|
||||
const idx = this.formElements[name].indexOf(child);
|
||||
if (idx > -1) {
|
||||
this.formElements[name].splice(idx, 1);
|
||||
}
|
||||
} else if (this.formElements[name]) {
|
||||
delete this.formElements[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onRequestToAddFormElement(ev) {
|
||||
const child = ev.detail.element;
|
||||
if (child === this) {
|
||||
// as we fire and listen - don't add ourselves
|
||||
return;
|
||||
}
|
||||
if (this.isRegisteredFormElement(child)) {
|
||||
// do not readd already existing elements
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
|
||||
// Check for siblings to determine the right order to insert into formElements
|
||||
// If there is no next sibling, index is -1
|
||||
let indexToInsertAt = -1;
|
||||
if (this.formElements && Array.isArray(this.formElements)) {
|
||||
indexToInsertAt = this.formElements.indexOf(child.nextElementSibling);
|
||||
}
|
||||
this.addFormElement(child, indexToInsertAt);
|
||||
}
|
||||
|
||||
_onRequestToRemoveFormElement(ev) {
|
||||
const child = ev.detail.element;
|
||||
if (child === this) {
|
||||
// as we fire and listen - don't remove ourselves
|
||||
return;
|
||||
}
|
||||
if (!this.isRegisteredFormElement(child)) {
|
||||
// do not remove non existing elements
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
|
||||
this.removeFormElement(child);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -1,19 +1,18 @@
|
|||
import { LitElement } from '@lion/core';
|
||||
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { FormRegisteringMixin } from '../src/FormRegisteringMixin.js';
|
||||
import { formRegistrarManager } from '../src/formRegistrarManager.js';
|
||||
import { FormRegistrarMixin } from '../src/FormRegistrarMixin.js';
|
||||
import { FormRegistrarPortalMixin } from '../src/FormRegistrarPortalMixin.js';
|
||||
import { FormRegisteringMixin } from '../src/registration/FormRegisteringMixin.js';
|
||||
import { formRegistrarManager } from '../src/registration/formRegistrarManager.js';
|
||||
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
||||
import { FormRegistrarPortalMixin } from '../src/registration/FormRegistrarPortalMixin.js';
|
||||
|
||||
export const runRegistrationSuite = customConfig => {
|
||||
const cfg = {
|
||||
baseElement: HTMLElement,
|
||||
suffix: null,
|
||||
...customConfig,
|
||||
};
|
||||
|
||||
describe(`FormRegistrationMixins${cfg.suffix ? ` (${cfg.suffix})` : ''}`, () => {
|
||||
describe('FormRegistrationMixins', () => {
|
||||
let parentTag;
|
||||
let childTag;
|
||||
let portalTag;
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export { LionFieldset } from './src/LionFieldset.js';
|
||||
export { FormGroupMixin } from './src/FormGroupMixin.js';
|
||||
|
|
|
|||
403
packages/fieldset/src/FormGroupMixin.js
Normal file
403
packages/fieldset/src/FormGroupMixin.js
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
import { html, dedupeMixin, SlotMixin } from '@lion/core';
|
||||
import { DisabledMixin } from '@lion/core/src/DisabledMixin.js';
|
||||
import { FormControlMixin, FormRegistrarMixin, FormControlsCollection } from '@lion/field';
|
||||
import { getAriaElementsInRightDomOrder } from '@lion/field/src/utils/getAriaElementsInRightDomOrder.js';
|
||||
import { ValidateMixin } from '@lion/validate';
|
||||
import { FormElementsHaveNoError } from './FormElementsHaveNoError.js';
|
||||
|
||||
/**
|
||||
* @desc Form group mixin serves as the basis for (sub) forms. Designed to be put on
|
||||
* elements with role=group (or radiogroup)
|
||||
* It bridges all the functionality of the child form controls:
|
||||
* ValidateMixin, InteractionStateMixin, FormatMixin, FormControlMixin etc.
|
||||
* It is designed to be used on top of FormRegstrarMixin and ChoiceGroupMixin
|
||||
* Also, the LionFieldset element (which supports name based retrieval of children via formElements
|
||||
* and the automatic grouping of formElements via '[]')
|
||||
*
|
||||
*/
|
||||
export const FormGroupMixin = dedupeMixin(
|
||||
superclass =>
|
||||
// eslint-disable-next-line no-shadow
|
||||
class FormGroupMixin extends FormRegistrarMixin(
|
||||
FormControlMixin(ValidateMixin(DisabledMixin(SlotMixin(superclass)))),
|
||||
) {
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* Interaction state that can be used to compute the visibility of
|
||||
* feedback messages
|
||||
*/
|
||||
// TODO: Move property submitted to InteractionStateMixin.
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get _inputNode() {
|
||||
return this;
|
||||
}
|
||||
|
||||
get modelValue() {
|
||||
return this._getFromAllFormElements('modelValue');
|
||||
}
|
||||
|
||||
set modelValue(values) {
|
||||
this._setValueMapForAllFormElements('modelValue', values);
|
||||
}
|
||||
|
||||
get serializedValue() {
|
||||
return this._getFromAllFormElements('serializedValue');
|
||||
}
|
||||
|
||||
set serializedValue(values) {
|
||||
this._setValueMapForAllFormElements('serializedValue', values);
|
||||
}
|
||||
|
||||
get formattedValue() {
|
||||
return this._getFromAllFormElements('formattedValue');
|
||||
}
|
||||
|
||||
set formattedValue(values) {
|
||||
this._setValueMapForAllFormElements('formattedValue', values);
|
||||
}
|
||||
|
||||
// TODO: should it be any or every? Should we maybe keep track of both,
|
||||
// so we can configure feedback visibility depending on scenario?
|
||||
// Should we allow configuring feedback visibility on validator instances
|
||||
// for maximal flexibility?
|
||||
// Document this...
|
||||
get prefilled() {
|
||||
return this._everyFormElementHas('prefilled');
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.disabled = false;
|
||||
this.submitted = false;
|
||||
this.dirty = false;
|
||||
this.touched = false;
|
||||
this.focused = false;
|
||||
this.__addedSubValidators = false;
|
||||
|
||||
this._checkForOutsideClick = this._checkForOutsideClick.bind(this);
|
||||
|
||||
this.addEventListener('focusin', this._syncFocused);
|
||||
this.addEventListener('focusout', this._onFocusOut);
|
||||
this.addEventListener('dirty-changed', this._syncDirty);
|
||||
this.addEventListener('validate-performed', this.__onChildValidatePerformed);
|
||||
|
||||
this.defaultValidators = [new FormElementsHaveNoError()];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// eslint-disable-next-line wc/guard-super-call
|
||||
super.connectedCallback();
|
||||
this.setAttribute('role', 'group');
|
||||
this.__initInteractionStates();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback(); // eslint-disable-line wc/guard-super-call
|
||||
|
||||
if (this.__hasActiveOutsideClickHandling) {
|
||||
document.removeEventListener('click', this._checkForOutsideClick);
|
||||
this.__hasActiveOutsideClickHandling = false;
|
||||
}
|
||||
}
|
||||
|
||||
async __initInteractionStates() {
|
||||
if (!this.__readyForRegistration) {
|
||||
await this.registrationReady;
|
||||
}
|
||||
this.formElements.forEach(el => {
|
||||
if (typeof el.initInteractionState === 'function') {
|
||||
el.initInteractionState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has('disabled')) {
|
||||
if (this.disabled) {
|
||||
this.__requestChildrenToBeDisabled();
|
||||
} else {
|
||||
this.__retractRequestChildrenToBeDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has('focused')) {
|
||||
if (this.focused === true) {
|
||||
this.__setupOutsideClickHandling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__setupOutsideClickHandling() {
|
||||
if (!this.__hasActiveOutsideClickHandling) {
|
||||
document.addEventListener('click', this._checkForOutsideClick);
|
||||
this.__hasActiveOutsideClickHandling = true;
|
||||
}
|
||||
}
|
||||
|
||||
_checkForOutsideClick(event) {
|
||||
const outsideGroupClicked = !this.contains(event.target);
|
||||
if (outsideGroupClicked) {
|
||||
this.touched = true;
|
||||
}
|
||||
}
|
||||
|
||||
__requestChildrenToBeDisabled() {
|
||||
this.formElements.forEach(child => {
|
||||
if (child.makeRequestToBeDisabled) {
|
||||
child.makeRequestToBeDisabled();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
__retractRequestChildrenToBeDisabled() {
|
||||
this.formElements.forEach(child => {
|
||||
if (child.retractRequestToBeDisabled) {
|
||||
child.retractRequestToBeDisabled();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
inputGroupTemplate() {
|
||||
return html`
|
||||
<div class="input-group">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc Handles interaction state 'submitted'.
|
||||
* This allows children to enable visibility of validation feedback
|
||||
*/
|
||||
submitGroup() {
|
||||
this.submitted = true;
|
||||
this.formElements.forEach(child => {
|
||||
if (typeof child.submitGroup === 'function') {
|
||||
child.submitGroup();
|
||||
} else {
|
||||
child.submitted = true; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resetGroup() {
|
||||
this.formElements.forEach(child => {
|
||||
if (typeof child.resetGroup === 'function') {
|
||||
child.resetGroup();
|
||||
} else if (typeof child.reset === 'function') {
|
||||
child.reset();
|
||||
}
|
||||
});
|
||||
|
||||
this.resetInteractionState();
|
||||
}
|
||||
|
||||
resetInteractionState() {
|
||||
this.submitted = false;
|
||||
this.touched = false;
|
||||
this.dirty = false;
|
||||
this.formElements.forEach(formElement => {
|
||||
if (typeof formElement.resetInteractionState === 'function') {
|
||||
formElement.resetInteractionState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getFromAllFormElements(property, filterCondition = el => !el.disabled) {
|
||||
const result = {};
|
||||
this.formElements.keys().forEach(name => {
|
||||
const elem = this.formElements[name];
|
||||
if (elem instanceof FormControlsCollection) {
|
||||
result[name] = elem.filter(el => filterCondition(el)).map(el => el[property]);
|
||||
} else if (filterCondition(elem)) {
|
||||
if (typeof elem._getFromAllFormElements === 'function') {
|
||||
result[name] = elem._getFromAllFormElements(property, filterCondition);
|
||||
} else {
|
||||
result[name] = elem[property];
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
_setValueForAllFormElements(property, value) {
|
||||
this.formElements.forEach(el => {
|
||||
el[property] = value; // eslint-disable-line no-param-reassign
|
||||
});
|
||||
}
|
||||
|
||||
async _setValueMapForAllFormElements(property, values) {
|
||||
if (!this.__readyForRegistration) {
|
||||
await this.registrationReady;
|
||||
}
|
||||
|
||||
if (values && typeof values === 'object') {
|
||||
Object.keys(values).forEach(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
this.formElements[name].forEach((el, index) => {
|
||||
el[property] = values[name][index]; // eslint-disable-line no-param-reassign
|
||||
});
|
||||
}
|
||||
this.formElements[name][property] = values[name];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_anyFormElementHas(property) {
|
||||
return Object.keys(this.formElements).some(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
return this.formElements[name].some(el => !!el[property]);
|
||||
}
|
||||
return !!this.formElements[name][property];
|
||||
});
|
||||
}
|
||||
|
||||
_anyFormElementHasFeedbackFor(state) {
|
||||
return Object.keys(this.formElements).some(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
return this.formElements[name].some(el => !!el.hasFeedbackFor.includes(state));
|
||||
}
|
||||
return !!this.formElements[name].hasFeedbackFor.includes(state);
|
||||
});
|
||||
}
|
||||
|
||||
_everyFormElementHas(property) {
|
||||
return Object.keys(this.formElements).every(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
return this.formElements[name].every(el => !!el[property]);
|
||||
}
|
||||
return !!this.formElements[name][property];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
__onChildValidatePerformed(ev) {
|
||||
if (ev && this.isRegisteredFormElement(ev.target)) {
|
||||
this.validate();
|
||||
}
|
||||
}
|
||||
|
||||
_syncFocused() {
|
||||
this.focused = this._anyFormElementHas('focused');
|
||||
}
|
||||
|
||||
_onFocusOut(ev) {
|
||||
const lastEl = this.formElements[this.formElements.length - 1];
|
||||
if (ev.target === lastEl) {
|
||||
this.touched = true;
|
||||
}
|
||||
this.focused = false;
|
||||
}
|
||||
|
||||
_syncDirty() {
|
||||
this.dirty = this._anyFormElementHas('dirty');
|
||||
}
|
||||
|
||||
__linkChildrenMessagesToParent(child) {
|
||||
// aria-describedby of (nested) children
|
||||
let parent = this;
|
||||
while (parent) {
|
||||
this.constructor._addDescriptionElementIdsToField(
|
||||
child,
|
||||
parent._getAriaDescriptionElements(),
|
||||
);
|
||||
// Also check if the newly added child needs to refer grandparents
|
||||
parent = parent.__parentFormGroup;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override of FormRegistrarMixin.
|
||||
* @desc Connects ValidateMixin and DisabledMixin
|
||||
* On top of this, error messages of children are linked to their parents
|
||||
*/
|
||||
addFormElement(child, indexToInsertAt) {
|
||||
super.addFormElement(child, indexToInsertAt);
|
||||
if (this.disabled) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
child.makeRequestToBeDisabled();
|
||||
}
|
||||
// TODO: Unlink in removeFormElement
|
||||
this.__linkChildrenMessagesToParent(child);
|
||||
this.validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers initial model values of all children. Used
|
||||
* when resetGroup() is called.
|
||||
*/
|
||||
get _initialModelValue() {
|
||||
return this._getFromAllFormElements('_initialModelValue');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add aria-describedby to child element(field), so that it points to feedback/help-text of
|
||||
* parent(fieldset)
|
||||
* @param {LionField} field - the child: lion-field/lion-input/lion-textarea
|
||||
* @param {array} descriptionElements - description elements like feedback and help-text
|
||||
*/
|
||||
static _addDescriptionElementIdsToField(field, descriptionElements) {
|
||||
const orderedEls = getAriaElementsInRightDomOrder(descriptionElements, { reverse: true });
|
||||
orderedEls.forEach(el => {
|
||||
if (field.addToAriaDescribedBy) {
|
||||
field.addToAriaDescribedBy(el, { reorder: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @override of FormRegistrarMixin. Connects ValidateMixin
|
||||
*/
|
||||
removeFormElement(...args) {
|
||||
super.removeFormElement(...args);
|
||||
this.validate();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -1,473 +1,29 @@
|
|||
import { html, LitElement, SlotMixin } from '@lion/core';
|
||||
import { DisabledMixin } from '@lion/core/src/DisabledMixin.js';
|
||||
import { FormControlMixin, FormRegistrarMixin } from '@lion/field';
|
||||
import { getAriaElementsInRightDomOrder } from '@lion/field/src/utils/getAriaElementsInRightDomOrder.js';
|
||||
import { ValidateMixin } from '@lion/validate';
|
||||
import { FormElementsHaveNoError } from './FormElementsHaveNoError.js';
|
||||
import { LitElement } from '@lion/core';
|
||||
import { FormGroupMixin } from './FormGroupMixin.js';
|
||||
|
||||
/**
|
||||
* LionFieldset: fieldset wrapper providing extra features and integration with lion-field elements.
|
||||
* @desc LionFieldset is basically a 'sub form' and can have its own nested sub forms.
|
||||
* It mimics the native <fieldset> element in this sense, but has all the functionality of
|
||||
* a FormControl (advanced styling, validation, interaction states etc.) Also see
|
||||
* FormGroupMixin it depends on.
|
||||
*
|
||||
* LionFieldset enables the '_isFormOrFieldset' flag in FormRegistrarMixin. This makes .formElements
|
||||
* act not only as an array, but also as an object (see FormRegistarMixin for more information).
|
||||
* As a bonus, It can also group children having names ending with '[]'.
|
||||
*
|
||||
* Above will be helpful for both forms and sub forms, which can contain sub forms as children
|
||||
* as well and allow for a nested form structure.
|
||||
* Contrary, other form groups (choice groups like radio-group, checkbox-group and (multi)select)
|
||||
* don't: they should be considered 'end nodes' or 'leaves' of the form and their children/formElements
|
||||
* cannot be accessed individually via object keys.
|
||||
*
|
||||
* @customElement lion-fieldset
|
||||
* @extends {LitElement}
|
||||
*/
|
||||
export class LionFieldset extends FormRegistrarMixin(
|
||||
FormControlMixin(ValidateMixin(DisabledMixin(SlotMixin(LitElement)))),
|
||||
) {
|
||||
static get properties() {
|
||||
return {
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
// TODO: Move property submitted to InteractionStateMixin.
|
||||
submitted: {
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
},
|
||||
focused: {
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
},
|
||||
dirty: {
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
},
|
||||
touched: {
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get touched() {
|
||||
return this.__touched;
|
||||
}
|
||||
|
||||
set touched(value) {
|
||||
const oldVal = this.__touched;
|
||||
this.__touched = value;
|
||||
this.requestUpdate('touched', oldVal);
|
||||
}
|
||||
|
||||
get _inputNode() {
|
||||
return this;
|
||||
}
|
||||
|
||||
get modelValue() {
|
||||
return this._getFromAllFormElements('modelValue');
|
||||
}
|
||||
|
||||
set modelValue(values) {
|
||||
this._setValueMapForAllFormElements('modelValue', values);
|
||||
}
|
||||
|
||||
get serializedValue() {
|
||||
return this._getFromAllFormElements('serializedValue');
|
||||
}
|
||||
|
||||
set serializedValue(values) {
|
||||
this._setValueMapForAllFormElements('serializedValue', values);
|
||||
}
|
||||
|
||||
get formattedValue() {
|
||||
return this._getFromAllFormElements('formattedValue');
|
||||
}
|
||||
|
||||
set formattedValue(values) {
|
||||
this._setValueMapForAllFormElements('formattedValue', values);
|
||||
}
|
||||
|
||||
get prefilled() {
|
||||
return this._everyFormElementHas('prefilled');
|
||||
}
|
||||
|
||||
get formElementsArray() {
|
||||
return Object.keys(this.formElements).reduce((result, name) => {
|
||||
const element = this.formElements[name];
|
||||
return result.concat(Array.isArray(element) ? element : [element]);
|
||||
}, []);
|
||||
}
|
||||
|
||||
set fieldName(value) {
|
||||
this.__fieldName = value;
|
||||
}
|
||||
|
||||
get fieldName() {
|
||||
const label =
|
||||
this.label ||
|
||||
(this.querySelector('[slot=label]') && this.querySelector('[slot=label]').textContent);
|
||||
return this.__fieldName || label || this.name;
|
||||
}
|
||||
|
||||
export class LionFieldset extends FormGroupMixin(LitElement) {
|
||||
constructor() {
|
||||
super();
|
||||
this.disabled = false;
|
||||
this.submitted = false;
|
||||
this.dirty = false;
|
||||
this.touched = false;
|
||||
this.focused = false;
|
||||
this.formElements = {};
|
||||
this.__addedSubValidators = false;
|
||||
|
||||
this._checkForOutsideClick = this._checkForOutsideClick.bind(this);
|
||||
|
||||
this.addEventListener('focusin', this._syncFocused);
|
||||
this.addEventListener('focusout', this._onFocusOut);
|
||||
this.addEventListener('dirty-changed', this._syncDirty);
|
||||
this.addEventListener('validate-performed', this.__validate);
|
||||
|
||||
this.defaultValidators = [new FormElementsHaveNoError()];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// eslint-disable-next-line wc/guard-super-call
|
||||
super.connectedCallback();
|
||||
this._setRole();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback(); // eslint-disable-line wc/guard-super-call
|
||||
|
||||
if (this.__hasActiveOutsideClickHandling) {
|
||||
document.removeEventListener('click', this._checkForOutsideClick);
|
||||
this.__hasActiveOutsideClickHandling = false;
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has('disabled')) {
|
||||
if (this.disabled) {
|
||||
this.__requestChildrenToBeDisabled();
|
||||
} else {
|
||||
this.__retractRequestChildrenToBeDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has('focused')) {
|
||||
if (this.focused === true) {
|
||||
this.__setupOutsideClickHandling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__setupOutsideClickHandling() {
|
||||
if (!this.__hasActiveOutsideClickHandling) {
|
||||
document.addEventListener('click', this._checkForOutsideClick);
|
||||
this.__hasActiveOutsideClickHandling = true;
|
||||
}
|
||||
}
|
||||
|
||||
_checkForOutsideClick(event) {
|
||||
const outsideGroupClicked = !this.contains(event.target);
|
||||
if (outsideGroupClicked) {
|
||||
this.touched = true;
|
||||
}
|
||||
}
|
||||
|
||||
__requestChildrenToBeDisabled() {
|
||||
this.formElementsArray.forEach(child => {
|
||||
if (child.makeRequestToBeDisabled) {
|
||||
child.makeRequestToBeDisabled();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
__retractRequestChildrenToBeDisabled() {
|
||||
this.formElementsArray.forEach(child => {
|
||||
if (child.retractRequestToBeDisabled) {
|
||||
child.retractRequestToBeDisabled();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
inputGroupTemplate() {
|
||||
return html`
|
||||
<div class="input-group">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
submitGroup() {
|
||||
this.submitted = true;
|
||||
this.formElementsArray.forEach(child => {
|
||||
if (typeof child.submitGroup === 'function') {
|
||||
child.submitGroup();
|
||||
} else {
|
||||
child.submitted = true; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
serializeGroup() {
|
||||
const childrenNames = Object.keys(this.formElements);
|
||||
const serializedValues = childrenNames.length > 0 ? {} : undefined;
|
||||
childrenNames.forEach(name => {
|
||||
const element = this.formElements[name];
|
||||
if (Array.isArray(element)) {
|
||||
serializedValues[name] = this.__serializeElements(element);
|
||||
} else {
|
||||
const serializedValue = this.__serializeElement(element);
|
||||
if (serializedValue || serializedValue === 0) {
|
||||
serializedValues[name] = serializedValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
return serializedValues;
|
||||
}
|
||||
|
||||
resetGroup() {
|
||||
this.formElementsArray.forEach(child => {
|
||||
if (typeof child.resetGroup === 'function') {
|
||||
child.resetGroup();
|
||||
} else if (typeof child.reset === 'function') {
|
||||
child.reset();
|
||||
}
|
||||
});
|
||||
|
||||
this.resetInteractionState();
|
||||
}
|
||||
|
||||
resetInteractionState() {
|
||||
this.submitted = false;
|
||||
this.touched = false;
|
||||
this.dirty = false;
|
||||
this.formElementsArray.forEach(formElement => {
|
||||
if (typeof formElement.resetInteractionState === 'function') {
|
||||
formElement.resetInteractionState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getFromAllFormElements(property) {
|
||||
if (!this.formElements) {
|
||||
return undefined;
|
||||
}
|
||||
const childrenNames = Object.keys(this.formElements);
|
||||
const values = {};
|
||||
childrenNames.forEach(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
// grouped via myName[]
|
||||
values[name] = this.formElements[name].map(node => node.modelValue);
|
||||
} else {
|
||||
// not grouped
|
||||
values[name] = this.formElements[name][property];
|
||||
}
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
_setValueForAllFormElements(property, value) {
|
||||
this.formElementsArray.forEach(el => {
|
||||
el[property] = value; // eslint-disable-line no-param-reassign
|
||||
});
|
||||
}
|
||||
|
||||
async _setValueMapForAllFormElements(property, values) {
|
||||
if (!this.__readyForRegistration) {
|
||||
await this.registrationReady;
|
||||
}
|
||||
|
||||
if (values && typeof values === 'object') {
|
||||
Object.keys(values).forEach(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
this.formElements[name].forEach((el, index) => {
|
||||
el[property] = values[name][index]; // eslint-disable-line no-param-reassign
|
||||
});
|
||||
}
|
||||
this.formElements[name][property] = values[name];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_anyFormElementHas(property) {
|
||||
return Object.keys(this.formElements).some(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
return this.formElements[name].some(el => !!el[property]);
|
||||
}
|
||||
return !!this.formElements[name][property];
|
||||
});
|
||||
}
|
||||
|
||||
_anyFormElementHasFeedbackFor(state) {
|
||||
return Object.keys(this.formElements).some(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
return this.formElements[name].some(el => !!el.hasFeedbackFor.includes(state));
|
||||
}
|
||||
return !!this.formElements[name].hasFeedbackFor.includes(state);
|
||||
});
|
||||
}
|
||||
|
||||
_everyFormElementHas(property) {
|
||||
return Object.keys(this.formElements).every(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
return this.formElements[name].every(el => !!el[property]);
|
||||
}
|
||||
return !!this.formElements[name][property];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
__validate(ev) {
|
||||
if (ev && this.isRegisteredFormElement(ev.target)) {
|
||||
this.validate();
|
||||
}
|
||||
}
|
||||
|
||||
_syncFocused() {
|
||||
this.focused = this._anyFormElementHas('focused');
|
||||
}
|
||||
|
||||
_onFocusOut(ev) {
|
||||
const lastEl = this.formElementsArray[this.formElementsArray.length - 1];
|
||||
if (ev.target === lastEl) {
|
||||
this.touched = true;
|
||||
}
|
||||
this.focused = false;
|
||||
}
|
||||
|
||||
_syncDirty() {
|
||||
this.dirty = this._anyFormElementHas('dirty');
|
||||
}
|
||||
|
||||
_setRole(role) {
|
||||
this.setAttribute('role', role || 'group');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
__serializeElement(element) {
|
||||
if (!element.disabled) {
|
||||
if (typeof element.serializeGroup === 'function') {
|
||||
return element.serializeGroup();
|
||||
}
|
||||
return element.serializedValue;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
__serializeElements(elements) {
|
||||
const serializedValues = [];
|
||||
elements.forEach(element => {
|
||||
const serializedValue = this.__serializeElement(element);
|
||||
if (serializedValue || serializedValue === 0) {
|
||||
serializedValues.push(serializedValue);
|
||||
}
|
||||
});
|
||||
return serializedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the element to an object with the child name as a key
|
||||
* Note: this is different to the default behavior of just beeing an array
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
addFormElement(child, indexToInsertAt) {
|
||||
const { name } = child;
|
||||
if (!name) {
|
||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||
throw new TypeError('You need to define a name');
|
||||
}
|
||||
if (name === this.name && !this._childrenCanHaveSameName) {
|
||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||
throw new TypeError(`You can not have the same name "${name}" as your parent`);
|
||||
}
|
||||
|
||||
if (this.disabled) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
child.makeRequestToBeDisabled();
|
||||
}
|
||||
|
||||
if (name.substr(-2) === '[]' || this._childNamesCanBeDuplicate) {
|
||||
if (!Array.isArray(this.formElements[name])) {
|
||||
this.formElements[name] = [];
|
||||
}
|
||||
if (indexToInsertAt > 0) {
|
||||
this.formElements[name].splice(indexToInsertAt, 0, child);
|
||||
} else {
|
||||
this.formElements[name].push(child);
|
||||
}
|
||||
} else if (!this.formElements[name]) {
|
||||
this.formElements[name] = child;
|
||||
} else {
|
||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||
throw new TypeError(
|
||||
`Name "${name}" is already registered - if you want an array add [] to the end`,
|
||||
);
|
||||
}
|
||||
|
||||
// This is a way to let the child element (a lion-fieldset or lion-field) know, about its parent
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
child.__parentFormGroup = this;
|
||||
|
||||
// aria-describedby of (nested) children
|
||||
|
||||
// TODO: Teardown in removeFormElement
|
||||
|
||||
let parent = this;
|
||||
while (parent) {
|
||||
this.constructor._addDescriptionElementIdsToField(
|
||||
child,
|
||||
parent._getAriaDescriptionElements(),
|
||||
);
|
||||
// Also check if the newly added child needs to refer grandparents
|
||||
parent = parent.__parentFormGroup;
|
||||
}
|
||||
|
||||
this.validate();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get _childrenCanHaveSameName() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get _childNamesCanBeDuplicate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers initial model values of all children. Used
|
||||
* when resetGroup() is called.
|
||||
*/
|
||||
get _initialModelValue() {
|
||||
return this._getFromAllFormElements('_initialModelValue');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add aria-describedby to child element(field), so that it points to feedback/help-text of
|
||||
* parent(fieldset)
|
||||
* @param {LionField} field - the child: lion-field/lion-input/lion-textarea
|
||||
* @param {array} descriptionElements - description elements like feedback and help-text
|
||||
*/
|
||||
static _addDescriptionElementIdsToField(field, descriptionElements) {
|
||||
const orderedEls = getAriaElementsInRightDomOrder(descriptionElements, { reverse: true });
|
||||
orderedEls.forEach(el => {
|
||||
if (field.addToAriaDescribedBy) {
|
||||
field.addToAriaDescribedBy(el, { reorder: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeFormElement(child) {
|
||||
const { name } = child;
|
||||
if (name.substr(-2) === '[]' && this.formElements[name]) {
|
||||
const index = this.formElements[name].indexOf(child);
|
||||
if (index > -1) {
|
||||
this.formElements[name].splice(index, 1);
|
||||
}
|
||||
} else if (this.formElements[name]) {
|
||||
delete this.formElements[name];
|
||||
}
|
||||
this.validate();
|
||||
/** @override from FormRegistrarMixin */
|
||||
this._isFormOrFieldset = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
expect,
|
||||
fixture,
|
||||
fixtureSync,
|
||||
html,
|
||||
unsafeStatic,
|
||||
triggerFocusFor,
|
||||
|
|
@ -39,7 +40,9 @@ beforeEach(() => {
|
|||
localizeTearDown();
|
||||
});
|
||||
|
||||
// TODO: seperate fieldset and FormGroup tests
|
||||
describe('<lion-fieldset>', () => {
|
||||
// TODO: Tests below belong to FormControlMixin. Preferably run suite integration test
|
||||
it(`has a fieldName based on the label`, async () => {
|
||||
const el1 = await fixture(html`<${tag} label="foo">${inputSlots}</${tag}>`);
|
||||
expect(el1.fieldName).to.equal(el1._labelNode.textContent);
|
||||
|
|
@ -60,13 +63,15 @@ describe('<lion-fieldset>', () => {
|
|||
expect(el.__fieldName).to.equal(el.fieldName);
|
||||
});
|
||||
|
||||
it(`${tagString} has an up to date list of every form element in #formElements`, async () => {
|
||||
// TODO: Tests below belong to FormRegistrarMixin. Preferably run suite integration test
|
||||
|
||||
it(`${tagString} has an up to date list of every form element in .formElements`, async () => {
|
||||
const el = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await nextFrame();
|
||||
expect(Object.keys(el.formElements).length).to.equal(3);
|
||||
expect(el.formElements.keys().length).to.equal(3);
|
||||
expect(el.formElements['hobbies[]'].length).to.equal(2);
|
||||
el.removeChild(el.formElements['hobbies[]'][0]);
|
||||
expect(Object.keys(el.formElements).length).to.equal(3);
|
||||
expect(el.formElements.keys().length).to.equal(3);
|
||||
expect(el.formElements['hobbies[]'].length).to.equal(1);
|
||||
});
|
||||
|
||||
|
|
@ -79,9 +84,9 @@ describe('<lion-fieldset>', () => {
|
|||
</${tag}>
|
||||
`);
|
||||
await nextFrame();
|
||||
expect(el.formElementsArray.length).to.equal(1);
|
||||
expect(el.formElements.length).to.equal(1);
|
||||
el.children[0].removeChild(el.formElements.foo);
|
||||
expect(el.formElementsArray.length).to.equal(0);
|
||||
expect(el.formElements.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('handles names with ending [] as an array', async () => {
|
||||
|
|
@ -91,7 +96,7 @@ describe('<lion-fieldset>', () => {
|
|||
el.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
|
||||
el.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
|
||||
|
||||
expect(Object.keys(el.formElements).length).to.equal(3);
|
||||
expect(el.formElements.keys().length).to.equal(3);
|
||||
expect(el.formElements['hobbies[]'].length).to.equal(2);
|
||||
expect(el.formElements['hobbies[]'][0].modelValue.value).to.equal('chess');
|
||||
expect(el.formElements['gender[]'][0].modelValue.value).to.equal('male');
|
||||
|
|
@ -166,15 +171,17 @@ describe('<lion-fieldset>', () => {
|
|||
const el = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
const newField = await fixture(html`<${childTag} name="lastName"></${childTag}>`);
|
||||
|
||||
expect(Object.keys(el.formElements).length).to.equal(3);
|
||||
expect(el.formElements.keys().length).to.equal(3);
|
||||
|
||||
el.appendChild(newField);
|
||||
expect(Object.keys(el.formElements).length).to.equal(4);
|
||||
expect(el.formElements.keys().length).to.equal(4);
|
||||
|
||||
el._inputNode.removeChild(newField);
|
||||
expect(Object.keys(el.formElements).length).to.equal(3);
|
||||
expect(el.formElements.keys().length).to.equal(3);
|
||||
});
|
||||
|
||||
// TODO: Tests below belong to FormGroupMixin. Preferably run suite integration test
|
||||
|
||||
it('can read/write all values (of every input) via this.modelValue', async () => {
|
||||
const el = await fixture(html`
|
||||
<${tag}>
|
||||
|
|
@ -231,6 +238,32 @@ describe('<lion-fieldset>', () => {
|
|||
expect(el.formElements.lastName.modelValue).to.equal(2);
|
||||
});
|
||||
|
||||
it('does not list disabled values in this.modelValue', async () => {
|
||||
const el = await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="a" disabled .modelValue="${'x'}"></${childTag}>
|
||||
<${childTag} name="b" .modelValue="${'x'}"></${childTag}>
|
||||
<${tag} name="newFieldset">
|
||||
<${childTag} name="c" .modelValue="${'x'}"></${childTag}>
|
||||
<${childTag} name="d" disabled .modelValue="${'x'}"></${childTag}>
|
||||
</${tag}>
|
||||
<${tag} name="disabledFieldset" disabled>
|
||||
<${childTag} name="e" .modelValue="${'x'}"></${childTag}>
|
||||
</${tag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await el.registrationReady;
|
||||
const newFieldset = el.querySelector('lion-fieldset');
|
||||
await newFieldset.registrationReady;
|
||||
|
||||
expect(el.modelValue).to.deep.equal({
|
||||
b: 'x',
|
||||
newFieldset: {
|
||||
c: 'x',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not throw if setter data of this.modelValue can not be handled', async () => {
|
||||
const el = await fixture(html`
|
||||
<${tag}>
|
||||
|
|
@ -308,7 +341,7 @@ describe('<lion-fieldset>', () => {
|
|||
expect(el.modelValue).to.eql(initialSerializedValue);
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
describe('Validation', () => {
|
||||
it('validates on init', async () => {
|
||||
class IsCat extends Validator {
|
||||
constructor() {
|
||||
|
|
@ -409,7 +442,7 @@ describe('<lion-fieldset>', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('interaction states', () => {
|
||||
describe('Interaction states', () => {
|
||||
it('has false states (dirty, touched, prefilled) on init', async () => {
|
||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await nextFrame();
|
||||
|
|
@ -453,7 +486,7 @@ describe('<lion-fieldset>', () => {
|
|||
it('becomes prefilled if all form elements are prefilled', async () => {
|
||||
const el = await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="input1" prefilled></${childTag}>
|
||||
<${childTag} name="input1" .modelValue="${'prefilled'}"></${childTag}>
|
||||
<${childTag} name="input2"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
|
|
@ -462,8 +495,8 @@ describe('<lion-fieldset>', () => {
|
|||
|
||||
const el2 = await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="input1" prefilled></${childTag}>
|
||||
<${childTag} name="input2" prefilled></${childTag}>
|
||||
<${childTag} name="input1" .modelValue="${'prefilled'}"></${childTag}>
|
||||
<${childTag} name="input2" .modelValue="${'prefilled'}"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await nextFrame();
|
||||
|
|
@ -515,7 +548,7 @@ describe('<lion-fieldset>', () => {
|
|||
`);
|
||||
|
||||
outside.click();
|
||||
expect(el.touched, 'unfocused fieldset should stays untouched').to.be.false;
|
||||
expect(el.touched, 'unfocused fieldset should stay untouched').to.be.false;
|
||||
|
||||
el.children[1].focus();
|
||||
el.children[2].focus();
|
||||
|
|
@ -524,7 +557,6 @@ describe('<lion-fieldset>', () => {
|
|||
outside.click(); // blur the group via a click
|
||||
outside.focus(); // a real mouse click moves focus as well
|
||||
expect(el.touched).to.be.true;
|
||||
|
||||
expect(el2.touched).to.be.false;
|
||||
});
|
||||
|
||||
|
|
@ -591,9 +623,30 @@ describe('<lion-fieldset>', () => {
|
|||
expect(el.validationStates.error.Input1IsTen).to.be.true;
|
||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||
});
|
||||
|
||||
it.skip('(re)initializes children interaction states on registration ready', async () => {
|
||||
const fieldset = await fixtureSync(html`
|
||||
<${tag} .modelValue="${{ a: '1', b: '2' }}">
|
||||
<${childTag} name="a"></${childTag}>
|
||||
<${childTag} name="b"></${childTag}>
|
||||
</${tag}>`);
|
||||
const childA = fieldset.querySelector('[name="a"]');
|
||||
const childB = fieldset.querySelector('[name="b"]');
|
||||
const spyA = sinon.spy(childA, 'initInteractionState');
|
||||
const spyB = sinon.spy(childB, 'initInteractionState');
|
||||
expect(fieldset.prefilled).to.be.false;
|
||||
expect(fieldset.dirty).to.be.false;
|
||||
await fieldset.registrationReady;
|
||||
await nextFrame();
|
||||
expect(spyA).to.have.been.called;
|
||||
expect(spyB).to.have.been.called;
|
||||
expect(fieldset.prefilled).to.be.true;
|
||||
expect(fieldset.dirty).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('serialize', () => {
|
||||
// TODO: this should be tested in FormGroupMixin
|
||||
describe('serializedValue', () => {
|
||||
it('use form elements serializedValue', async () => {
|
||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await nextFrame();
|
||||
|
|
@ -604,7 +657,7 @@ describe('<lion-fieldset>', () => {
|
|||
fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
||||
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||
expect(fieldset.formElements['hobbies[]'][0].serializedValue).to.equal('Bar-serialized');
|
||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||
expect(fieldset.serializedValue).to.deep.equal({
|
||||
'hobbies[]': ['Bar-serialized', { checked: false, value: 'rugby' }],
|
||||
'gender[]': [
|
||||
{ checked: false, value: 'male' },
|
||||
|
|
@ -614,6 +667,27 @@ describe('<lion-fieldset>', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('treats names with ending [] as arrays', async () => {
|
||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await nextFrame();
|
||||
fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
|
||||
fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
|
||||
fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' };
|
||||
fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
||||
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||
expect(fieldset.serializedValue).to.deep.equal({
|
||||
'hobbies[]': [
|
||||
{ checked: false, value: 'chess' },
|
||||
{ checked: false, value: 'rugby' },
|
||||
],
|
||||
'gender[]': [
|
||||
{ checked: false, value: 'male' },
|
||||
{ checked: false, value: 'female' },
|
||||
],
|
||||
color: { checked: false, value: 'blue' },
|
||||
});
|
||||
});
|
||||
|
||||
it('0 is a valid value to be serialized', async () => {
|
||||
const fieldset = await fixture(html`
|
||||
<${tag}>
|
||||
|
|
@ -621,48 +695,22 @@ describe('<lion-fieldset>', () => {
|
|||
</${tag}>`);
|
||||
await nextFrame();
|
||||
fieldset.formElements.price.modelValue = 0;
|
||||
expect(fieldset.serializeGroup()).to.deep.equal({ price: 0 });
|
||||
expect(fieldset.serializedValue).to.deep.equal({ price: 0 });
|
||||
});
|
||||
|
||||
it('__serializeElements serializes 0 as a valid value', async () => {
|
||||
const fieldset = await fixture(html`<${tag}></${tag}>`);
|
||||
it.skip('serializes undefined values as ""(nb radios/checkboxes are always serialized)', async () => {
|
||||
const fieldset = await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="custom[]"></${childTag}>
|
||||
<${childTag} name="custom[]"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await nextFrame();
|
||||
const elements = [{ serializedValue: 0 }];
|
||||
expect(fieldset.__serializeElements(elements)).to.deep.equal([0]);
|
||||
});
|
||||
fieldset.formElements['custom[]'][0].modelValue = 'custom 1';
|
||||
fieldset.formElements['custom[]'][1].modelValue = undefined;
|
||||
|
||||
it('form elements which are not disabled', async () => {
|
||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await nextFrame();
|
||||
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||
fieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'football' };
|
||||
fieldset.formElements['gender[]'][0].modelValue = { checked: true, value: 'male' };
|
||||
fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
|
||||
fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
||||
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||
|
||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||
'hobbies[]': [
|
||||
{ checked: true, value: 'football' },
|
||||
{ checked: false, value: 'rugby' },
|
||||
],
|
||||
'gender[]': [
|
||||
{ checked: true, value: 'male' },
|
||||
{ checked: false, value: 'female' },
|
||||
],
|
||||
color: { checked: false, value: 'blue' },
|
||||
});
|
||||
fieldset.formElements.color.disabled = true;
|
||||
|
||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||
'hobbies[]': [
|
||||
{ checked: true, value: 'football' },
|
||||
{ checked: false, value: 'rugby' },
|
||||
],
|
||||
'gender[]': [
|
||||
{ checked: true, value: 'male' },
|
||||
{ checked: false, value: 'female' },
|
||||
],
|
||||
expect(fieldset.serializedValue).to.deep.equal({
|
||||
'custom[]': ['custom 1', ''],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -681,9 +729,9 @@ describe('<lion-fieldset>', () => {
|
|||
newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
||||
newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||
fieldset.formElements.comment.modelValue = 'Foo';
|
||||
expect(Object.keys(fieldset.formElements).length).to.equal(2);
|
||||
expect(Object.keys(newFieldset.formElements).length).to.equal(3);
|
||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||
expect(fieldset.formElements.keys().length).to.equal(2);
|
||||
expect(newFieldset.formElements.keys().length).to.equal(3);
|
||||
expect(fieldset.serializedValue).to.deep.equal({
|
||||
comment: 'Foo',
|
||||
newfieldset: {
|
||||
'hobbies[]': [
|
||||
|
|
@ -699,7 +747,23 @@ describe('<lion-fieldset>', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('will exclude form elements within an disabled fieldset', async () => {
|
||||
it('does not serialize disabled values', async () => {
|
||||
const fieldset = await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="custom[]"></${childTag}>
|
||||
<${childTag} name="custom[]"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await nextFrame();
|
||||
fieldset.formElements['custom[]'][0].modelValue = 'custom 1';
|
||||
fieldset.formElements['custom[]'][1].disabled = true;
|
||||
|
||||
expect(fieldset.serializedValue).to.deep.equal({
|
||||
'custom[]': ['custom 1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('will exclude form elements within a disabled fieldset', async () => {
|
||||
const fieldset = await fixture(html`
|
||||
<${tag} name="userData">
|
||||
<${childTag} name="comment"></${childTag}>
|
||||
|
|
@ -717,7 +781,7 @@ describe('<lion-fieldset>', () => {
|
|||
newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||
newFieldset.formElements.color.disabled = true;
|
||||
|
||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||
expect(fieldset.serializedValue).to.deep.equal({
|
||||
comment: 'Foo',
|
||||
newfieldset: {
|
||||
'hobbies[]': [
|
||||
|
|
@ -732,7 +796,7 @@ describe('<lion-fieldset>', () => {
|
|||
});
|
||||
|
||||
newFieldset.formElements.color.disabled = false;
|
||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||
expect(fieldset.serializedValue).to.deep.equal({
|
||||
comment: 'Foo',
|
||||
newfieldset: {
|
||||
'hobbies[]': [
|
||||
|
|
@ -747,43 +811,6 @@ describe('<lion-fieldset>', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('treats names with ending [] as arrays', async () => {
|
||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await nextFrame();
|
||||
fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
|
||||
fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
|
||||
fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' };
|
||||
fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
||||
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||
'hobbies[]': [
|
||||
{ checked: false, value: 'chess' },
|
||||
{ checked: false, value: 'rugby' },
|
||||
],
|
||||
'gender[]': [
|
||||
{ checked: false, value: 'male' },
|
||||
{ checked: false, value: 'female' },
|
||||
],
|
||||
color: { checked: false, value: 'blue' },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not serialize undefined values (nb radios/checkboxes are always serialized)', async () => {
|
||||
const fieldset = await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="custom[]"></${childTag}>
|
||||
<${childTag} name="custom[]"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await nextFrame();
|
||||
fieldset.formElements['custom[]'][0].modelValue = 'custom 1';
|
||||
fieldset.formElements['custom[]'][1].modelValue = undefined;
|
||||
|
||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||
'custom[]': ['custom 1'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reset', () => {
|
||||
|
|
@ -883,7 +910,7 @@ describe('<lion-fieldset>', () => {
|
|||
fieldset.submitted = true;
|
||||
fieldset.resetGroup();
|
||||
expect(fieldset.submitted).to.equal(false);
|
||||
fieldset.formElementsArray.forEach(el => {
|
||||
fieldset.formElements.forEach(el => {
|
||||
expect(el.submitted).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
33
packages/form-system/test/form-integrations.test.js
Normal file
33
packages/form-system/test/form-integrations.test.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import './helpers/umbrella-form.js';
|
||||
|
||||
// Test umbrella form
|
||||
describe('Form Integrations', () => {
|
||||
it.skip('".serializedValue" returns all non disabled fields based on form structure', async () => {
|
||||
const el = await fixture(
|
||||
html`
|
||||
<umbrella-form></umbrella-form>
|
||||
`,
|
||||
);
|
||||
const formEl = el._lionFormNode;
|
||||
expect(formEl.serializedValue).to.eql({
|
||||
bio: '',
|
||||
'checkers[]': [[]],
|
||||
comments: '',
|
||||
date: '2000-12-12',
|
||||
datepicker: '2020-12-12',
|
||||
dinosaurs: '',
|
||||
email: '',
|
||||
favoriteColor: 'hotpink',
|
||||
full_name: {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
iban: '',
|
||||
lyrics: '1',
|
||||
money: '',
|
||||
range: 2.3,
|
||||
terms: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
106
packages/form-system/test/helpers/umbrella-form.js
Normal file
106
packages/form-system/test/helpers/umbrella-form.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { LitElement, html } from '@lion/core';
|
||||
import { Required, MinLength } from '@lion/validate';
|
||||
|
||||
export class UmbrellaForm extends LitElement {
|
||||
get _lionFormNode() {
|
||||
return this.shadowRoot.querySelector('lion-form');
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<lion-form>
|
||||
<form>
|
||||
<lion-fieldset name="full_name">
|
||||
<lion-input
|
||||
name="first_name"
|
||||
label="First Name"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
name="last_name"
|
||||
label="Last Name"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input>
|
||||
</lion-fieldset>
|
||||
<lion-input-date
|
||||
name="date"
|
||||
label="Date of application"
|
||||
.modelValue="${new Date('2000-12-12')}"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input-date>
|
||||
<lion-input-datepicker
|
||||
name="datepicker"
|
||||
label="Date to be picked"
|
||||
.modelValue="${new Date('2020-12-12')}"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input-datepicker>
|
||||
<lion-textarea
|
||||
name="bio"
|
||||
label="Biography"
|
||||
.validators="${[new Required(), new MinLength(10)]}"
|
||||
help-text="Please enter at least 10 characters"
|
||||
></lion-textarea>
|
||||
<lion-input-amount name="money" label="Money"></lion-input-amount>
|
||||
<lion-input-iban name="iban" label="Iban"></lion-input-iban>
|
||||
<lion-input-email name="email" label="Email"></lion-input-email>
|
||||
<lion-checkbox-group
|
||||
label="What do you like?"
|
||||
name="checkers[]"
|
||||
.validators="${[new Required()]}"
|
||||
>
|
||||
<lion-checkbox .choiceValue=${'foo'} label="I like foo"></lion-checkbox>
|
||||
<lion-checkbox .choiceValue=${'bar'} label="I like bar"></lion-checkbox>
|
||||
<lion-checkbox .choiceValue=${'baz'} label="I like baz"></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<lion-radio-group
|
||||
name="dinosaurs"
|
||||
label="Favorite dinosaur"
|
||||
.validators="${[new Required()]}"
|
||||
>
|
||||
<lion-radio .choiceValue=${'allosaurus'} label="allosaurus"></lion-radio>
|
||||
<lion-radio .choiceValue=${'brontosaurus'} label="brontosaurus"></lion-radio>
|
||||
<lion-radio .choiceValue=${'diplodocus'} label="diplodocus"></lion-radio>
|
||||
</lion-radio-group>
|
||||
<lion-select-rich name="favoriteColor" label="Favorite color">
|
||||
<lion-options slot="input">
|
||||
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
||||
<lion-option .choiceValue=${'hotpink'} checked>Hotpink</lion-option>
|
||||
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
||||
</lion-options>
|
||||
</lion-select-rich>
|
||||
<lion-select label="Lyrics" name="lyrics" .validators="${[new Required()]}">
|
||||
<select slot="input">
|
||||
<option value="1">Fire up that loud</option>
|
||||
<option value="2">Another round of shots...</option>
|
||||
<option value="3">Drop down for what?</option>
|
||||
</select>
|
||||
</lion-select>
|
||||
<lion-input-range
|
||||
name="range"
|
||||
min="1"
|
||||
max="5"
|
||||
.modelValue="${2.3}"
|
||||
unit="%"
|
||||
step="0.1"
|
||||
label="Input range"
|
||||
></lion-input-range>
|
||||
<lion-checkbox-group name="terms" .validators="${[new Required()]}">
|
||||
<lion-checkbox label="I blindly accept all terms and conditions"></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<lion-textarea name="comments" label="Comments"></lion-textarea>
|
||||
<div class="buttons">
|
||||
<lion-button raised>Submit</lion-button>
|
||||
<lion-button
|
||||
type="button"
|
||||
raised
|
||||
@click=${ev =>
|
||||
ev.currentTarget.parentElement.parentElement.parentElement.resetGroup()}
|
||||
>Reset</lion-button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</lion-form>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('umbrella-form', UmbrellaForm);
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import { LitElement } from '@lion/core';
|
||||
import { ChoiceGroupMixin } from '@lion/choice-input';
|
||||
import { LionFieldset } from '@lion/fieldset';
|
||||
import { FormGroupMixin } from '@lion/fieldset';
|
||||
|
||||
/**
|
||||
* A wrapper around multiple radios.
|
||||
*
|
||||
* @extends {LionFieldset}
|
||||
*/
|
||||
export class LionRadioGroup extends ChoiceGroupMixin(LionFieldset) {
|
||||
export class LionRadioGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {
|
||||
connectedCallback() {
|
||||
// eslint-disable-next-line wc/guard-super-call
|
||||
super.connectedCallback();
|
||||
this._setRole('radiogroup');
|
||||
this.setAttribute('role', 'radiogroup');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ describe('<lion-radio-group>', () => {
|
|||
</lion-radio-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
const male = el.formElementsArray[0];
|
||||
const male = el.formElements[0];
|
||||
const maleInput = male.querySelector('input');
|
||||
const female = el.formElementsArray[1];
|
||||
const female = el.formElements[1];
|
||||
const femaleInput = female.querySelector('input');
|
||||
|
||||
expect(male.checked).to.equal(false);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ChoiceGroupMixin } from '@lion/choice-input';
|
||||
import { css, html, LitElement, SlotMixin } from '@lion/core';
|
||||
import { FormControlMixin, FormRegistrarMixin, InteractionStateMixin } from '@lion/field';
|
||||
import { formRegistrarManager } from '@lion/field/src/formRegistrarManager.js';
|
||||
import { formRegistrarManager } from '@lion/field/src/registration/formRegistrarManager.js';
|
||||
import { OverlayMixin, withDropdownConfig } from '@lion/overlays';
|
||||
import { ValidateMixin } from '@lion/validate';
|
||||
import '../lion-select-invoker.js';
|
||||
|
|
@ -132,6 +132,11 @@ export class LionSelectRich extends ChoiceGroupMixin(
|
|||
this.requestUpdate('modelValue');
|
||||
}
|
||||
|
||||
// TODO: quick and dirty fix. Should be possible to do it nicer on a higher layer
|
||||
get serializedValue() {
|
||||
return this.modelValue;
|
||||
}
|
||||
|
||||
get checkedIndex() {
|
||||
let checkedIndex = -1;
|
||||
this.formElements.forEach((option, i) => {
|
||||
|
|
|
|||
|
|
@ -41,15 +41,15 @@ describe('lion-select-rich', () => {
|
|||
`);
|
||||
await nextFrame();
|
||||
|
||||
expect(el.formElementsArray[0].name).to.equal('foo');
|
||||
expect(el.formElementsArray[1].name).to.equal('foo');
|
||||
expect(el.formElements[0].name).to.equal('foo');
|
||||
expect(el.formElements[1].name).to.equal('foo');
|
||||
|
||||
const validChild = await fixture(html`
|
||||
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
||||
`);
|
||||
el.appendChild(validChild);
|
||||
|
||||
expect(el.formElementsArray[2].name).to.equal('foo');
|
||||
expect(el.formElements[2].name).to.equal('foo');
|
||||
});
|
||||
|
||||
it('throws if a child element without a modelValue like { value: "foo", checked: false } tries to register', async () => {
|
||||
|
|
@ -108,7 +108,7 @@ describe('lion-select-rich', () => {
|
|||
`);
|
||||
|
||||
expect(el.modelValue).to.equal('other');
|
||||
expect(el.formElementsArray[2].checked).to.be.true;
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
});
|
||||
|
||||
it(`has a fieldName based on the label`, async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue