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 { ChoiceGroupMixin } from '@lion/choice-input';
|
||||||
import { LionFieldset } from '@lion/fieldset';
|
import { FormGroupMixin } from '@lion/fieldset';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around multiple checkboxes
|
* A wrapper around multiple checkboxes
|
||||||
*
|
*
|
||||||
* @extends {LionFieldset}
|
* @extends {LionFieldset}
|
||||||
*/
|
*/
|
||||||
export class LionCheckboxGroup extends ChoiceGroupMixin(LionFieldset) {
|
export class LionCheckboxGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.multipleChoice = true;
|
this.multipleChoice = true;
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,30 @@ export const ChoiceGroupMixin = dedupeMixin(
|
||||||
superclass =>
|
superclass =>
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
class ChoiceGroupMixin extends FormRegistrarMixin(InteractionStateMixin(superclass)) {
|
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() {
|
get modelValue() {
|
||||||
const elems = this._getCheckedElements();
|
const elems = this._getCheckedElements();
|
||||||
if (this.multipleChoice) {
|
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.map(el => el.modelValue.value);
|
||||||
}
|
}
|
||||||
return elems ? elems.modelValue.value : '';
|
return elems[0] ? elems[0].modelValue.value : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
set modelValue(value) {
|
set modelValue(value) {
|
||||||
|
|
@ -18,11 +36,19 @@ export const ChoiceGroupMixin = dedupeMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
get serializedValue() {
|
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();
|
const elems = this._getCheckedElements();
|
||||||
if (this.multipleChoice) {
|
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) {
|
set serializedValue(value) {
|
||||||
|
|
@ -53,24 +79,22 @@ export const ChoiceGroupMixin = dedupeMixin(
|
||||||
*/
|
*/
|
||||||
addFormElement(child, indexToInsertAt) {
|
addFormElement(child, indexToInsertAt) {
|
||||||
this._throwWhenInvalidChildModelValue(child);
|
this._throwWhenInvalidChildModelValue(child);
|
||||||
|
// TODO: nice to have or does it have a function (since names are meant as keys for
|
||||||
|
// formElements)?
|
||||||
this.__delegateNameAttribute(child);
|
this.__delegateNameAttribute(child);
|
||||||
super.addFormElement(child, indexToInsertAt);
|
super.addFormElement(child, indexToInsertAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override from LionFieldset
|
* @override
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
_getFromAllFormElements(property, filterCondition = () => true) {
|
||||||
get _childrenCanHaveSameName() {
|
// For modelValue and serializedValue, an exception should be made,
|
||||||
return true;
|
// The reset can be requested from children
|
||||||
|
if (property === 'modelValue' || property === 'serializedValue') {
|
||||||
|
return this[property];
|
||||||
}
|
}
|
||||||
|
return this.formElements.filter(filterCondition).map(el => el.property);
|
||||||
/**
|
|
||||||
* @override from LionFieldset
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
get _childNamesCanBeDuplicate() {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_throwWhenInvalidChildModelValue(child) {
|
_throwWhenInvalidChildModelValue(child) {
|
||||||
|
|
@ -108,7 +132,7 @@ export const ChoiceGroupMixin = dedupeMixin(
|
||||||
if (target.checked === false) return;
|
if (target.checked === false) return;
|
||||||
|
|
||||||
const groupName = target.name;
|
const groupName = target.name;
|
||||||
this.formElementsArray
|
this.formElements
|
||||||
.filter(i => i.name === groupName)
|
.filter(i => i.name === groupName)
|
||||||
.forEach(choice => {
|
.forEach(choice => {
|
||||||
if (choice !== target) {
|
if (choice !== target) {
|
||||||
|
|
@ -119,11 +143,8 @@ export const ChoiceGroupMixin = dedupeMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
_getCheckedElements() {
|
_getCheckedElements() {
|
||||||
const filtered = this.formElementsArray.filter(el => el.checked === true);
|
// We want to filter out disabled values out by default
|
||||||
if (this.multipleChoice) {
|
return this.formElements.filter(el => el.checked && !el.disabled);
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
return filtered.length > 0 ? filtered[0] : undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _setCheckedElements(value, check) {
|
async _setCheckedElements(value, check) {
|
||||||
|
|
@ -131,12 +152,12 @@ export const ChoiceGroupMixin = dedupeMixin(
|
||||||
await this.registrationReady;
|
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) {
|
if (this.multipleChoice) {
|
||||||
this.formElementsArray[i].checked = value.includes(this.formElementsArray[i].value);
|
this.formElements[i].checked = value.includes(this.formElements[i].value);
|
||||||
} else if (check(this.formElementsArray[i], value)) {
|
} else if (check(this.formElements[i], value)) {
|
||||||
// Allows checking against custom values e.g. formattedValue or serializedValue
|
// 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 { html, LitElement } from '@lion/core';
|
||||||
import { LionFieldset } from '@lion/fieldset';
|
import { FormGroupMixin } from '@lion/fieldset';
|
||||||
import { LionInput } from '@lion/input';
|
import { LionInput } from '@lion/input';
|
||||||
import { Required } from '@lion/validate';
|
import { Required } from '@lion/validate';
|
||||||
import { expect, fixture, nextFrame } from '@open-wc/testing';
|
import { expect, fixture, nextFrame } from '@open-wc/testing';
|
||||||
import { ChoiceGroupMixin } from '../src/ChoiceGroupMixin.js';
|
import { ChoiceGroupMixin } from '../src/ChoiceGroupMixin.js';
|
||||||
import { ChoiceInputMixin } from '../src/ChoiceInputMixin.js';
|
import { ChoiceInputMixin } from '../src/ChoiceInputMixin.js';
|
||||||
|
import '@lion/fieldset/lion-fieldset.js';
|
||||||
|
|
||||||
describe('ChoiceGroupMixin', () => {
|
describe('ChoiceGroupMixin', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
class ChoiceInput extends ChoiceInputMixin(LionInput) {}
|
class ChoiceInput extends ChoiceInputMixin(LionInput) {}
|
||||||
customElements.define('choice-group-input', ChoiceInput);
|
customElements.define('choice-group-input', ChoiceInput);
|
||||||
|
|
||||||
class ChoiceGroup extends ChoiceGroupMixin(LionFieldset) {}
|
class ChoiceGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {}
|
||||||
customElements.define('choice-group', ChoiceGroup);
|
customElements.define('choice-group', ChoiceGroup);
|
||||||
|
|
||||||
class ChoiceGroupMultiple extends ChoiceGroupMixin(LionFieldset) {
|
class ChoiceGroupMultiple extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.multipleChoice = true;
|
this.multipleChoice = true;
|
||||||
|
|
@ -33,9 +34,9 @@ describe('ChoiceGroupMixin', () => {
|
||||||
`);
|
`);
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
expect(el.modelValue).to.equal('female');
|
expect(el.modelValue).to.equal('female');
|
||||||
el.formElementsArray[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
expect(el.modelValue).to.equal('male');
|
expect(el.modelValue).to.equal('male');
|
||||||
el.formElementsArray[2].checked = true;
|
el.formElements[2].checked = true;
|
||||||
expect(el.modelValue).to.equal('other');
|
expect(el.modelValue).to.equal('other');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -68,15 +69,15 @@ describe('ChoiceGroupMixin', () => {
|
||||||
`);
|
`);
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
|
|
||||||
expect(el.formElementsArray[0].name).to.equal('gender');
|
expect(el.formElements[0].name).to.equal('gender');
|
||||||
expect(el.formElementsArray[1].name).to.equal('gender');
|
expect(el.formElements[1].name).to.equal('gender');
|
||||||
|
|
||||||
const validChild = await fixture(html`
|
const validChild = await fixture(html`
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
`);
|
`);
|
||||||
el.appendChild(validChild);
|
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 () => {
|
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;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(el.modelValue).to.equal('other');
|
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 () => {
|
it('can handle complex data via choiceValue', async () => {
|
||||||
|
|
@ -127,7 +128,7 @@ describe('ChoiceGroupMixin', () => {
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
|
|
||||||
expect(el.modelValue).to.equal(date);
|
expect(el.modelValue).to.equal(date);
|
||||||
el.formElementsArray[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
expect(el.modelValue).to.deep.equal({ some: 'data' });
|
expect(el.modelValue).to.deep.equal({ some: 'data' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -141,7 +142,7 @@ describe('ChoiceGroupMixin', () => {
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
|
|
||||||
expect(el.modelValue).to.equal(0);
|
expect(el.modelValue).to.equal(0);
|
||||||
el.formElementsArray[1].checked = true;
|
el.formElements[1].checked = true;
|
||||||
expect(el.modelValue).to.equal('');
|
expect(el.modelValue).to.equal('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -160,7 +161,7 @@ describe('ChoiceGroupMixin', () => {
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
expect(el.modelValue).to.equal('female');
|
expect(el.modelValue).to.equal('female');
|
||||||
el.modelValue = 'other';
|
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 () => {
|
it('expect child nodes to only fire one model-value-changed event per instance', async () => {
|
||||||
|
|
@ -180,14 +181,14 @@ describe('ChoiceGroupMixin', () => {
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
counter = 0; // reset after setup which may result in different results
|
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
|
expect(counter).to.equal(2); // male becomes checked, female becomes unchecked
|
||||||
|
|
||||||
// not changed values trigger no event
|
// not changed values trigger no event
|
||||||
el.formElementsArray[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
expect(counter).to.equal(2);
|
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
|
expect(counter).to.equal(4); // other becomes checked, male becomes unchecked
|
||||||
|
|
||||||
// not found values trigger no event
|
// not found values trigger no event
|
||||||
|
|
@ -211,12 +212,12 @@ describe('ChoiceGroupMixin', () => {
|
||||||
expect(el.validationStates).to.have.a.property('error');
|
expect(el.validationStates).to.have.a.property('error');
|
||||||
expect(el.validationStates.error).to.have.a.property('Required');
|
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.hasFeedbackFor).not.to.include('error');
|
||||||
expect(el.validationStates).to.have.a.property('error');
|
expect(el.validationStates).to.have.a.property('error');
|
||||||
expect(el.validationStates.error).not.to.have.a.property('Required');
|
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.hasFeedbackFor).not.to.include('error');
|
||||||
expect(el.validationStates).to.have.a.property('error');
|
expect(el.validationStates).to.have.a.property('error');
|
||||||
expect(el.validationStates.error).not.to.have.a.property('Required');
|
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-input .choiceValue=${'female'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`);
|
||||||
el.formElementsArray[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
expect(el.serializedValue).to.deep.equal({ checked: true, value: 'male' });
|
expect(el.serializedValue).to.deep.equal('male');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns serialized value on unchecked state', async () => {
|
it('returns serialized value on unchecked state', async () => {
|
||||||
|
|
@ -256,9 +257,9 @@ describe('ChoiceGroupMixin', () => {
|
||||||
`);
|
`);
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
expect(el.modelValue).to.eql(['female']);
|
expect(el.modelValue).to.eql(['female']);
|
||||||
el.formElementsArray[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
expect(el.modelValue).to.eql(['male', 'female']);
|
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']);
|
expect(el.modelValue).to.eql(['male', 'female', 'other']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -276,8 +277,8 @@ describe('ChoiceGroupMixin', () => {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
el.modelValue = ['male', 'other'];
|
el.modelValue = ['male', 'other'];
|
||||||
expect(el.modelValue).to.eql(['male', 'other']);
|
expect(el.modelValue).to.eql(['male', 'other']);
|
||||||
expect(el.formElementsArray[0].checked).to.be.true;
|
expect(el.formElements[0].checked).to.be.true;
|
||||||
expect(el.formElementsArray[2].checked).to.be.true;
|
expect(el.formElements[2].checked).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unchecks non-matching checkboxes when setting the modelValue', async () => {
|
it('unchecks non-matching checkboxes when setting the modelValue', async () => {
|
||||||
|
|
@ -293,13 +294,40 @@ describe('ChoiceGroupMixin', () => {
|
||||||
await el.registrationReady;
|
await el.registrationReady;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.modelValue).to.eql(['male', 'other']);
|
expect(el.modelValue).to.eql(['male', 'other']);
|
||||||
expect(el.formElementsArray[0].checked).to.be.true;
|
expect(el.formElements[0].checked).to.be.true;
|
||||||
expect(el.formElementsArray[2].checked).to.be.true;
|
expect(el.formElements[2].checked).to.be.true;
|
||||||
|
|
||||||
el.modelValue = ['female'];
|
el.modelValue = ['female'];
|
||||||
expect(el.formElementsArray[0].checked).to.be.false;
|
expect(el.formElements[0].checked).to.be.false;
|
||||||
expect(el.formElementsArray[1].checked).to.be.true;
|
expect(el.formElements[1].checked).to.be.true;
|
||||||
expect(el.formElementsArray[2].checked).to.be.false;
|
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 { FocusMixin } from './src/FocusMixin.js';
|
||||||
export { FormatMixin } from './src/FormatMixin.js';
|
export { FormatMixin } from './src/FormatMixin.js';
|
||||||
|
export { FieldCustomMixin } from './src/FieldCustomMixin.js';
|
||||||
export { FormControlMixin } from './src/FormControlMixin.js';
|
export { FormControlMixin } from './src/FormControlMixin.js';
|
||||||
export { InteractionStateMixin } from './src/InteractionStateMixin.js'; // applies FocusMixin
|
export { InteractionStateMixin } from './src/InteractionStateMixin.js'; // applies FocusMixin
|
||||||
export { LionField } from './src/LionField.js';
|
export { LionField } from './src/LionField.js';
|
||||||
export { FormRegisteringMixin } from './src/FormRegisteringMixin.js';
|
export { FormRegisteringMixin } from './src/registration/FormRegisteringMixin.js';
|
||||||
export { FormRegistrarMixin } from './src/FormRegistrarMixin.js';
|
export { FormRegistrarMixin } from './src/registration/FormRegistrarMixin.js';
|
||||||
export { FormRegistrarPortalMixin } from './src/FormRegistrarPortalMixin.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 { 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';
|
import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,11 +28,18 @@ export const FormControlMixin = dedupeMixin(
|
||||||
class FormControlMixin extends FormRegisteringMixin(SlotMixin(superclass)) {
|
class FormControlMixin extends FormRegisteringMixin(SlotMixin(superclass)) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
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
|
* When no light dom defined and prop set
|
||||||
*/
|
*/
|
||||||
label: String,
|
label: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When no light dom defined and prop set
|
* When no light dom defined and prop set
|
||||||
*/
|
*/
|
||||||
|
|
@ -40,12 +47,10 @@ export const FormControlMixin = dedupeMixin(
|
||||||
type: String,
|
type: String,
|
||||||
attribute: 'help-text',
|
attribute: 'help-text',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains all elements that should end up in aria-labelledby of `._inputNode`
|
* Contains all elements that should end up in aria-labelledby of `._inputNode`
|
||||||
*/
|
*/
|
||||||
_ariaLabelledNodes: Array,
|
_ariaLabelledNodes: Array,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains all elements that should end up in aria-describedby of `._inputNode`
|
* Contains all elements that should end up in aria-describedby of `._inputNode`
|
||||||
*/
|
*/
|
||||||
|
|
@ -73,6 +78,14 @@ export const FormControlMixin = dedupeMixin(
|
||||||
this.requestUpdate('helpText', oldValue);
|
this.requestUpdate('helpText', oldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set fieldName(value) {
|
||||||
|
this.__fieldName = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fieldName() {
|
||||||
|
return this.__fieldName || this.label || this.name;
|
||||||
|
}
|
||||||
|
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -146,9 +159,6 @@ export const FormControlMixin = dedupeMixin(
|
||||||
this._enhanceLightDomA11y();
|
this._enhanceLightDomA11y();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Public methods
|
|
||||||
*/
|
|
||||||
_enhanceLightDomClasses() {
|
_enhanceLightDomClasses() {
|
||||||
if (this._inputNode) {
|
if (this._inputNode) {
|
||||||
this._inputNode.classList.add('form-control');
|
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 { LitElement } from '@lion/core';
|
||||||
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { FormRegisteringMixin } from '../src/FormRegisteringMixin.js';
|
import { FormRegisteringMixin } from '../src/registration/FormRegisteringMixin.js';
|
||||||
import { formRegistrarManager } from '../src/formRegistrarManager.js';
|
import { formRegistrarManager } from '../src/registration/formRegistrarManager.js';
|
||||||
import { FormRegistrarMixin } from '../src/FormRegistrarMixin.js';
|
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
||||||
import { FormRegistrarPortalMixin } from '../src/FormRegistrarPortalMixin.js';
|
import { FormRegistrarPortalMixin } from '../src/registration/FormRegistrarPortalMixin.js';
|
||||||
|
|
||||||
export const runRegistrationSuite = customConfig => {
|
export const runRegistrationSuite = customConfig => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
baseElement: HTMLElement,
|
baseElement: HTMLElement,
|
||||||
suffix: null,
|
|
||||||
...customConfig,
|
...customConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe(`FormRegistrationMixins${cfg.suffix ? ` (${cfg.suffix})` : ''}`, () => {
|
describe('FormRegistrationMixins', () => {
|
||||||
let parentTag;
|
let parentTag;
|
||||||
let childTag;
|
let childTag;
|
||||||
let portalTag;
|
let portalTag;
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export { LionFieldset } from './src/LionFieldset.js';
|
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 { LitElement } from '@lion/core';
|
||||||
import { DisabledMixin } from '@lion/core/src/DisabledMixin.js';
|
import { FormGroupMixin } from './FormGroupMixin.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';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* @customElement lion-fieldset
|
||||||
* @extends {LitElement}
|
* @extends {LitElement}
|
||||||
*/
|
*/
|
||||||
export class LionFieldset extends FormRegistrarMixin(
|
export class LionFieldset extends FormGroupMixin(LitElement) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.disabled = false;
|
/** @override from FormRegistrarMixin */
|
||||||
this.submitted = false;
|
this._isFormOrFieldset = true;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
expect,
|
expect,
|
||||||
fixture,
|
fixture,
|
||||||
|
fixtureSync,
|
||||||
html,
|
html,
|
||||||
unsafeStatic,
|
unsafeStatic,
|
||||||
triggerFocusFor,
|
triggerFocusFor,
|
||||||
|
|
@ -39,7 +40,9 @@ beforeEach(() => {
|
||||||
localizeTearDown();
|
localizeTearDown();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: seperate fieldset and FormGroup tests
|
||||||
describe('<lion-fieldset>', () => {
|
describe('<lion-fieldset>', () => {
|
||||||
|
// TODO: Tests below belong to FormControlMixin. Preferably run suite integration test
|
||||||
it(`has a fieldName based on the label`, async () => {
|
it(`has a fieldName based on the label`, async () => {
|
||||||
const el1 = await fixture(html`<${tag} label="foo">${inputSlots}</${tag}>`);
|
const el1 = await fixture(html`<${tag} label="foo">${inputSlots}</${tag}>`);
|
||||||
expect(el1.fieldName).to.equal(el1._labelNode.textContent);
|
expect(el1.fieldName).to.equal(el1._labelNode.textContent);
|
||||||
|
|
@ -60,13 +63,15 @@ describe('<lion-fieldset>', () => {
|
||||||
expect(el.__fieldName).to.equal(el.fieldName);
|
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}>`);
|
const el = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||||
await nextFrame();
|
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);
|
expect(el.formElements['hobbies[]'].length).to.equal(2);
|
||||||
el.removeChild(el.formElements['hobbies[]'][0]);
|
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);
|
expect(el.formElements['hobbies[]'].length).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -79,9 +84,9 @@ describe('<lion-fieldset>', () => {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
expect(el.formElementsArray.length).to.equal(1);
|
expect(el.formElements.length).to.equal(1);
|
||||||
el.children[0].removeChild(el.formElements.foo);
|
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 () => {
|
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[]'][0].modelValue = { checked: false, value: 'chess' };
|
||||||
el.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
|
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[]'].length).to.equal(2);
|
||||||
expect(el.formElements['hobbies[]'][0].modelValue.value).to.equal('chess');
|
expect(el.formElements['hobbies[]'][0].modelValue.value).to.equal('chess');
|
||||||
expect(el.formElements['gender[]'][0].modelValue.value).to.equal('male');
|
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 el = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||||
const newField = await fixture(html`<${childTag} name="lastName"></${childTag}>`);
|
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);
|
el.appendChild(newField);
|
||||||
expect(Object.keys(el.formElements).length).to.equal(4);
|
expect(el.formElements.keys().length).to.equal(4);
|
||||||
|
|
||||||
el._inputNode.removeChild(newField);
|
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 () => {
|
it('can read/write all values (of every input) via this.modelValue', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag}>
|
<${tag}>
|
||||||
|
|
@ -231,6 +238,32 @@ describe('<lion-fieldset>', () => {
|
||||||
expect(el.formElements.lastName.modelValue).to.equal(2);
|
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 () => {
|
it('does not throw if setter data of this.modelValue can not be handled', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag}>
|
<${tag}>
|
||||||
|
|
@ -308,7 +341,7 @@ describe('<lion-fieldset>', () => {
|
||||||
expect(el.modelValue).to.eql(initialSerializedValue);
|
expect(el.modelValue).to.eql(initialSerializedValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validation', () => {
|
describe('Validation', () => {
|
||||||
it('validates on init', async () => {
|
it('validates on init', async () => {
|
||||||
class IsCat extends Validator {
|
class IsCat extends Validator {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -409,7 +442,7 @@ describe('<lion-fieldset>', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('interaction states', () => {
|
describe('Interaction states', () => {
|
||||||
it('has false states (dirty, touched, prefilled) on init', async () => {
|
it('has false states (dirty, touched, prefilled) on init', async () => {
|
||||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
|
|
@ -453,7 +486,7 @@ describe('<lion-fieldset>', () => {
|
||||||
it('becomes prefilled if all form elements are prefilled', async () => {
|
it('becomes prefilled if all form elements are prefilled', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag}>
|
<${tag}>
|
||||||
<${childTag} name="input1" prefilled></${childTag}>
|
<${childTag} name="input1" .modelValue="${'prefilled'}"></${childTag}>
|
||||||
<${childTag} name="input2"></${childTag}>
|
<${childTag} name="input2"></${childTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
|
@ -462,8 +495,8 @@ describe('<lion-fieldset>', () => {
|
||||||
|
|
||||||
const el2 = await fixture(html`
|
const el2 = await fixture(html`
|
||||||
<${tag}>
|
<${tag}>
|
||||||
<${childTag} name="input1" prefilled></${childTag}>
|
<${childTag} name="input1" .modelValue="${'prefilled'}"></${childTag}>
|
||||||
<${childTag} name="input2" prefilled></${childTag}>
|
<${childTag} name="input2" .modelValue="${'prefilled'}"></${childTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
|
|
@ -515,7 +548,7 @@ describe('<lion-fieldset>', () => {
|
||||||
`);
|
`);
|
||||||
|
|
||||||
outside.click();
|
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[1].focus();
|
||||||
el.children[2].focus();
|
el.children[2].focus();
|
||||||
|
|
@ -524,7 +557,6 @@ describe('<lion-fieldset>', () => {
|
||||||
outside.click(); // blur the group via a click
|
outside.click(); // blur the group via a click
|
||||||
outside.focus(); // a real mouse click moves focus as well
|
outside.focus(); // a real mouse click moves focus as well
|
||||||
expect(el.touched).to.be.true;
|
expect(el.touched).to.be.true;
|
||||||
|
|
||||||
expect(el2.touched).to.be.false;
|
expect(el2.touched).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -591,9 +623,30 @@ describe('<lion-fieldset>', () => {
|
||||||
expect(el.validationStates.error.Input1IsTen).to.be.true;
|
expect(el.validationStates.error.Input1IsTen).to.be.true;
|
||||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
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 () => {
|
it('use form elements serializedValue', async () => {
|
||||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
|
|
@ -604,7 +657,7 @@ describe('<lion-fieldset>', () => {
|
||||||
fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
||||||
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||||
expect(fieldset.formElements['hobbies[]'][0].serializedValue).to.equal('Bar-serialized');
|
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' }],
|
'hobbies[]': ['Bar-serialized', { checked: false, value: 'rugby' }],
|
||||||
'gender[]': [
|
'gender[]': [
|
||||||
{ checked: false, value: 'male' },
|
{ 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 () => {
|
it('0 is a valid value to be serialized', async () => {
|
||||||
const fieldset = await fixture(html`
|
const fieldset = await fixture(html`
|
||||||
<${tag}>
|
<${tag}>
|
||||||
|
|
@ -621,48 +695,22 @@ describe('<lion-fieldset>', () => {
|
||||||
</${tag}>`);
|
</${tag}>`);
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
fieldset.formElements.price.modelValue = 0;
|
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 () => {
|
it.skip('serializes undefined values as ""(nb radios/checkboxes are always serialized)', async () => {
|
||||||
const fieldset = await fixture(html`<${tag}></${tag}>`);
|
const fieldset = await fixture(html`
|
||||||
|
<${tag}>
|
||||||
|
<${childTag} name="custom[]"></${childTag}>
|
||||||
|
<${childTag} name="custom[]"></${childTag}>
|
||||||
|
</${tag}>
|
||||||
|
`);
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
const elements = [{ serializedValue: 0 }];
|
fieldset.formElements['custom[]'][0].modelValue = 'custom 1';
|
||||||
expect(fieldset.__serializeElements(elements)).to.deep.equal([0]);
|
fieldset.formElements['custom[]'][1].modelValue = undefined;
|
||||||
});
|
|
||||||
|
|
||||||
it('form elements which are not disabled', async () => {
|
expect(fieldset.serializedValue).to.deep.equal({
|
||||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
'custom[]': ['custom 1', ''],
|
||||||
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' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -681,9 +729,9 @@ describe('<lion-fieldset>', () => {
|
||||||
newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
||||||
newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||||
fieldset.formElements.comment.modelValue = 'Foo';
|
fieldset.formElements.comment.modelValue = 'Foo';
|
||||||
expect(Object.keys(fieldset.formElements).length).to.equal(2);
|
expect(fieldset.formElements.keys().length).to.equal(2);
|
||||||
expect(Object.keys(newFieldset.formElements).length).to.equal(3);
|
expect(newFieldset.formElements.keys().length).to.equal(3);
|
||||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
expect(fieldset.serializedValue).to.deep.equal({
|
||||||
comment: 'Foo',
|
comment: 'Foo',
|
||||||
newfieldset: {
|
newfieldset: {
|
||||||
'hobbies[]': [
|
'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`
|
const fieldset = await fixture(html`
|
||||||
<${tag} name="userData">
|
<${tag} name="userData">
|
||||||
<${childTag} name="comment"></${childTag}>
|
<${childTag} name="comment"></${childTag}>
|
||||||
|
|
@ -717,7 +781,7 @@ describe('<lion-fieldset>', () => {
|
||||||
newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||||
newFieldset.formElements.color.disabled = true;
|
newFieldset.formElements.color.disabled = true;
|
||||||
|
|
||||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
expect(fieldset.serializedValue).to.deep.equal({
|
||||||
comment: 'Foo',
|
comment: 'Foo',
|
||||||
newfieldset: {
|
newfieldset: {
|
||||||
'hobbies[]': [
|
'hobbies[]': [
|
||||||
|
|
@ -732,7 +796,7 @@ describe('<lion-fieldset>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
newFieldset.formElements.color.disabled = false;
|
newFieldset.formElements.color.disabled = false;
|
||||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
expect(fieldset.serializedValue).to.deep.equal({
|
||||||
comment: 'Foo',
|
comment: 'Foo',
|
||||||
newfieldset: {
|
newfieldset: {
|
||||||
'hobbies[]': [
|
'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', () => {
|
describe('Reset', () => {
|
||||||
|
|
@ -883,7 +910,7 @@ describe('<lion-fieldset>', () => {
|
||||||
fieldset.submitted = true;
|
fieldset.submitted = true;
|
||||||
fieldset.resetGroup();
|
fieldset.resetGroup();
|
||||||
expect(fieldset.submitted).to.equal(false);
|
expect(fieldset.submitted).to.equal(false);
|
||||||
fieldset.formElementsArray.forEach(el => {
|
fieldset.formElements.forEach(el => {
|
||||||
expect(el.submitted).to.equal(false);
|
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 { ChoiceGroupMixin } from '@lion/choice-input';
|
||||||
import { LionFieldset } from '@lion/fieldset';
|
import { FormGroupMixin } from '@lion/fieldset';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around multiple radios.
|
* A wrapper around multiple radios.
|
||||||
*
|
*
|
||||||
* @extends {LionFieldset}
|
* @extends {LionFieldset}
|
||||||
*/
|
*/
|
||||||
export class LionRadioGroup extends ChoiceGroupMixin(LionFieldset) {
|
export class LionRadioGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
// eslint-disable-next-line wc/guard-super-call
|
// eslint-disable-next-line wc/guard-super-call
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._setRole('radiogroup');
|
this.setAttribute('role', 'radiogroup');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,9 @@ describe('<lion-radio-group>', () => {
|
||||||
</lion-radio-group>
|
</lion-radio-group>
|
||||||
`);
|
`);
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
const male = el.formElementsArray[0];
|
const male = el.formElements[0];
|
||||||
const maleInput = male.querySelector('input');
|
const maleInput = male.querySelector('input');
|
||||||
const female = el.formElementsArray[1];
|
const female = el.formElements[1];
|
||||||
const femaleInput = female.querySelector('input');
|
const femaleInput = female.querySelector('input');
|
||||||
|
|
||||||
expect(male.checked).to.equal(false);
|
expect(male.checked).to.equal(false);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { ChoiceGroupMixin } from '@lion/choice-input';
|
import { ChoiceGroupMixin } from '@lion/choice-input';
|
||||||
import { css, html, LitElement, SlotMixin } from '@lion/core';
|
import { css, html, LitElement, SlotMixin } from '@lion/core';
|
||||||
import { FormControlMixin, FormRegistrarMixin, InteractionStateMixin } from '@lion/field';
|
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 { OverlayMixin, withDropdownConfig } from '@lion/overlays';
|
||||||
import { ValidateMixin } from '@lion/validate';
|
import { ValidateMixin } from '@lion/validate';
|
||||||
import '../lion-select-invoker.js';
|
import '../lion-select-invoker.js';
|
||||||
|
|
@ -132,6 +132,11 @@ export class LionSelectRich extends ChoiceGroupMixin(
|
||||||
this.requestUpdate('modelValue');
|
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() {
|
get checkedIndex() {
|
||||||
let checkedIndex = -1;
|
let checkedIndex = -1;
|
||||||
this.formElements.forEach((option, i) => {
|
this.formElements.forEach((option, i) => {
|
||||||
|
|
|
||||||
|
|
@ -41,15 +41,15 @@ describe('lion-select-rich', () => {
|
||||||
`);
|
`);
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
|
|
||||||
expect(el.formElementsArray[0].name).to.equal('foo');
|
expect(el.formElements[0].name).to.equal('foo');
|
||||||
expect(el.formElementsArray[1].name).to.equal('foo');
|
expect(el.formElements[1].name).to.equal('foo');
|
||||||
|
|
||||||
const validChild = await fixture(html`
|
const validChild = await fixture(html`
|
||||||
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
||||||
`);
|
`);
|
||||||
el.appendChild(validChild);
|
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 () => {
|
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.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 () => {
|
it(`has a fieldName based on the label`, async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue