Merge pull request #583 from ing-bank/fix/serializedValues
feat: api normalisation formElements and values
This commit is contained in:
commit
d5a8e86816
44 changed files with 1374 additions and 1037 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];
|
||||||
/**
|
}
|
||||||
* @override from LionFieldset
|
return this.formElements.filter(filterCondition).map(el => el.property);
|
||||||
*/
|
|
||||||
// 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'],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -101,12 +101,6 @@ function deserializeDate(serializeValue, options) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### FieldCustomMixin
|
|
||||||
|
|
||||||
When creating your own custom input, please use `FieldCustomMixin` as a basis for this.
|
|
||||||
Concrete examples can be found at [`<lion-input-date>`](../../input-date/) and
|
|
||||||
[`<lion-input-amount>`](../../input-amount/).
|
|
||||||
|
|
||||||
## Flow diagram
|
## Flow diagram
|
||||||
|
|
||||||
The following flow diagram is based on both end user input and interaction programmed by the
|
The following flow diagram is based on both end user input and interaction programmed by the
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
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 { 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,46 +0,0 @@
|
||||||
import { dedupeMixin, nothing } from '@lion/core';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* #FieldCustomMixin
|
|
||||||
*
|
|
||||||
* @polymerMixin
|
|
||||||
* @mixinFunction
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const FieldCustomMixin = dedupeMixin(
|
|
||||||
superclass =>
|
|
||||||
// eslint-disable-next-line no-shadow, max-len
|
|
||||||
class FieldCustomMixin extends superclass {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* When no light dom defined and prop set
|
|
||||||
*/
|
|
||||||
disableHelpText: {
|
|
||||||
type: Boolean,
|
|
||||||
attribute: 'disable-help-text',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get slots() {
|
|
||||||
return {
|
|
||||||
...super.slots,
|
|
||||||
'help-text': () => {
|
|
||||||
if (!this.disableHelpText) {
|
|
||||||
return super.slots['help-text']();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
helpTextTemplate(...args) {
|
|
||||||
if (this.disableHelpText || !super.helpTextTemplate) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.helpTextTemplate.apply(this, args);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -164,7 +164,7 @@ export const FormatMixin = dedupeMixin(
|
||||||
* @returns {String} serializedValue
|
* @returns {String} serializedValue
|
||||||
*/
|
*/
|
||||||
serializer(v) {
|
serializer(v) {
|
||||||
return v;
|
return v !== undefined ? v : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -175,7 +175,7 @@ export const FormatMixin = dedupeMixin(
|
||||||
* @returns {Object} modelValue
|
* @returns {Object} modelValue
|
||||||
*/
|
*/
|
||||||
deserializer(v) {
|
deserializer(v) {
|
||||||
return v;
|
return v === undefined ? '' : v;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,13 @@ export const InteractionStateMixin = dedupeMixin(
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
reflect: true,
|
reflect: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when user has typed in something in the input field.
|
* True when user has changed the value of the field.
|
||||||
*/
|
*/
|
||||||
dirty: {
|
dirty: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
reflect: true,
|
reflect: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when user has left non-empty field or input is prefilled.
|
* True when user has left non-empty field or input is prefilled.
|
||||||
* The name must be seen from the point of view of the input field:
|
* The name must be seen from the point of view of the input field:
|
||||||
|
|
@ -112,9 +110,8 @@ export const InteractionStateMixin = dedupeMixin(
|
||||||
* Since this method will be called twice in last mentioned scenario, it must stay idempotent.
|
* Since this method will be called twice in last mentioned scenario, it must stay idempotent.
|
||||||
*/
|
*/
|
||||||
initInteractionState() {
|
initInteractionState() {
|
||||||
if (this.constructor._isPrefilled(this.modelValue)) {
|
this.dirty = false;
|
||||||
this.prefilled = true;
|
this.prefilled = this.constructor._isPrefilled(this.modelValue);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,6 @@ export class LionField extends FormControlMixin(
|
||||||
// make sure validation can be triggered based on observer
|
// make sure validation can be triggered based on observer
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
autocomplete: {
|
autocomplete: {
|
||||||
type: String,
|
type: String,
|
||||||
reflect: true,
|
reflect: true,
|
||||||
|
|
@ -217,12 +213,4 @@ export class LionField extends FormControlMixin(
|
||||||
this._inputNode.value = newValue;
|
this._inputNode.value = newValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set fieldName(value) {
|
|
||||||
this.__fieldName = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
get fieldName() {
|
|
||||||
return this.__fieldName || this.label || this.name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe(`FormatMixin ${cfg.suffix ? `(${cfg.suffix})` : ''}`, async () => {
|
describe('FormatMixin', async () => {
|
||||||
let elem;
|
let elem;
|
||||||
let nonFormat;
|
let nonFormat;
|
||||||
let fooFormat;
|
let fooFormat;
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,10 @@ export function runInteractionStateMixinSuite(customConfig) {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
tagString: null,
|
tagString: null,
|
||||||
allowedModelValueTypes: [Array, Object, Number, Boolean, String, Date],
|
allowedModelValueTypes: [Array, Object, Number, Boolean, String, Date],
|
||||||
suffix: '',
|
|
||||||
...customConfig,
|
...customConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe(`InteractionStateMixin ${cfg.suffix ? `(${cfg.suffix})` : ''}`, async () => {
|
describe(`InteractionStateMixin`, async () => {
|
||||||
let tag;
|
let tag;
|
||||||
before(() => {
|
before(() => {
|
||||||
if (!cfg.tagString) {
|
if (!cfg.tagString) {
|
||||||
|
|
@ -174,6 +173,18 @@ export function runInteractionStateMixinSuite(customConfig) {
|
||||||
expect(el.prefilled).to.be.true;
|
expect(el.prefilled).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has a method initInteractionState()', async () => {
|
||||||
|
const el = await fixture(html`<${tag}></${tag}>`);
|
||||||
|
el.modelValue = 'Some value';
|
||||||
|
expect(el.dirty).to.be.true;
|
||||||
|
expect(el.touched).to.be.false;
|
||||||
|
expect(el.prefilled).to.be.false;
|
||||||
|
el.initInteractionState();
|
||||||
|
expect(el.dirty).to.be.false;
|
||||||
|
expect(el.touched).to.be.false;
|
||||||
|
expect(el.prefilled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
describe('SubClassers', () => {
|
describe('SubClassers', () => {
|
||||||
it('can override the `_leaveEvent`', async () => {
|
it('can override the `_leaveEvent`', async () => {
|
||||||
const tagLeaveString = defineCE(
|
const tagLeaveString = defineCE(
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { expect, fixture, defineCE } from '@open-wc/testing';
|
|
||||||
import { LionField } from '../src/LionField.js';
|
|
||||||
|
|
||||||
import { FieldCustomMixin } from '../src/FieldCustomMixin.js';
|
|
||||||
|
|
||||||
describe('FieldCustomMixin', () => {
|
|
||||||
const inputSlot = '<input slot="input" />';
|
|
||||||
let elem;
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
const FieldCustomMixinClass = class extends FieldCustomMixin(LionField) {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
elem = defineCE(FieldCustomMixinClass);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has the capability to disable help text', async () => {
|
|
||||||
const lionField = await fixture(`
|
|
||||||
<${elem} disable-help-text>${inputSlot}</${elem}>
|
|
||||||
`);
|
|
||||||
expect(Array.from(lionField.children).find(child => child.slot === 'help-text')).to.equal(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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('(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('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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import '@lion/fieldset/lion-fieldset.js';
|
||||||
import '@lion/form/lion-form.js';
|
import '@lion/form/lion-form.js';
|
||||||
import '@lion/input-amount/lion-input-amount.js';
|
import '@lion/input-amount/lion-input-amount.js';
|
||||||
import '@lion/input-date/lion-input-date.js';
|
import '@lion/input-date/lion-input-date.js';
|
||||||
|
import '@lion/input-datepicker/lion-input-datepicker.js';
|
||||||
import '@lion/input-email/lion-input-email.js';
|
import '@lion/input-email/lion-input-email.js';
|
||||||
import '@lion/input-iban/lion-input-iban.js';
|
import '@lion/input-iban/lion-input-iban.js';
|
||||||
import '@lion/input-range/lion-input-range.js';
|
import '@lion/input-range/lion-input-range.js';
|
||||||
|
|
@ -28,25 +29,35 @@ For usage and installation please see the appropriate packages.
|
||||||
|
|
||||||
<Preview>
|
<Preview>
|
||||||
<Story name="Example">
|
<Story name="Example">
|
||||||
{html`
|
{() => {
|
||||||
|
Required.getMessage = () => 'Please enter a value';
|
||||||
|
return html`
|
||||||
<lion-form>
|
<lion-form>
|
||||||
<form>
|
<form>
|
||||||
<lion-input
|
<lion-fieldset name="full_name">
|
||||||
name="first_name"
|
<lion-input
|
||||||
label="First Name"
|
name="first_name"
|
||||||
.validators="${[new Required()]}"
|
label="First Name"
|
||||||
></lion-input>
|
.validators="${[new Required()]}"
|
||||||
<lion-input
|
></lion-input>
|
||||||
name="last_name"
|
<lion-input
|
||||||
label="Last Name"
|
name="last_name"
|
||||||
.validators="${[new Required()]}"
|
label="Last Name"
|
||||||
></lion-input>
|
.validators="${[new Required()]}"
|
||||||
|
></lion-input>
|
||||||
|
</lion-fieldset>
|
||||||
<lion-input-date
|
<lion-input-date
|
||||||
name="date"
|
name="date"
|
||||||
label="Date of application"
|
label="Date of application"
|
||||||
.modelValue="${new Date('2000-12-12')}"
|
.modelValue="${new Date('2000-12-12')}"
|
||||||
.validators="${[new Required()]}"
|
.validators="${[new Required()]}"
|
||||||
></lion-input-date>
|
></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
|
<lion-textarea
|
||||||
name="bio"
|
name="bio"
|
||||||
label="Biography"
|
label="Biography"
|
||||||
|
|
@ -58,7 +69,7 @@ For usage and installation please see the appropriate packages.
|
||||||
<lion-input-email name="email" label="Email"></lion-input-email>
|
<lion-input-email name="email" label="Email"></lion-input-email>
|
||||||
<lion-checkbox-group
|
<lion-checkbox-group
|
||||||
label="What do you like?"
|
label="What do you like?"
|
||||||
name="checkers[]"
|
name="checkers"
|
||||||
.validators="${[new Required()]}"
|
.validators="${[new Required()]}"
|
||||||
>
|
>
|
||||||
<lion-checkbox .choiceValue=${'foo'} label="I like foo"></lion-checkbox>
|
<lion-checkbox .choiceValue=${'foo'} label="I like foo"></lion-checkbox>
|
||||||
|
|
@ -101,7 +112,7 @@ For usage and installation please see the appropriate packages.
|
||||||
step="0.1"
|
step="0.1"
|
||||||
label="Input range"
|
label="Input range"
|
||||||
></lion-input-range>
|
></lion-input-range>
|
||||||
<lion-checkbox-group name="terms" .validators="${[new Required()]}">
|
<lion-checkbox-group .mulipleChoice="${false}" name="terms" .validators="${[new Required()]}">
|
||||||
<lion-checkbox
|
<lion-checkbox
|
||||||
label="I blindly accept all terms and conditions"
|
label="I blindly accept all terms and conditions"
|
||||||
></lion-checkbox>
|
></lion-checkbox>
|
||||||
|
|
@ -118,7 +129,7 @@ For usage and installation please see the appropriate packages.
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</lion-form>
|
</lion-form>
|
||||||
`}
|
`;}}
|
||||||
</Story>
|
</Story>
|
||||||
</Preview>
|
</Preview>
|
||||||
|
|
||||||
|
|
@ -129,6 +140,7 @@ import '@lion/fieldset/lion-fieldset.js';
|
||||||
import '@lion/form/lion-form.js';
|
import '@lion/form/lion-form.js';
|
||||||
import '@lion/input-amount/lion-input-amount.js';
|
import '@lion/input-amount/lion-input-amount.js';
|
||||||
import '@lion/input-date/lion-input-date.js';
|
import '@lion/input-date/lion-input-date.js';
|
||||||
|
import '@lion/input-datepicker/lion-input-datepicker.js';
|
||||||
import '@lion/input-email/lion-input-email.js';
|
import '@lion/input-email/lion-input-email.js';
|
||||||
import '@lion/input-iban/lion-input-iban.js';
|
import '@lion/input-iban/lion-input-iban.js';
|
||||||
import '@lion/input-range/lion-input-range.js';
|
import '@lion/input-range/lion-input-range.js';
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,10 @@ import {
|
||||||
|
|
||||||
## Required Validator
|
## Required Validator
|
||||||
|
|
||||||
The required validator can be put onto every form field element and will make sure that element is not empty.
|
The required validator can be put onto every form field element and will make sure that element is
|
||||||
For an input that may mean that it is not an empty string while for a checkbox group it means at least one checkbox needs to be checked.
|
not empty.
|
||||||
|
For an input that may mean that it is not an empty string,
|
||||||
|
while for a checkbox group it means at least one checkbox needs to be checked.
|
||||||
|
|
||||||
<Story name="Required Validator">
|
<Story name="Required Validator">
|
||||||
{html`
|
{html`
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ it has a tabindex=“0” applied.
|
||||||
|
|
||||||
Now we want to integrate the slider in our form framework to enrich the user interface, get
|
Now we want to integrate the slider in our form framework to enrich the user interface, get
|
||||||
validation support and get all the other [benefits of LionField](/?path=/docs/forms-system-overview--page).
|
validation support and get all the other [benefits of LionField](/?path=/docs/forms-system-overview--page).
|
||||||
We start of by creating a component `<lion-slider>` that extends from `LionField`.
|
We start by creating a component `<lion-slider>` that extends from `LionField`.
|
||||||
Then we follow the steps below:
|
Then we follow the steps below:
|
||||||
|
|
||||||
#### 1. Add your interaction element as ‘input slot'
|
#### 1. Add your interaction element as ‘input slot'
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ Alternatively you can also let the fieldset validator be dependent on the error
|
||||||
|
|
||||||
Simply loop over the formElements inside your Validator's `execute` method:
|
Simply loop over the formElements inside your Validator's `execute` method:
|
||||||
```js
|
```js
|
||||||
this.formElementsArray.some(el => el.hasFeedbackFor.includes('error'));
|
this.formElements.some(el => el.hasFeedbackFor.includes('error'));
|
||||||
```
|
```
|
||||||
|
|
||||||
### Validating multiple fieldsets
|
### Validating multiple fieldsets
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
import { Story, Meta, html, Preview } from '@open-wc/demoing-storybook';
|
||||||
import { Required, MaxLength, loadDefaultFeedbackMessages } from '@lion/validate';
|
import { Required, MaxLength, loadDefaultFeedbackMessages } from '@lion/validate';
|
||||||
import '@lion/fieldset/lion-fieldset.js';
|
import '@lion/fieldset/lion-fieldset.js';
|
||||||
import '@lion/input/lion-input.js';
|
import '@lion/input/lion-input.js';
|
||||||
|
|
@ -10,6 +10,7 @@ import '@lion/form/lion-form.js';
|
||||||
|
|
||||||
A form can have multiple nested fieldsets.
|
A form can have multiple nested fieldsets.
|
||||||
|
|
||||||
|
<Preview>
|
||||||
<Story name="Default">
|
<Story name="Default">
|
||||||
{html`
|
{html`
|
||||||
<lion-form id="form">
|
<lion-form id="form">
|
||||||
|
|
@ -25,48 +26,38 @@ A form can have multiple nested fieldsets.
|
||||||
</lion-fieldset>
|
</lion-fieldset>
|
||||||
<lion-input name="birthdate" label="Birthdate" .modelValue=${'23-04-1991'}></lion-input>
|
<lion-input name="birthdate" label="Birthdate" .modelValue=${'23-04-1991'}></lion-input>
|
||||||
</lion-fieldset>
|
</lion-fieldset>
|
||||||
<button type="button" @click=${() => console.log(document.querySelector('#form').serializeGroup())}>
|
<button type="button" @click=${() => console.log(document.querySelector('#form').serializedValue)}>
|
||||||
Log to Action Logger
|
Log to Action Logger
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</lion-form>
|
</lion-form>
|
||||||
`}
|
`}
|
||||||
</Story>
|
</Story>
|
||||||
|
</Preview>
|
||||||
```html
|
|
||||||
<lion-form id="form">
|
|
||||||
<form>
|
|
||||||
<lion-fieldset label="Personal data" name="personalData">
|
|
||||||
<lion-fieldset label="Full Name" name="fullName">
|
|
||||||
<lion-input name="firstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
|
||||||
<lion-input name="lastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
|
||||||
</lion-fieldset>
|
|
||||||
<lion-fieldset label="Location" name="location">
|
|
||||||
<lion-input name="country" label="Country" .modelValue=${'Netherlands'}></lion-input>
|
|
||||||
<lion-input name="city" label="City" .modelValue=${'Amsterdam'}></lion-input>
|
|
||||||
</lion-fieldset>
|
|
||||||
<lion-input name="birthdate" label="Birthdate" .modelValue=${'23-04-1991'}></lion-input>
|
|
||||||
</lion-fieldset>
|
|
||||||
<button type="button" @click=${() => console.log(document.querySelector('#form').serializeGroup())}>
|
|
||||||
Log to Action Logger
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</lion-form>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Form Submit / Reset
|
## Form Submit / Reset
|
||||||
|
|
||||||
You can control whether a form gets submitted based on validation states.
|
You can control whether a form gets submitted based on validation states.
|
||||||
Same thing goes for resetting the inputs to the original state.
|
Same thing goes for resetting the inputs to the original state.
|
||||||
|
|
||||||
|
Be sure to call `serializedValue` when a you want to submit a form.
|
||||||
|
This value automatically filters out disabled children and makes sure the values
|
||||||
|
that are retrieved can be transmitted over a wire. (for instance, an input-date with a modelValue
|
||||||
|
of type `Date` will be serialized to an iso formatted string).
|
||||||
|
|
||||||
|
|
||||||
|
> Note: Offering a reset button is a bad practice in terms of accessibility.
|
||||||
|
This button is only used here to demonstrate the `serializeGroup()` method.
|
||||||
|
|
||||||
|
<Preview>
|
||||||
<Story name="Submit/reset">
|
<Story name="Submit/reset">
|
||||||
{() => {
|
{() => {
|
||||||
loadDefaultFeedbackMessages();
|
loadDefaultFeedbackMessages();
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
const form = document.querySelector('#form2');
|
const form = document.querySelector('#form2');
|
||||||
if (!form.hasFeedbackFor.includes('error')) {
|
if (!form.hasFeedbackFor.includes('error')) {
|
||||||
console.log(form.serializeGroup());
|
document.getElementById('form2_output').innerText = JSON.stringify(form.serializedValue);
|
||||||
form.resetGroup();
|
document.querySelector('#form2').resetGroup();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return html`
|
return html`
|
||||||
|
|
@ -85,58 +76,65 @@ Same thing goes for resetting the inputs to the original state.
|
||||||
.validators=${[new Required(), new MaxLength(15)]}
|
.validators=${[new Required(), new MaxLength(15)]}
|
||||||
>
|
>
|
||||||
</lion-input>
|
</lion-input>
|
||||||
|
<lion-input
|
||||||
|
name="address"
|
||||||
|
disabled
|
||||||
|
label="Address"
|
||||||
|
.validators=${[new MaxLength(15)]}
|
||||||
|
>
|
||||||
|
</lion-input>
|
||||||
</lion-fieldset>
|
</lion-fieldset>
|
||||||
<button type="submit">Submit & Reset (if successfully submitted)</button>
|
<button type="submit">Submit & Reset (if successfully submitted)</button>
|
||||||
<button type="button" @click=${() => document.querySelector('#form2').resetGroup()}>
|
<button type="button" @click=${() => {
|
||||||
Reset
|
document.querySelector('#form2').resetGroup();
|
||||||
|
const form = document.querySelector('#form2');
|
||||||
|
document.getElementById('form2_output').innerText = JSON.stringify(form.serializedValue);
|
||||||
|
}}>
|
||||||
|
reset
|
||||||
</button>
|
</button>
|
||||||
<p>
|
<pre id="form2_output">
|
||||||
A reset button should never be offered to users. This button is only used here to
|
</pre>
|
||||||
demonstrate the functionality.
|
|
||||||
</p>
|
|
||||||
</form></lion-form
|
</form></lion-form
|
||||||
>
|
>
|
||||||
`;
|
`;
|
||||||
}}
|
}}
|
||||||
</Story>
|
</Story>
|
||||||
|
</Preview>
|
||||||
|
|
||||||
```js
|
|
||||||
import { Required, MaxLength } from '@lion/validate'
|
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
const form = document.querySelector('#form2');
|
|
||||||
if (!form.hasFeedbackFor.includes('error')) {
|
|
||||||
console.log(form.serializeGroup());
|
|
||||||
form.resetGroup();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
```html
|
## Serialize in a multistep form
|
||||||
<lion-form id="form2" @submit="${submit}">
|
|
||||||
<form>
|
In a multistep form (consisting of multiple forms) it might be handy to wrap the serialized output
|
||||||
<lion-fieldset label="Name" name="name">
|
with the name of the form.
|
||||||
<lion-input
|
|
||||||
name="firstName"
|
<Preview>
|
||||||
label="First Name"
|
<Story name="Multistep">
|
||||||
.validators=${[new Required(), new MaxLength(15)]}
|
{() => {
|
||||||
>
|
loadDefaultFeedbackMessages();
|
||||||
</lion-input>
|
const serializeWithName = (formId, outputId) => {
|
||||||
<lion-input
|
const form = document.getElementById(formId);
|
||||||
name="lastName"
|
if (!form.hasFeedbackFor.includes('error')) {
|
||||||
label="Last Name"
|
const output = { [form.name]: form.serializedValue };
|
||||||
.validators=${[new Required(), new MaxLength(15)]}
|
document.getElementById(outputId).innerText = JSON.stringify(output);
|
||||||
>
|
}
|
||||||
</lion-input>
|
};
|
||||||
</lion-fieldset>
|
return html`
|
||||||
<button type="submit">Submit & Reset (if successfully submitted)</button>
|
<lion-form name="step1FormName" id="form3"><form>
|
||||||
<button type="button" @click=${() => console.log(document.querySelector('#form2'))}>
|
<lion-input name="step1InputName" label="Step 1 Input"></lion-input>
|
||||||
Reset
|
<button @click="${() => serializeWithName('form3', 'form3_output')}">
|
||||||
</button>
|
serialize step 1 with name
|
||||||
<p>
|
</button>
|
||||||
A reset button should never be offered to users. This button is only used here to
|
<pre id="form3_output"></pre>
|
||||||
demonstrate the functionality.
|
</form></lion-form>
|
||||||
</p>
|
<lion-form name="step2FormName" id="form4"><form>
|
||||||
</form>
|
<lion-input name="step2InputName" label="Step 2 Input"></lion-input>
|
||||||
</lion-form>
|
<button @click="${() => serializeWithName('form4', 'form4_output')}">
|
||||||
```
|
serialize step 2 with name
|
||||||
|
</button>
|
||||||
|
<pre id="form4_output"></pre>
|
||||||
|
</form></lion-form>
|
||||||
|
`;
|
||||||
|
}}
|
||||||
|
</Story>
|
||||||
|
</Preview>
|
||||||
|
|
|
||||||
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('".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,7 +1,6 @@
|
||||||
import { css } from '@lion/core';
|
import { css } from '@lion/core';
|
||||||
import { LocalizeMixin, getCurrencyName } from '@lion/localize';
|
import { LocalizeMixin, getCurrencyName } from '@lion/localize';
|
||||||
import { LionInput } from '@lion/input';
|
import { LionInput } from '@lion/input';
|
||||||
import { FieldCustomMixin } from '@lion/field';
|
|
||||||
import { IsNumber } from '@lion/validate';
|
import { IsNumber } from '@lion/validate';
|
||||||
import { parseAmount } from './parsers.js';
|
import { parseAmount } from './parsers.js';
|
||||||
import { formatAmount } from './formatters.js';
|
import { formatAmount } from './formatters.js';
|
||||||
|
|
@ -12,22 +11,26 @@ import { formatAmount } from './formatters.js';
|
||||||
* @customElement lion-input-amount
|
* @customElement lion-input-amount
|
||||||
* @extends {LionInput}
|
* @extends {LionInput}
|
||||||
*/
|
*/
|
||||||
export class LionInputAmount extends FieldCustomMixin(LocalizeMixin(LionInput)) {
|
export class LionInputAmount extends LocalizeMixin(LionInput) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
currency: {
|
/**
|
||||||
type: String,
|
* @desc an iso code like 'EUR' or 'USD' that will be displayed next to the input
|
||||||
},
|
* and from which an accessible label (like 'euros') is computed for screen
|
||||||
|
* reader users
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
currency: String,
|
||||||
|
/**
|
||||||
|
* @desc the modelValue of the input-amount has the 'Number' type. This allows
|
||||||
|
* Application Developers to easily read from and write to this input or write custom
|
||||||
|
* validators.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
modelValue: Number,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
updated(changedProps) {
|
|
||||||
super.updated(changedProps);
|
|
||||||
if (changedProps.has('currency')) {
|
|
||||||
this._onCurrencyChanged({ currency: this.currency });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -45,6 +48,21 @@ export class LionInputAmount extends FieldCustomMixin(LocalizeMixin(LionInput))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get _currencyDisplayNode() {
|
||||||
|
return Array.from(this.children).find(child => child.slot === 'after');
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
...super.styles,
|
||||||
|
css`
|
||||||
|
.input-group__container > .input-group__input ::slotted(.form-control) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.parser = parseAmount;
|
this.parser = parseAmount;
|
||||||
|
|
@ -59,18 +77,6 @@ export class LionInputAmount extends FieldCustomMixin(LocalizeMixin(LionInput))
|
||||||
this.defaultValidators.push(new IsNumber());
|
this.defaultValidators.push(new IsNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
__callParser(value = this.formattedValue) {
|
|
||||||
// TODO: input and change events both trigger parsing therefore we need to handle the second parse
|
|
||||||
this.__parserCallcountSincePaste += 1;
|
|
||||||
this.__isPasting = this.__parserCallcountSincePaste === 2;
|
|
||||||
this.formatOptions.mode = this.__isPasting === true ? 'pasted' : 'auto';
|
|
||||||
return super.__callParser(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
_reflectBackOn() {
|
|
||||||
return super._reflectBackOn() || this.__isPasting;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
// eslint-disable-next-line wc/guard-super-call
|
// eslint-disable-next-line wc/guard-super-call
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
@ -81,12 +87,29 @@ export class LionInputAmount extends FieldCustomMixin(LocalizeMixin(LionInput))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
__setCurrencyDisplayLabel() {
|
updated(changedProps) {
|
||||||
this._currencyDisplayNode.setAttribute('aria-label', getCurrencyName(this.currency));
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has('currency')) {
|
||||||
|
this._onCurrencyChanged({ currency: this.currency });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get _currencyDisplayNode() {
|
/**
|
||||||
return Array.from(this.children).find(child => child.slot === 'after');
|
* @override of FormatMixin
|
||||||
|
*/
|
||||||
|
__callParser(value = this.formattedValue) {
|
||||||
|
// TODO: input and change events both trigger parsing therefore we need to handle the second parse
|
||||||
|
this.__parserCallcountSincePaste += 1;
|
||||||
|
this.__isPasting = this.__parserCallcountSincePaste === 2;
|
||||||
|
this.formatOptions.mode = this.__isPasting === true ? 'pasted' : 'auto';
|
||||||
|
return super.__callParser(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override of FormatMixin
|
||||||
|
*/
|
||||||
|
_reflectBackOn() {
|
||||||
|
return super._reflectBackOn() || this.__isPasting;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCurrencyChanged({ currency }) {
|
_onCurrencyChanged({ currency }) {
|
||||||
|
|
@ -98,14 +121,10 @@ export class LionInputAmount extends FieldCustomMixin(LocalizeMixin(LionInput))
|
||||||
this.__setCurrencyDisplayLabel();
|
this.__setCurrencyDisplayLabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles() {
|
__setCurrencyDisplayLabel() {
|
||||||
return [
|
// TODO: for optimal a11y, abbreviations should be part of aria-label
|
||||||
...super.styles,
|
// example, for a language switch with text 'en', an aria-label of 'english' is not
|
||||||
css`
|
// sufficient, it should also contain the abbreviation.
|
||||||
.input-group__container > .input-group__input ::slotted(.form-control) {
|
this._currencyDisplayNode.setAttribute('aria-label', getCurrencyName(this.currency));
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { LocalizeMixin, formatDate, parseDate } from '@lion/localize';
|
import { LocalizeMixin, formatDate, parseDate } from '@lion/localize';
|
||||||
import { FieldCustomMixin } from '@lion/field';
|
|
||||||
import { LionInput } from '@lion/input';
|
import { LionInput } from '@lion/input';
|
||||||
import { IsDate } from '@lion/validate';
|
import { IsDate } from '@lion/validate';
|
||||||
|
|
||||||
|
|
@ -10,7 +9,7 @@ import { IsDate } from '@lion/validate';
|
||||||
* @customElement lion-input-date
|
* @customElement lion-input-date
|
||||||
* @extends {LionInput}
|
* @extends {LionInput}
|
||||||
*/
|
*/
|
||||||
export class LionInputDate extends FieldCustomMixin(LocalizeMixin(LionInput)) {
|
export class LionInputDate extends LocalizeMixin(LionInput) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
modelValue: Date,
|
modelValue: Date,
|
||||||
|
|
@ -36,4 +35,17 @@ export class LionInputDate extends FieldCustomMixin(LocalizeMixin(LionInput)) {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.type = 'text';
|
this.type = 'text';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
serializer(modelValue) {
|
||||||
|
if (!(modelValue instanceof Date)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return modelValue.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
deserializer(serializedValue) {
|
||||||
|
return new Date(serializedValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { runFormatMixinSuite } from '@lion/field/test-suites/FormatMixin.suite.j
|
||||||
import '../lion-input-date.js';
|
import '../lion-input-date.js';
|
||||||
|
|
||||||
const tagString = 'lion-input-date';
|
const tagString = 'lion-input-date';
|
||||||
|
|
||||||
describe('<lion-input-date> integrations', () => {
|
describe('<lion-input-date> integrations', () => {
|
||||||
runInteractionStateMixinSuite({
|
runInteractionStateMixinSuite({
|
||||||
tagString,
|
tagString,
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,29 @@ describe('<lion-input-date>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns undefined when value is empty string', async () => {
|
it('returns undefined when value is empty string', async () => {
|
||||||
const el = await fixture(`<lion-input-date></lion-input-date>`);
|
const el = await fixture(
|
||||||
|
html`
|
||||||
|
<lion-input-date></lion-input-date>
|
||||||
|
`,
|
||||||
|
);
|
||||||
expect(el.parser('')).to.equal(undefined);
|
expect(el.parser('')).to.equal(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has type="text" to activate default keyboard on mobile with all necessary symbols', async () => {
|
it('has type="text" to activate default keyboard on mobile with all necessary symbols', async () => {
|
||||||
const el = await fixture(`<lion-input-date></lion-input-date>`);
|
const el = await fixture(
|
||||||
|
html`
|
||||||
|
<lion-input-date></lion-input-date>
|
||||||
|
`,
|
||||||
|
);
|
||||||
expect(el._inputNode.type).to.equal('text');
|
expect(el._inputNode.type).to.equal('text');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has validator "isDate" applied by default', async () => {
|
it('has validator "isDate" applied by default', async () => {
|
||||||
const el = await fixture(`<lion-input-date></lion-input-date>`);
|
const el = await fixture(
|
||||||
|
html`
|
||||||
|
<lion-input-date></lion-input-date>
|
||||||
|
`,
|
||||||
|
);
|
||||||
el.modelValue = '2005/11/10';
|
el.modelValue = '2005/11/10';
|
||||||
expect(el.hasFeedbackFor).to.include('error');
|
expect(el.hasFeedbackFor).to.include('error');
|
||||||
expect(el.validationStates).to.have.a.property('error');
|
expect(el.validationStates).to.have.a.property('error');
|
||||||
|
|
@ -99,24 +111,39 @@ describe('<lion-input-date>', () => {
|
||||||
|
|
||||||
it('is accessible', async () => {
|
it('is accessible', async () => {
|
||||||
const el = await fixture(
|
const el = await fixture(
|
||||||
`<lion-input-date><label slot="label">Label</label></lion-input-date>`,
|
html`
|
||||||
|
<lion-input-date><label slot="label">Label</label></lion-input-date>
|
||||||
|
`,
|
||||||
);
|
);
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is accessible when readonly', async () => {
|
it('is accessible when readonly', async () => {
|
||||||
const el = await fixture(
|
const el = await fixture(
|
||||||
`<lion-input-date readonly .modelValue=${new Date(
|
html`
|
||||||
'2017/06/15',
|
<lion-input-date readonly .modelValue=${new Date('2017/06/15')}
|
||||||
)}><label slot="label">Label</label></lion-input-date>`,
|
><label slot="label">Label</label></lion-input-date
|
||||||
|
>
|
||||||
|
`,
|
||||||
);
|
);
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is accessible when disabled', async () => {
|
it('is accessible when disabled', async () => {
|
||||||
const el = await fixture(
|
const el = await fixture(
|
||||||
`<lion-input-date disabled><label slot="label">Label</label></lion-input-date>`,
|
html`
|
||||||
|
<lion-input-date disabled><label slot="label">Label</label></lion-input-date>
|
||||||
|
`,
|
||||||
);
|
);
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('serializes to iso format', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html`
|
||||||
|
<lion-input-date .modelValue="${new Date('2000-12-12')}"></lion-input-date>
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
expect(el.serializedValue).to.equal('2000-12-12');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { LocalizeMixin } from '@lion/localize';
|
import { LocalizeMixin } from '@lion/localize';
|
||||||
import { FieldCustomMixin } from '@lion/field';
|
|
||||||
import { LionInput } from '@lion/input';
|
import { LionInput } from '@lion/input';
|
||||||
import { IsEmail } from '@lion/validate';
|
import { IsEmail } from '@lion/validate';
|
||||||
|
|
||||||
|
|
@ -9,7 +8,7 @@ import { IsEmail } from '@lion/validate';
|
||||||
* @customElement lion-input-email
|
* @customElement lion-input-email
|
||||||
* @extends {LionInput}
|
* @extends {LionInput}
|
||||||
*/
|
*/
|
||||||
export class LionInputEmail extends FieldCustomMixin(LocalizeMixin(LionInput)) {
|
export class LionInputEmail extends LocalizeMixin(LionInput) {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
// local-part@domain where the local part may be up to 64 characters long
|
// local-part@domain where the local part may be up to 64 characters long
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { LocalizeMixin } from '@lion/localize';
|
import { LocalizeMixin } from '@lion/localize';
|
||||||
import { LionInput } from '@lion/input';
|
import { LionInput } from '@lion/input';
|
||||||
import { FieldCustomMixin } from '@lion/field';
|
|
||||||
import { formatIBAN } from './formatters.js';
|
import { formatIBAN } from './formatters.js';
|
||||||
import { parseIBAN } from './parsers.js';
|
import { parseIBAN } from './parsers.js';
|
||||||
import { IsIBAN } from './validators.js';
|
import { IsIBAN } from './validators.js';
|
||||||
|
|
@ -10,7 +9,7 @@ import { IsIBAN } from './validators.js';
|
||||||
*
|
*
|
||||||
* @extends {LionInput}
|
* @extends {LionInput}
|
||||||
*/
|
*/
|
||||||
export class LionInputIban extends FieldCustomMixin(LocalizeMixin(LionInput)) {
|
export class LionInputIban extends LocalizeMixin(LionInput) {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.formatter = formatIBAN;
|
this.formatter = formatIBAN;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable import/no-extraneous-dependencies */
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
import { LocalizeMixin, formatNumber } from '@lion/localize';
|
import { LocalizeMixin, formatNumber } from '@lion/localize';
|
||||||
import { FieldCustomMixin } from '@lion/field';
|
|
||||||
import { LionInput } from '@lion/input';
|
import { LionInput } from '@lion/input';
|
||||||
import { html, css, unsafeCSS } from '@lion/core';
|
import { html, css, unsafeCSS } from '@lion/core';
|
||||||
|
|
||||||
|
|
@ -10,7 +9,7 @@ import { html, css, unsafeCSS } from '@lion/core';
|
||||||
* @customElement `lion-input-range`
|
* @customElement `lion-input-range`
|
||||||
* @extends LionInput
|
* @extends LionInput
|
||||||
*/
|
*/
|
||||||
export class LionInputRange extends FieldCustomMixin(LocalizeMixin(LionInput)) {
|
export class LionInputRange extends LocalizeMixin(LionInput) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
min: Number,
|
min: Number,
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,7 @@ export class LionInput extends LionField {
|
||||||
input: () => {
|
input: () => {
|
||||||
// TODO: Find a better way to do value delegation via attr
|
// TODO: Find a better way to do value delegation via attr
|
||||||
const native = document.createElement('input');
|
const native = document.createElement('input');
|
||||||
if (this.__dataInstanceProps && this.__dataInstanceProps.modelValue) {
|
if (this.hasAttribute('value')) {
|
||||||
native.setAttribute('value', this.__dataInstanceProps.modelValue);
|
|
||||||
} else if (this.hasAttribute('value')) {
|
|
||||||
native.setAttribute('value', this.getAttribute('value'));
|
native.setAttribute('value', this.getAttribute('value'));
|
||||||
}
|
}
|
||||||
return native;
|
return native;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { expect, fixture } from '@open-wc/testing';
|
import { expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||||
|
|
||||||
import '../lion-input.js';
|
import '../lion-input.js';
|
||||||
|
|
||||||
|
const tagString = 'lion-input';
|
||||||
|
const tag = unsafeStatic(tagString);
|
||||||
|
|
||||||
describe('<lion-input>', () => {
|
describe('<lion-input>', () => {
|
||||||
it('delegates readOnly property and readonly attribute', async () => {
|
it('delegates readOnly property and readonly attribute', async () => {
|
||||||
const el = await fixture(
|
const el = await fixture(html`<${tag} readonly></${tag}>`);
|
||||||
`<lion-input readonly><label slot="label">Testing readonly</label></lion-input>`,
|
|
||||||
);
|
|
||||||
expect(el._inputNode.readOnly).to.equal(true);
|
expect(el._inputNode.readOnly).to.equal(true);
|
||||||
el.readOnly = false;
|
el.readOnly = false;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -14,13 +15,20 @@ describe('<lion-input>', () => {
|
||||||
expect(el._inputNode.readOnly).to.equal(false);
|
expect(el._inputNode.readOnly).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('delegates value attribute', async () => {
|
||||||
|
const el = await fixture(html`<${tag} value="prefilled"></${tag}>`);
|
||||||
|
expect(el._inputNode.value).to.equal('prefilled');
|
||||||
|
});
|
||||||
|
|
||||||
it('automatically creates an <input> element if not provided by user', async () => {
|
it('automatically creates an <input> element if not provided by user', async () => {
|
||||||
const el = await fixture(`<lion-input></lion-input>`);
|
const el = await fixture(html`
|
||||||
|
<${tag}></${tag}>
|
||||||
|
`);
|
||||||
expect(el.querySelector('input')).to.equal(el._inputNode);
|
expect(el.querySelector('input')).to.equal(el._inputNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a type which is reflected to an attribute and is synced down to the native input', async () => {
|
it('has a type which is reflected to an attribute and is synced down to the native input', async () => {
|
||||||
const el = await fixture(`<lion-input></lion-input>`);
|
const el = await fixture(html`<${tag}></${tag}>`);
|
||||||
expect(el.type).to.equal('text');
|
expect(el.type).to.equal('text');
|
||||||
expect(el.getAttribute('type')).to.equal('text');
|
expect(el.getAttribute('type')).to.equal('text');
|
||||||
expect(el._inputNode.getAttribute('type')).to.equal('text');
|
expect(el._inputNode.getAttribute('type')).to.equal('text');
|
||||||
|
|
@ -32,7 +40,7 @@ describe('<lion-input>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has an attribute that can be used to set the placeholder text of the input', async () => {
|
it('has an attribute that can be used to set the placeholder text of the input', async () => {
|
||||||
const el = await fixture(`<lion-input placeholder="text"></lion-input>`);
|
const el = await fixture(html`<${tag} placeholder="text"></${tag}>`);
|
||||||
expect(el.getAttribute('placeholder')).to.equal('text');
|
expect(el.getAttribute('placeholder')).to.equal('text');
|
||||||
expect(el._inputNode.getAttribute('placeholder')).to.equal('text');
|
expect(el._inputNode.getAttribute('placeholder')).to.equal('text');
|
||||||
|
|
||||||
|
|
@ -42,20 +50,20 @@ describe('<lion-input>', () => {
|
||||||
expect(el._inputNode.getAttribute('placeholder')).to.equal('foo');
|
expect(el._inputNode.getAttribute('placeholder')).to.equal('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is accessible', async () => {
|
describe('Accessibility', () => {
|
||||||
const el = await fixture(`<lion-input><label slot="label">Label</label></lion-input>`);
|
it('is accessible', async () => {
|
||||||
await expect(el).to.be.accessible();
|
const el = await fixture(html`<${tag} label="Label"></${tag}>`);
|
||||||
});
|
await expect(el).to.be.accessible();
|
||||||
|
});
|
||||||
|
|
||||||
it('is accessible when readonly', async () => {
|
it('is accessible when readonly', async () => {
|
||||||
const el = await fixture(
|
const el = await fixture(html`<${tag} readonly label="Label"></${tag}>`);
|
||||||
`<lion-input readonly .modelValue=${'read only'}><label slot="label">Label</label></lion-input>`,
|
await expect(el).to.be.accessible();
|
||||||
);
|
});
|
||||||
await expect(el).to.be.accessible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is accessible when disabled', async () => {
|
it('is accessible when disabled', async () => {
|
||||||
const el = await fixture(`<lion-input disabled><label slot="label">Label</label></lion-input>`);
|
const el = await fixture(html`<${tag} disabled label="Label"></${tag}>`);
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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