feat: flatten modelValue and remove checkedValue

Co-authored-by: daKmoR <Thomas.Allmer@ing.com>
Co-authored-by: CubLion <Alex.Ghiu@ing.com>
This commit is contained in:
Joren Broekema 2020-01-16 10:14:35 +01:00 committed by CubLion
parent 3995eb8397
commit 848ff06887
30 changed files with 2436 additions and 2247 deletions

View file

@ -79,7 +79,7 @@
},
{
"path": "./bundlesize/dist/all/all-*.js",
"maxSize": "30 kB"
"maxSize": "40 kB"
}
]
}

View file

@ -27,11 +27,11 @@ import '@lion/checkbox-group/lion-checkbox-group.js';
```html
<lion-checkbox-group
name="scientistsGroup"
name="scientists[]"
label="Favorite scientists"
>
<lion-checkbox name="scientists[]" label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
<lion-checkbox name="scientists[]" label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
<lion-checkbox name="scientists[]" label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
<lion-checkbox label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
</lion-checkbox-group>
```

View file

@ -32,6 +32,7 @@
"*.js"
],
"dependencies": {
"@lion/choice-input": "0.5.9",
"@lion/core": "0.4.3",
"@lion/fieldset": "0.6.9"
},

View file

@ -1,17 +1,9 @@
import { ChoiceGroupMixin } from '@lion/choice-input';
import { LionFieldset } from '@lion/fieldset';
export class LionCheckboxGroup extends LionFieldset {
// eslint-disable-next-line class-methods-use-this
_isEmpty(modelValues) {
const keys = Object.keys(modelValues);
for (let i = 0; i < keys.length; i += 1) {
const modelValue = modelValues[keys[i]];
if (Array.isArray(modelValue)) {
// grouped via myName[]
return !modelValue.some(node => node.checked);
}
return !modelValue.checked;
}
return true;
export class LionCheckboxGroup extends ChoiceGroupMixin(LionFieldset) {
constructor() {
super();
this.multipleChoice = true;
}
}

View file

@ -16,24 +16,24 @@ Its purpose is to provide a way for users to check **multiple** options amongst
<Story name="Default">
{html`
<lion-checkbox-group
name="scientistsGroup"
name="scientists[]"
label="Favorite scientists"
>
<lion-checkbox name="scientists[]" label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
<lion-checkbox name="scientists[]" label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
<lion-checkbox name="scientists[]" label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
<lion-checkbox label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
</lion-checkbox-group>
`}
</Story>
```html
<lion-checkbox-group
name="scientistsGroup"
name="scientists[]"
label="Favourite scientists"
>
<lion-checkbox name="scientists[]" label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
<lion-checkbox name="scientists[]" label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
<lion-checkbox name="scientists[]" label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
<lion-checkbox label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
</lion-checkbox-group>
```
@ -62,20 +62,17 @@ You can pre-select options by targeting the `modelValue` object of the option an
<Story name="Pre-select">
{html`
<lion-checkbox-group name="scientistsGroup" label="Favorite scientists">
<lion-checkbox-group name="scientists" label="Favorite scientists">
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
checked
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.modelValue=${{ value: 'Marie Curie', checked: true }}
></lion-checkbox>
@ -84,20 +81,17 @@ You can pre-select options by targeting the `modelValue` object of the option an
</Story>
```html
<lion-checkbox-group name="scientistsGroup" label="Favorite scientists">
<lion-checkbox-group name="scientists[]" label="Favorite scientists">
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
checked
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.modelValue=${{ value: 'Marie Curie', checked: true }}
></lion-checkbox>
@ -110,19 +104,16 @@ You can disable the entire group by setting the `disabled` attribute on the `<li
<Story name="Disabled">
{html`
<lion-checkbox-group name="scientistsGroup" label="Favorite scientists" disabled>
<lion-checkbox-group name="scientists[]" label="Favorite scientists" disabled>
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.modelValue=${{ value: 'Marie Curie', checked: true }}
></lion-checkbox>
@ -131,19 +122,16 @@ You can disable the entire group by setting the `disabled` attribute on the `<li
</Story>
```html
<lion-checkbox-group name="scientistsGroup" label="Favorite scientists" disabled>
<lion-checkbox-group name="scientists[]" label="Favorite scientists" disabled>
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.modelValue=${{ value: 'Marie Curie', checked: true }}
></lion-checkbox>
@ -164,23 +152,20 @@ The interaction states of the `<lion-checkbox-group>` are evaluated in order to
};
return html`
<lion-checkbox-group
id="scientistsGroup"
name="scientistsGroup"
id="scientists"
name="scientists[]"
label="Favorite scientists"
.validators=${[new Required()]}
>
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.choiceValue=${'Marie Curie'}
></lion-checkbox>
@ -201,23 +186,20 @@ const validate = () => {
```html
<lion-checkbox-group
id="scientistsGroup"
name="scientistsGroup"
id="scientists"
name="scientists[]"
label="Favorite scientists"
.validators=${[new Required()]}
>
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.choiceValue=${'Marie Curie'}
></lion-checkbox>
@ -239,41 +221,33 @@ Below is a more advanced validator on the group that evaluates the children chec
this.name = 'HasMinTwoChecked';
}
execute(value) {
let hasError = false;
const selectedValues = value['scientists[]'].filter(v => v.checked === true);
if (!(selectedValues.length >= 2)) {
hasError = true;
}
return hasError;
return value.length < 2;
}
static async getMessage() {
return 'You need to select at least 2 values.';
}
}
const validate = () => {
const checkboxGroup = document.querySelector('#scientistsGroup2');
const checkboxGroup = document.querySelector('#scientists2');
checkboxGroup.submitted = !checkboxGroup.submitted;
};
return html`
<lion-checkbox-group
id="scientistsGroup2"
name="scientistsGroup"
id="scientists2"
name="scientists[]"
label="Favorite scientists"
help-text="You should have at least 2 of those"
.validators=${[new Required(), new HasMinTwoChecked()]}
>
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.choiceValue=${'Marie Curie'}
></lion-checkbox>
@ -295,12 +269,7 @@ class HasMinTwoChecked extends Validator {
}
execute(value) {
let hasError = false;
const selectedValues = value['scientists[]'].filter(v => v.checked === true);
if (!(selectedValues.length >= 2)) {
hasError = true;
}
return hasError;
return value.length < 2;
}
static async getMessage() {
@ -309,31 +278,28 @@ class HasMinTwoChecked extends Validator {
}
const validate = () => {
const checkboxGroup = document.querySelector('#scientistsGroup');
const checkboxGroup = document.querySelector('#scientists2');
checkboxGroup.submitted = !checkboxGroup.submitted;
};
```
```html
<lion-checkbox-group
id="scientistsGroup"
name="scientistsGroup"
id="scientists2"
name="scientists[]"
label="Favorite scientists"
help-text="You should have at least 2 of those"
.validators=${[new Required(), new HasMinTwoChecked()]}
>
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.choiceValue=${'Marie Curie'}
></lion-checkbox>

View file

@ -1,9 +1,6 @@
import { expect, html, fixture, nextFrame } from '@open-wc/testing';
import { localizeTearDown } from '@lion/localize/test-helpers.js';
import { Required } from '@lion/validate';
import '@lion/checkbox/lion-checkbox.js';
import { localizeTearDown } from '@lion/localize/test-helpers.js';
import { expect, fixture, html } from '@open-wc/testing';
import '../lion-checkbox-group.js';
beforeEach(() => {
@ -11,36 +8,12 @@ beforeEach(() => {
});
describe('<lion-checkbox-group>', () => {
it('can be required', async () => {
const el = await fixture(html`
<lion-checkbox-group .validators=${[new Required()]}>
<lion-checkbox name="sports[]" .choiceValue=${'running'}></lion-checkbox>
<lion-checkbox name="sports[]" .choiceValue=${'swimming'}></lion-checkbox>
</lion-checkbox-group>
`);
await nextFrame();
expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.validationStates.error.Required).to.be.true;
el.formElements['sports[]'][0].checked = true;
expect(el.hasFeedbackFor).to.deep.equal([]);
});
it('is accessible', async () => {
const el = await fixture(html`
<lion-checkbox-group name="scientistsGroup" label="Who are your favorite scientists?">
<lion-checkbox-group name="scientists[]" label="Who are your favorite scientists?">
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.modelValue=${{ value: 'Marie Curie', checked: false }}
></lion-checkbox>
@ -51,20 +24,14 @@ describe('<lion-checkbox-group>', () => {
it('is accessible when pre-selected', async () => {
const el = await fixture(html`
<lion-checkbox-group name="scientistsGroup" label="Who are your favorite scientists?">
<lion-checkbox-group name="scientists[]" label="Who are your favorite scientists?">
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
.choiceChecked=${true}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.modelValue=${{ value: 'Marie Curie', checked: true }}
></lion-checkbox>
@ -75,23 +42,10 @@ describe('<lion-checkbox-group>', () => {
it('is accessible when disabled', async () => {
const el = await fixture(html`
<lion-checkbox-group
name="scientistsGroup"
label="Who are your favorite scientists?"
disabled
>
<lion-checkbox-group name="scientists[]" label="Who are your favorite scientists?" disabled>
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.modelValue=${{ value: 'Marie Curie', checked: true }}
></lion-checkbox>

View file

@ -1 +1,2 @@
export { ChoiceGroupMixin } from './src/ChoiceGroupMixin.js';
export { ChoiceInputMixin } from './src/ChoiceInputMixin.js';

View file

@ -36,6 +36,7 @@
"@lion/field": "0.8.9"
},
"devDependencies": {
"@lion/fieldset": "0.6.9",
"@lion/input": "0.5.9",
"@lion/validate": "0.6.6",
"@open-wc/demoing-storybook": "^1.8.3",

View file

@ -0,0 +1,167 @@
import { dedupeMixin } from '@lion/core';
import { InteractionStateMixin, FormRegistrarMixin } from '@lion/field';
export const ChoiceGroupMixin = dedupeMixin(
superclass =>
// eslint-disable-next-line
class ChoiceGroupMixin extends FormRegistrarMixin(InteractionStateMixin(superclass)) {
get modelValue() {
const elems = this._getCheckedElements();
if (this.multipleChoice) {
return elems.map(el => el.modelValue.value);
}
return elems ? elems.modelValue.value : '';
}
set modelValue(value) {
this._setCheckedElements(value, (el, val) => el.modelValue.value === val);
}
get serializedValue() {
const elems = this._getCheckedElements();
if (this.multipleChoice) {
return this.modelValue;
}
return elems ? elems.serializedValue : '';
}
set serializedValue(value) {
this._setCheckedElements(value, (el, val) => el.serializedValue === val);
}
constructor() {
super();
this.multipleChoice = false;
}
connectedCallback() {
super.connectedCallback();
if (!this.multipleChoice) {
this.addEventListener('model-value-changed', this._checkSingleChoiceElements);
}
}
disconnectedCallback() {
super.disconnectedCallback();
if (!this.multipleChoice) {
this.removeEventListener('model-value-changed', this._checkSingleChoiceElements);
}
}
/**
* @override from FormRegistrarMixin
*/
addFormElement(child, indexToInsertAt) {
this._throwWhenInvalidChildModelValue(child);
this.__delegateNameAttribute(child);
super.addFormElement(child, indexToInsertAt);
}
/**
* @override from LionFieldset
*/
// eslint-disable-next-line class-methods-use-this
get _childrenCanHaveSameName() {
return true;
}
/**
* @override from LionFieldset
*/
// eslint-disable-next-line class-methods-use-this
get _childNamesCanBeDuplicate() {
return true;
}
_throwWhenInvalidChildModelValue(child) {
if (
typeof child.modelValue.checked !== 'boolean' ||
!Object.prototype.hasOwnProperty.call(child.modelValue, 'value')
) {
throw new Error(
`The ${this.tagName.toLowerCase()} name="${
this.name
}" does not allow to register ${child.tagName.toLowerCase()} with .modelValue="${
child.modelValue
}" - The modelValue should represent an Object { value: "foo", checked: false }`,
);
}
}
_isEmpty() {
const value = this.modelValue;
if (this.multipleChoice) {
return this.modelValue.length === 0;
}
if (typeof value === 'string' && value === '') {
return true;
}
if (value === undefined || value === null) {
return true;
}
return false;
}
_checkSingleChoiceElements(ev) {
const { target } = ev;
if (target.checked === false) return;
const groupName = target.name;
this.formElementsArray
.filter(i => i.name === groupName)
.forEach(choice => {
if (choice !== target) {
choice.checked = false; // eslint-disable-line no-param-reassign
}
});
this.__triggerCheckedValueChanged();
}
_getCheckedElements() {
const filtered = this.formElementsArray.filter(el => el.checked === true);
if (this.multipleChoice) {
return filtered;
}
return filtered.length > 0 ? filtered[0] : undefined;
}
async _setCheckedElements(value, check) {
if (!this.__readyForRegistration) {
await this.registrationReady;
}
for (let i = 0; i < this.formElementsArray.length; i += 1) {
if (this.multipleChoice) {
this.formElementsArray[i].checked = value.includes(this.formElementsArray[i].value);
} else if (check(this.formElementsArray[i], value)) {
// Allows checking against custom values e.g. formattedValue or serializedValue
this.formElementsArray[i].checked = true;
}
}
}
__triggerCheckedValueChanged() {
const value = this.modelValue;
if (value != null && value !== this.__previousCheckedValue) {
this.touched = true;
this.__previousCheckedValue = value;
}
}
__delegateNameAttribute(child) {
if (!child.name || child.name === this.name) {
// eslint-disable-next-line no-param-reassign
child.name = this.name;
} else {
throw new Error(
`The ${this.tagName.toLowerCase()} name="${
this.name
}" does not allow to register ${child.tagName.toLowerCase()} with custom names (name="${
child.name
}" given)`,
);
}
}
},
);

View file

@ -0,0 +1,305 @@
import { html } from '@lion/core';
import { LionFieldset } from '@lion/fieldset';
import { LionInput } from '@lion/input';
import { Required } from '@lion/validate';
import { expect, fixture, nextFrame } from '@open-wc/testing';
import { ChoiceGroupMixin } from '../src/ChoiceGroupMixin.js';
import { ChoiceInputMixin } from '../src/ChoiceInputMixin.js';
describe('ChoiceGroupMixin', () => {
before(() => {
class ChoiceInput extends ChoiceInputMixin(LionInput) {}
customElements.define('choice-group-input', ChoiceInput);
class ChoiceGroup extends ChoiceGroupMixin(LionFieldset) {}
customElements.define('choice-group', ChoiceGroup);
class ChoiceGroupMultiple extends ChoiceGroupMixin(LionFieldset) {
constructor() {
super();
this.multipleChoice = true;
}
}
customElements.define('choice-group-multiple', ChoiceGroupMultiple);
});
it('has a single modelValue representing the currently checked radio value', async () => {
const el = await fixture(html`
<choice-group name="gender">
<choice-group-input .choiceValue=${'male'}></choice-group-input>
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
<choice-group-input .choiceValue=${'other'}></choice-group-input>
</choice-group>
`);
await nextFrame();
expect(el.modelValue).to.equal('female');
el.formElementsArray[0].checked = true;
expect(el.modelValue).to.equal('male');
el.formElementsArray[2].checked = true;
expect(el.modelValue).to.equal('other');
});
it('throws if a child element without a modelValue like { value: "foo", checked: false } tries to register', async () => {
const el = await fixture(html`
<choice-group name="gender">
<choice-group-input .choiceValue=${'male'}></choice-group-input>
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
<choice-group-input .choiceValue=${'other'}></choice-group-input>
</choice-group>
`);
await nextFrame();
const invalidChild = await fixture(html`
<choice-group-input .modelValue=${'Lara'}></choice-group-input>
`);
expect(() => {
el.addFormElement(invalidChild);
}).to.throw(
'The choice-group name="gender" does not allow to register choice-group-input with .modelValue="Lara" - The modelValue should represent an Object { value: "foo", checked: false }',
);
});
it('automatically sets the name property of child radios to its own name', async () => {
const el = await fixture(html`
<choice-group name="gender">
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
<choice-group-input .choiceValue=${'other'}></choice-group-input>
</choice-group>
`);
await nextFrame();
expect(el.formElementsArray[0].name).to.equal('gender');
expect(el.formElementsArray[1].name).to.equal('gender');
const validChild = await fixture(html`
<choice-group-input .choiceValue=${'male'}></choice-group-input>
`);
el.appendChild(validChild);
expect(el.formElementsArray[2].name).to.equal('gender');
});
it('throws if a child element with a different name than the group tries to register', async () => {
const el = await fixture(html`
<choice-group name="gender">
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
<choice-group-input .choiceValue=${'other'}></choice-group-input>
</choice-group>
`);
await nextFrame();
const invalidChild = await fixture(html`
<choice-group-input name="foo" .choiceValue=${'male'}></choice-group-input>
`);
expect(() => {
el.addFormElement(invalidChild);
}).to.throw(
'The choice-group name="gender" does not allow to register choice-group-input with custom names (name="foo" given)',
);
});
it('can set initial modelValue on creation', async () => {
const el = await fixture(html`
<choice-group name="gender" .modelValue=${'other'}>
<choice-group-input .choiceValue=${'male'}></choice-group-input>
<choice-group-input .choiceValue=${'female'}></choice-group-input>
<choice-group-input .choiceValue=${'other'}></choice-group-input>
</choice-group>
`);
await nextFrame();
await el.registrationReady;
await el.updateComplete;
expect(el.modelValue).to.equal('other');
expect(el.formElementsArray[2].checked).to.be.true;
});
it('can handle complex data via choiceValue', async () => {
const date = new Date(2018, 11, 24, 10, 33, 30, 0);
const el = await fixture(html`
<choice-group name="data">
<choice-group-input .choiceValue=${{ some: 'data' }}></choice-group-input>
<choice-group-input .choiceValue=${date} checked></choice-group-input>
</choice-group>
`);
await nextFrame();
expect(el.modelValue).to.equal(date);
el.formElementsArray[0].checked = true;
expect(el.modelValue).to.deep.equal({ some: 'data' });
});
it('can handle 0 and empty string as valid values', async () => {
const el = await fixture(html`
<choice-group name="data">
<choice-group-input .choiceValue=${0} checked></choice-group-input>
<choice-group-input .choiceValue=${''}></choice-group-input>
</choice-group>
`);
await nextFrame();
expect(el.modelValue).to.equal(0);
el.formElementsArray[1].checked = true;
expect(el.modelValue).to.equal('');
});
it('can check a radio by supplying an available modelValue', async () => {
const el = await fixture(html`
<choice-group name="gender">
<choice-group-input .modelValue="${{ value: 'male', checked: false }}"></choice-group-input>
<choice-group-input
.modelValue="${{ value: 'female', checked: true }}"
></choice-group-input>
<choice-group-input
.modelValue="${{ value: 'other', checked: false }}"
></choice-group-input>
</choice-group>
`);
await nextFrame();
expect(el.modelValue).to.equal('female');
el.modelValue = 'other';
expect(el.formElementsArray[2].checked).to.be.true;
});
it('expect child nodes to only fire one model-value-changed event per instance', async () => {
let counter = 0;
const el = await fixture(html`
<choice-group
name="gender"
@model-value-changed=${() => {
counter += 1;
}}
>
<choice-group-input .choiceValue=${'male'}></choice-group-input>
<choice-group-input .modelValue=${{ value: 'female', checked: true }}></choice-group-input>
<choice-group-input .choiceValue=${'other'}></choice-group-input>
</choice-group>
`);
await nextFrame();
counter = 0; // reset after setup which may result in different results
el.formElementsArray[0].checked = true;
expect(counter).to.equal(2); // male becomes checked, female becomes unchecked
// not changed values trigger no event
el.formElementsArray[0].checked = true;
expect(counter).to.equal(2);
el.formElementsArray[2].checked = true;
expect(counter).to.equal(4); // other becomes checked, male becomes unchecked
// not found values trigger no event
el.modelValue = 'foo';
expect(counter).to.equal(4);
el.modelValue = 'male';
expect(counter).to.equal(6); // male becomes checked, other becomes unchecked
});
it('can be required', async () => {
const el = await fixture(html`
<choice-group name="gender" .validators=${[new Required()]}>
<choice-group-input .choiceValue=${'male'}></choice-group-input>
<choice-group-input
.choiceValue=${{ subObject: 'satisfies required' }}
></choice-group-input>
</choice-group>
`);
expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).to.have.a.property('Required');
el.formElementsArray[0].checked = true;
expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('Required');
el.formElementsArray[1].checked = true;
expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('Required');
});
it('returns serialized value', async () => {
const el = await fixture(html`
<choice-group name="gender">
<choice-group-input .choiceValue=${'male'}></choice-group-input>
<choice-group-input .choiceValue=${'female'}></choice-group-input>
</choice-group>
`);
el.formElementsArray[0].checked = true;
expect(el.serializedValue).to.deep.equal({ checked: true, value: 'male' });
});
it('returns serialized value on unchecked state', async () => {
const el = await fixture(html`
<choice-group name="gender">
<choice-group-input .choiceValue=${'male'}></choice-group-input>
<choice-group-input .choiceValue=${'female'}></choice-group-input>
</choice-group>
`);
await nextFrame();
expect(el.serializedValue).to.deep.equal('');
});
describe('multipleChoice', () => {
it('has a single modelValue representing all currently checked values', async () => {
const el = await fixture(html`
<choice-group-multiple name="gender[]">
<choice-group-input .choiceValue=${'male'}></choice-group-input>
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
<choice-group-input .choiceValue=${'other'}></choice-group-input>
</choice-group-multiple>
`);
await nextFrame();
expect(el.modelValue).to.eql(['female']);
el.formElementsArray[0].checked = true;
expect(el.modelValue).to.eql(['male', 'female']);
el.formElementsArray[2].checked = true;
expect(el.modelValue).to.eql(['male', 'female', 'other']);
});
it('can check multiple checkboxes by setting the modelValue', async () => {
const el = await fixture(html`
<lion-checkbox-group name="gender[]">
<lion-checkbox .choiceValue=${'male'}></lion-checkbox>
<lion-checkbox .choiceValue=${'female'}></lion-checkbox>
<lion-checkbox .choiceValue=${'other'}></lion-checkbox>
</lion-checkbox-group>
`);
await nextFrame();
await el.registrationReady;
await el.updateComplete;
el.modelValue = ['male', 'other'];
expect(el.modelValue).to.eql(['male', 'other']);
expect(el.formElementsArray[0].checked).to.be.true;
expect(el.formElementsArray[2].checked).to.be.true;
});
it('unchecks non-matching checkboxes when setting the modelValue', async () => {
const el = await fixture(html`
<lion-checkbox-group name="gender[]">
<lion-checkbox .choiceValue=${'male'} checked></lion-checkbox>
<lion-checkbox .choiceValue=${'female'}></lion-checkbox>
<lion-checkbox .choiceValue=${'other'} checked></lion-checkbox>
</lion-checkbox-group>
`);
await nextFrame();
await el.registrationReady;
await el.updateComplete;
expect(el.modelValue).to.eql(['male', 'other']);
expect(el.formElementsArray[0].checked).to.be.true;
expect(el.formElementsArray[2].checked).to.be.true;
el.modelValue = ['female'];
expect(el.formElementsArray[0].checked).to.be.false;
expect(el.formElementsArray[1].checked).to.be.true;
expect(el.formElementsArray[2].checked).to.be.false;
});
});
});

View file

@ -1,9 +1,8 @@
import { expect, fixture } from '@open-wc/testing';
import { html } from '@lion/core';
import sinon from 'sinon';
import { Required } from '@lion/validate';
import { LionInput } from '@lion/input';
import { Required } from '@lion/validate';
import { expect, fixture } from '@open-wc/testing';
import sinon from 'sinon';
import { ChoiceInputMixin } from '../src/ChoiceInputMixin.js';
describe('ChoiceInputMixin', () => {

View file

@ -1,6 +1,6 @@
import { dedupeMixin } from '@lion/core';
import { formRegistrarManager } from './formRegistrarManager.js';
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
import { formRegistrarManager } from './formRegistrarManager.js';
/**
* This allows an element to become the manager of a register

View file

@ -1,11 +1,10 @@
import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing';
import { LitElement } from '@lion/core';
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
import sinon from 'sinon';
import { FormRegistrarMixin } from '../src/FormRegistrarMixin.js';
import { FormRegisteringMixin } from '../src/FormRegisteringMixin.js';
import { FormRegistrarPortalMixin } from '../src/FormRegistrarPortalMixin.js';
import { formRegistrarManager } from '../src/formRegistrarManager.js';
import { FormRegistrarMixin } from '../src/FormRegistrarMixin.js';
import { FormRegistrarPortalMixin } from '../src/FormRegistrarPortalMixin.js';
export const runRegistrationSuite = customConfig => {
const cfg = {
@ -127,6 +126,25 @@ export const runRegistrationSuite = customConfig => {
expect(el.formElements.length).to.equal(1);
});
it('adds elements to formElements in the right order (DOM)', async () => {
const el = await fixture(html`
<${parentTag}>
<${childTag}></${childTag}>
<${childTag}></${childTag}>
<${childTag}></${childTag}>
</${parentTag}>
`);
const newField = await fixture(html`
<${childTag}></${childTag}>
`);
newField.myProp = 'test';
el.children[1].insertAdjacentElement('beforebegin', newField);
expect(el.formElements.length).to.equal(4);
expect(el.children[1].myProp).to.equal('test');
});
describe('FormRegistrarPortalMixin', () => {
it('throws if there is no .registrationTarget', async () => {
// we test the private api directly as errors thrown from a web component are in a

View file

@ -370,13 +370,13 @@ export class LionFieldset extends FormRegistrarMixin(
*
* @override
*/
addFormElement(child) {
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) {
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`);
}
@ -385,11 +385,16 @@ export class LionFieldset extends FormRegistrarMixin(
// eslint-disable-next-line no-param-reassign
child.makeRequestToBeDisabled();
}
if (name.substr(-2) === '[]') {
if (name.substr(-2) === '[]' || this._childNamesCanBeDuplicate) {
if (!Array.isArray(this.formElements[name])) {
this.formElements[name] = [];
}
this.formElements[name].push(child);
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 {
@ -417,6 +422,16 @@ export class LionFieldset extends FormRegistrarMixin(
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.

View file

@ -62,18 +62,18 @@ For usage and installation please see the appropriate packages.
name="checkers"
.validators="${[new Required()]}"
>
<lion-checkbox name="checkers[]" value="foo" label="I like foo"></lion-checkbox>
<lion-checkbox name="checkers[]" value="bar" label="I like bar"></lion-checkbox>
<lion-checkbox name="checkers[]" value="baz" label="I like baz"></lion-checkbox>
<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 name="dinosaurs[]" value="allosaurus" label="allosaurus"></lion-radio>
<lion-radio name="dinosaurs[]" value="brontosaurus" label="brontosaurus"></lion-radio>
<lion-radio name="dinosaurs[]" value="diplodocus" label="diplodocus"></lion-radio>
<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">
@ -104,7 +104,6 @@ For usage and installation please see the appropriate packages.
></lion-input-range>
<lion-checkbox-group name="terms" .validators="${[new Required()]}">
<lion-checkbox
name="terms[]"
label="I blindly accept all terms and conditions"
></lion-checkbox>
</lion-checkbox-group>

View file

@ -169,10 +169,10 @@ In the following example we will demonstrate this with interaction states, the m
<h3>
Set conditions for validation feedback visibility
</h3>
<lion-checkbox-group name="props" @model-value-changed="${fetchConditionsAndReevaluate}">
<lion-checkbox-group name="props[]" @model-value-changed="${fetchConditionsAndReevaluate}">
${props.map(
p => html`
<lion-checkbox name="props[]" .label="${p}" .choiceValue="${p}"> </lion-checkbox>
<lion-checkbox .label="${p}" .choiceValue="${p}"> </lion-checkbox>
`,
)}
</lion-checkbox-group>

View file

@ -1,6 +1,6 @@
import { html, css, LitElement, DisabledMixin } from '@lion/core';
import { FormRegisteringMixin } from '@lion/field';
import { ChoiceInputMixin } from '@lion/choice-input';
import { css, DisabledMixin, html, LitElement } from '@lion/core';
import { FormRegisteringMixin } from '@lion/field';
/**
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option
@ -16,6 +16,10 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
type: Boolean,
reflect: true,
},
name: {
type: String,
reflect: true,
},
};
}

View file

@ -26,10 +26,10 @@ import '@lion/radio-group/lion-radio-group.js';
### Example
```html
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?">
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'} checked></lion-radio>
<lion-radio-group name="dinos" label="What are your favourite dinosaurs?">
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'} checked></lion-radio>
</lion-radio-group>
```

View file

@ -32,6 +32,7 @@
"*.js"
],
"dependencies": {
"@lion/choice-input": "0.5.9",
"@lion/core": "0.4.3",
"@lion/fieldset": "0.6.9"
},
@ -39,6 +40,7 @@
"@lion/radio": "0.3.9",
"@lion/validate": "0.6.6",
"@open-wc/demoing-storybook": "^1.8.3",
"@open-wc/testing": "^2.5.0"
"@open-wc/testing": "^2.5.0",
"sinon": "^7.2.2"
}
}

View file

@ -1,21 +1,22 @@
import { ChoiceGroupMixin } from '@lion/choice-input';
import { LionFieldset } from '@lion/fieldset';
/**
* LionRadioGroup: extends the lion-fieldset
*
* <lion-radio-group>
* <lion-radio-group name="radios">
* <label slot="label">My Radio</label>
* <lion-radio name="name[]">
* <lion-radio>
* <label slot="label">Male</label>
* </lion-radio>
* <lion-radio name="name[]">
* <lion-radio>
* <label slot="label">Female</label>
* </lion-radio>
* </lion-radio-group>
*
* You can preselect an option by setting marking an lion-radio checked.
* Example:
* <lion-radio name="name[]" checked>
* <lion-radio checked></lion-radio>
*
* It extends LionFieldset so it inherits it's features.
*
@ -24,102 +25,9 @@ import { LionFieldset } from '@lion/fieldset';
* @extends {LionFieldset}
*/
export class LionRadioGroup extends LionFieldset {
get checkedValue() {
const el = this._getCheckedRadioElement();
return el ? el.modelValue.value : '';
}
set checkedValue(value) {
this._setCheckedRadioElement(value, (el, val) => el.modelValue.value === val);
}
get serializedValue() {
const el = this._getCheckedRadioElement();
return el ? el.serializedValue : '';
}
set serializedValue(value) {
this._setCheckedRadioElement(value, (el, val) => el.serializedValue === val);
}
get formattedValue() {
const el = this._getCheckedRadioElement();
return el ? el.formattedValue : '';
}
set formattedValue(value) {
this._setCheckedRadioElement(value, (el, val) => el.formattedValue === val);
}
export class LionRadioGroup extends ChoiceGroupMixin(LionFieldset) {
connectedCallback() {
super.connectedCallback();
this.addEventListener('model-value-changed', this._checkRadioElements);
this._setRole('radiogroup');
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('model-value-changed', this._checkRadioElements);
}
_checkRadioElements(ev) {
const { target } = ev;
if (target.type !== 'radio' || target.checked === false) return;
const groupName = target.name;
this.formElementsArray
.filter(i => i.name === groupName)
.forEach(radio => {
if (radio !== target) {
radio.checked = false; // eslint-disable-line no-param-reassign
}
});
this.__triggerCheckedValueChanged();
}
_getCheckedRadioElement() {
const filtered = this.formElementsArray.filter(el => el.checked === true);
return filtered.length > 0 ? filtered[0] : undefined;
}
async _setCheckedRadioElement(value, check) {
if (!this.__readyForRegistration) {
await this.registrationReady;
}
for (let i = 0; i < this.formElementsArray.length; i += 1) {
if (check(this.formElementsArray[i], value)) {
this.formElementsArray[i].checked = true;
return;
}
}
}
_onFocusOut() {
this.touched = true;
this.focused = false;
}
__triggerCheckedValueChanged() {
const value = this.checkedValue;
if (value != null && value !== this.__previousCheckedValue) {
this.dispatchEvent(
new CustomEvent('checked-value-changed', { bubbles: true, composed: true }),
);
this.touched = true;
this.__previousCheckedValue = value;
}
}
_isEmpty() {
const value = this.checkedValue;
if (typeof value === 'string' && value === '') {
return true;
}
if (value === undefined || value === null) {
return true;
}
return false;
}
}

View file

@ -11,19 +11,19 @@ You should use `<lion-radio>`s inside this element.
<Story name="Default">
{html`
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?">
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
<lion-radio-group name="dinos" label="What are your favourite dinosaurs?">
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group>
`}
</Story>
```html
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?">
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
<lion-radio-group name="dinos" label="What are your favourite dinosaurs?">
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group>
```
@ -60,19 +60,19 @@ You can pre-select an option by adding the checked attribute to the selected `li
<Story name="Pre-select">
{html`
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?">
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'} checked></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
<lion-radio-group name="dinos" label="What are your favourite dinosaurs?">
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'} checked></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group>
`}
</Story>
```html
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?">
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'} checked></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
<lion-radio-group name="dinos" label="What are your favourite dinosaurs?">
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'} checked></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group>
```
@ -82,19 +82,19 @@ You can disable a specific `lion-radio` option by adding the `disabled` attribut
<Story name="Disabled radio">
{html`
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?">
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'} disabled></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
<lion-radio-group name="dinos" label="What are your favourite dinosaurs?">
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'} disabled></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group>
`}
</Story>
```html
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?">
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'} disabled></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
<lion-radio-group name="dinos" label="What are your favourite dinosaurs?">
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'} disabled></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group>
```
@ -102,19 +102,19 @@ You can do the same thing for the entire group by setting the `disabled` attribu
<Story name="Disabled group">
{html`
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?" disabled>
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
<lion-radio-group name="dinos" label="What are your favourite dinosaurs?" disabled>
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group>
`}
</Story>
```html
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?">
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'} disabled></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
<lion-radio-group name="dinos" label="What are your favourite dinosaurs?">
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'} disabled></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group>
```
@ -124,19 +124,19 @@ You can do the same thing for the entire group by setting the `disabled` attribu
{() => {
loadDefaultFeedbackMessages();
const validate = () => {
const radioGroup = document.querySelector('#dinosGroup');
const radioGroup = document.querySelector('#dinos');
radioGroup.submitted = !radioGroup.submitted;
};
return html`
<lion-radio-group
id="dinosGroup"
name="dinosGroup"
id="dinos"
name="dinos"
label="Favourite dinosaur"
.validators=${[new Required()]}
>
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue="${'diplodocus'}"></lion-radio>
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio label="diplodocus" .choiceValue="${'diplodocus'}"></lion-radio>
</lion-radio-group>
<button @click="${() => validate()}">Validate</button>
`;
@ -154,14 +154,14 @@ const validate = () => {
```html
<lion-radio-group
id="dinosGroup"
name="dinosGroup"
id="dinos"
name="dinos"
label="Favourite dinosaur"
.validators=${[new Required()]}
>
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue="${'diplodocus'}"></lion-radio>
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio label="diplodocus" .choiceValue="${'diplodocus'}"></lion-radio>
</lion-radio-group>
<button @click="${() => validate()}">Validate</button>
```
@ -176,28 +176,26 @@ You can also create a validator that validates whether a certain option is check
this.name = 'IsBrontosaurus';
}
execute(value) {
const selectedValue = value['dinos[]'].find(v => v.checked === true);
const hasError = selectedValue ? selectedValue.value !== 'brontosaurus' : false;
return hasError;
return value === 'brontosaurus';
}
static async getMessage() {
return 'You need to select "brontosaurus"';
}
}
const validate = () => {
const radioGroup = document.querySelector('#dinosGroupTwo');
const radioGroup = document.querySelector('#dinosTwo');
radioGroup.submitted = !radioGroup.submitted;
};
return html`
<lion-radio-group
id="dinosGroupTwo"
name="dinosGroup"
id="dinosTwo"
name="dinos"
label="Favourite dinosaur"
.validators=${[new Required(), new IsBrontosaurus()]}
>
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group>
<button @click="${() => validate()}">Validate</button>
`;
@ -213,9 +211,7 @@ class IsBrontosaurus extends Validator {
this.name = 'IsBrontosaurus';
}
execute(value) {
const selectedValue = value['dinos[]'].find(v => v.checked === true);
const hasError = selectedValue ? selectedValue.value !== 'brontosaurus' : false;
return hasError;
return value === 'brontosaurus';
}
static async getMessage() {
return 'You need to select "brontosaurus"';
@ -230,13 +226,13 @@ const validate = () => {
```html
<lion-radio-group
id="dinosGroupTwo"
name="dinosGroup"
name="dinos"
label="Favourite dinosaur"
.validators=${[new Required(), new IsBrontosaurus()]}
>
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group>
<button @click="${() => validate()}">Validate</button>
```

View file

@ -1,194 +1,45 @@
import { expect, fixture, nextFrame, html } from '@open-wc/testing';
import { Required } from '@lion/validate';
import '@lion/radio/lion-radio.js';
import { expect, fixture, html, nextFrame } from '@open-wc/testing';
import '../lion-radio-group.js';
describe('<lion-radio-group>', () => {
it('has a single checkedValue representing the currently checked radio value', async () => {
it('should have role = radiogroup', async () => {
const el = await fixture(html`
<lion-radio-group>
<lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'female'} checked></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'alien'}></lion-radio>
<lion-radio-group label="Gender" name="gender">
<lion-radio label="male" value="male"></lion-radio>
<lion-radio label="female" value="female" checked></lion-radio>
</lion-radio-group>
`);
await nextFrame();
expect(el.checkedValue).to.equal('female');
el.formElementsArray[0].checked = true;
expect(el.checkedValue).to.equal('male');
el.formElementsArray[2].checked = true;
expect(el.checkedValue).to.equal('alien');
expect(el.getAttribute('role')).to.equal('radiogroup');
});
it('can set initial checkedValue on creation', async () => {
const checkedValue = 'alien';
it(`becomes "touched" once a single element of the group changes`, async () => {
const el = await fixture(html`
<lion-radio-group .checkedValue="${checkedValue}">
<lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'female'}></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'alien'}></lion-radio>
</lion-radio-group>
`);
await nextFrame();
await el.registrationReady;
await el.updateComplete;
expect(el.checkedValue).to.equal('alien');
expect(el.formElementsArray[2].checked).to.be.true;
});
it('can handle complex data via choiceValue', async () => {
const date = new Date(2018, 11, 24, 10, 33, 30, 0);
const el = await fixture(html`
<lion-radio-group>
<lion-radio name="data[]" .choiceValue=${{ some: 'data' }}></lion-radio>
<lion-radio name="data[]" .choiceValue=${date} checked></lion-radio>
<lion-radio-group name="myGroup">
<lion-radio></lion-radio>
<lion-radio></lion-radio>
</lion-radio-group>
`);
await nextFrame();
expect(el.checkedValue).to.equal(date);
el.formElementsArray[0].checked = true;
expect(el.checkedValue).to.deep.equal({ some: 'data' });
});
it('can handle 0 and empty string as valid values ', async () => {
const el = await fixture(html`
<lion-radio-group>
<lion-radio name="data[]" .choiceValue=${0} checked></lion-radio>
<lion-radio name="data[]" .choiceValue=${''}></lion-radio>
</lion-radio-group>
`);
await nextFrame();
expect(el.checkedValue).to.equal(0);
el.formElementsArray[1].checked = true;
expect(el.checkedValue).to.equal('');
});
it('still has a full modelValue ', async () => {
const el = await fixture(html`
<lion-radio-group>
<lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'female'} checked></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'alien'}></lion-radio>
</lion-radio-group>
`);
await nextFrame();
expect(el.modelValue).to.deep.equal({
'gender[]': [
{ value: 'male', checked: false },
{ value: 'female', checked: true },
{ value: 'alien', checked: false },
],
});
});
it('can check a radio by supplying an available checkedValue', async () => {
const el = await fixture(html`
<lion-radio-group>
<lion-radio name="gender[]" .modelValue="${{ value: 'male', checked: false }}"></lion-radio>
<lion-radio
name="gender[]"
.modelValue="${{ value: 'female', checked: true }}"
></lion-radio>
<lion-radio
name="gender[]"
.modelValue="${{ value: 'alien', checked: false }}"
></lion-radio>
</lion-radio-group>
`);
await nextFrame();
expect(el.checkedValue).to.equal('female');
el.checkedValue = 'alien';
expect(el.formElementsArray[2].checked).to.be.true;
});
it('fires checked-value-changed event only once per checked change', async () => {
let counter = 0;
const el = await fixture(html`
<lion-radio-group
@checked-value-changed=${() => {
counter += 1;
}}
>
<lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio>
<lion-radio name="gender[]" .modelValue=${{ value: 'female', checked: true }}></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'alien'}></lion-radio>
</lion-radio-group>
`);
await nextFrame();
expect(counter).to.equal(0);
el.formElementsArray[0].checked = true;
expect(counter).to.equal(1);
// not changed values trigger no event
el.formElementsArray[0].checked = true;
expect(counter).to.equal(1);
el.formElementsArray[2].checked = true;
expect(counter).to.equal(2);
// not found values trigger no event
el.checkedValue = 'foo';
expect(counter).to.equal(2);
el.checkedValue = 'male';
expect(counter).to.equal(3);
});
it('expect child nodes to only fire one model-value-changed event per instance', async () => {
let counter = 0;
const el = await fixture(html`
<lion-radio-group
@model-value-changed=${() => {
counter += 1;
}}
>
<lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio>
<lion-radio name="gender[]" .modelValue=${{ value: 'female', checked: true }}></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'alien'}></lion-radio>
</lion-radio-group>
`);
await nextFrame();
counter = 0; // reset after setup which may result in different results
el.formElementsArray[0].checked = true;
expect(counter).to.equal(2); // male becomes checked, female becomes unchecked
// not changed values trigger no event
el.formElementsArray[0].checked = true;
expect(counter).to.equal(2);
el.formElementsArray[2].checked = true;
expect(counter).to.equal(4); // alien becomes checked, male becomes unchecked
// not found values trigger no event
el.checkedValue = 'foo';
expect(counter).to.equal(4);
el.checkedValue = 'male';
expect(counter).to.equal(6); // male becomes checked, alien becomes unchecked
el.children[1].focus();
expect(el.touched).to.equal(false, 'initially, touched state is false');
el.children[1].checked = true;
expect(el.touched, `focused via a mouse click, group should be touched`).to.be.true;
});
it('allows selection of only one radio in a named group', async () => {
const el = await fixture(html`
<lion-radio-group>
<lion-radio name="gender[]" .modelValue="${{ value: 'male', checked: false }}"></lion-radio>
<lion-radio
name="gender[]"
.modelValue="${{ value: 'female', checked: false }}"
></lion-radio>
<lion-radio-group name="gender">
<lion-radio .modelValue="${{ value: 'male', checked: false }}"></lion-radio>
<lion-radio .modelValue="${{ value: 'female', checked: false }}"></lion-radio>
</lion-radio-group>
`);
await nextFrame();
const male = el.formElements['gender[]'][0];
const male = el.formElementsArray[0];
const maleInput = male.querySelector('input');
const female = el.formElements['gender[]'][1];
const female = el.formElementsArray[1];
const femaleInput = female.querySelector('input');
expect(male.checked).to.equal(false);
@ -205,95 +56,11 @@ describe('<lion-radio-group>', () => {
expect(female.checked).to.equal(true);
});
it('should have role = radiogroup', async () => {
const el = await fixture(html`
<lion-radio-group>
<label slot="label">My group</label>
<lion-radio name="gender[]" value="male">
<label slot="label">male</label>
</lion-radio>
<lion-radio name="gender[]" value="female">
<label slot="label">female</label>
</lion-radio>
</lion-radio-group>
`);
await nextFrame();
expect(el.getAttribute('role')).to.equal('radiogroup');
});
it('can be required', async () => {
const el = await fixture(html`
<lion-radio-group .validators=${[new Required()]}>
<lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio>
<lion-radio
name="gender[]"
.choiceValue=${{ subObject: 'satisfies required' }}
></lion-radio>
</lion-radio-group>
`);
expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).to.have.a.property('Required');
el.formElements['gender[]'][0].checked = true;
expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('Required');
el.formElements['gender[]'][1].checked = true;
expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('Required');
});
it('returns serialized value', async () => {
const el = await fixture(html`
<lion-radio-group>
<lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'female'}></lion-radio>
</lion-radio-group>
`);
el.formElements['gender[]'][0].checked = true;
expect(el.serializedValue).to.deep.equal({ checked: true, value: 'male' });
});
it('returns serialized value on unchecked state', async () => {
const el = await fixture(html`
<lion-radio-group>
<lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'female'}></lion-radio>
</lion-radio-group>
`);
await nextFrame();
expect(el.serializedValue).to.deep.equal('');
});
it(`becomes "touched" once a single element of the group changes`, async () => {
const el = await fixture(html`
<lion-radio-group>
<lion-radio name="myGroup[]"></lion-radio>
<lion-radio name="myGroup[]"></lion-radio>
</lion-radio-group>
`);
await nextFrame();
el.children[1].focus();
expect(el.touched).to.equal(false, 'initially, touched state is false');
el.children[1].checked = true;
expect(el.touched, `focused via a mouse click, group should be touched`).to.be.true;
});
it('is accessible', async () => {
const el = await fixture(html`
<lion-radio-group>
<label slot="label">My group</label>
<lion-radio name="gender[]" value="male">
<label slot="label">male</label>
</lion-radio>
<lion-radio name="gender[]" value="female" checked>
<label slot="label">female</label>
</lion-radio>
<lion-radio-group label="My group" name="gender">
<lion-radio label="male" value="male"></lion-radio>
<lion-radio label="female" value="female" checked></lion-radio>
</lion-radio-group>
`);
await nextFrame();
@ -302,14 +69,9 @@ describe('<lion-radio-group>', () => {
it('is accessible when the group is disabled', async () => {
const el = await fixture(html`
<lion-radio-group disabled>
<label slot="label">My group</label>
<lion-radio name="gender[]" value="male">
<label slot="label">male</label>
</lion-radio>
<lion-radio name="gender[]" value="female">
<label slot="label">female</label>
</lion-radio>
<lion-radio-group label="My group" name="gender" disabled>
<lion-radio label="male" value="male"></lion-radio>
<lion-radio label="female" value="female" checked></lion-radio>
</lion-radio-group>
`);
await nextFrame();
@ -318,14 +80,9 @@ describe('<lion-radio-group>', () => {
it('is accessible when an option is disabled', async () => {
const el = await fixture(html`
<lion-radio-group>
<label slot="label">My group</label>
<lion-radio name="gender[]" value="male" disabled>
<label slot="label">male</label>
</lion-radio>
<lion-radio name="gender[]" value="female">
<label slot="label">female</label>
</lion-radio>
<lion-radio-group label="My group" name="gender">
<lion-radio label="male" value="male" disabled></lion-radio>
<lion-radio label="female" value="female" checked></lion-radio>
</lion-radio-group>
`);
await nextFrame();

View file

@ -1,22 +1,22 @@
import { LionInput } from '@lion/input';
import { ChoiceInputMixin } from '@lion/choice-input';
import { LionInput } from '@lion/input';
/**
* Lion-radio can be used inside a lion-radio-group.
*
* <lion-radio-group>
* <lion-radio-group name="radios">
* <label slot="label">My Radio</label>
* <lion-radio name="name[]">
* <lion-radio>
* <label slot="label">Male</label>
* </lion-radio>
* <lion-radio name="name[]">
* <lion-radio>
* <label slot="label">Female</label>
* </lion-radio>
* </lion-radio-group>
*
* You can preselect an option by setting marking an lion-radio checked.
* Example:
* <lion-radio name="name[]" checked>
* <lion-radio checked>
*
*
* @customElement lion-radio

View file

@ -9,15 +9,19 @@ import '../lion-radio.js';
`lion-radio` component is a sub-element to be used in [lion-radio-group](?path=/docs/forms-radio-group--default-story) elements. Its purpose is to provide a way for users to check a **single** option amongst a set of choices.
<Story name="Default">{html`
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'} checked></lion-radio>
<lion-radio-group name="dinos">
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'} checked></lion-radio>
</lion-radio-group>
`}</Story>
```html
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'} checked></lion-radio>
<lion-radio-group name="dinos">
<lion-radio label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio label="diplodocus" .choiceValue=${'diplodocus'} checked></lion-radio>
</lion-radio-group>
```
- Use this component inside a [lion-radio-group](?path=/docs/forms-radio-group--default-story)

View file

@ -1,21 +1,12 @@
import { expect, fixture, nextFrame } from '@open-wc/testing';
import '../lion-radio.js';
describe('<lion-radio>', () => {
it('should have type = radio', async () => {
const el = await fixture(`
<lion-radio-group>
<label slot="label">My group</label>
<lion-radio name="gender[]" value="male">
<label slot="label">male</label>
</lion-radio>
<lion-radio name="gender[]" value="female">
<label slot="label">female</label>
</lion-radio>
</lion-radio-group>
<lion-radio name="radio" value="male"></lion-radio>
`);
await nextFrame();
expect(el.children[1]._inputNode.getAttribute('type')).to.equal('radio');
expect(el.getAttribute('type')).to.equal('radio');
});
});

View file

@ -37,6 +37,7 @@
],
"dependencies": {
"@lion/button": "0.5.4",
"@lion/choice-input": "0.5.9",
"@lion/core": "0.4.3",
"@lion/field": "0.8.9",
"@lion/option": "0.4.9",

View file

@ -1,10 +1,11 @@
import { html, css, LitElement, SlotMixin } from '@lion/core';
import { withDropdownConfig, OverlayMixin } from '@lion/overlays';
import { FormControlMixin, InteractionStateMixin, FormRegistrarMixin } from '@lion/field';
import { ChoiceGroupMixin } from '@lion/choice-input';
import { css, html, LitElement, SlotMixin } from '@lion/core';
import { FormControlMixin, FormRegistrarMixin, InteractionStateMixin } from '@lion/field';
import { formRegistrarManager } from '@lion/field/src/formRegistrarManager.js';
import { OverlayMixin, withDropdownConfig } from '@lion/overlays';
import { ValidateMixin } from '@lion/validate';
import './differentKeyNamesShimIE.js';
import '../lion-select-invoker.js';
import './differentKeyNamesShimIE.js';
function uuid() {
return Math.random()
@ -45,15 +46,15 @@ function isInView(container, element, partial = false) {
* @customElement lion-select-rich
* @extends {LitElement}
*/
export class LionSelectRich extends OverlayMixin(
FormRegistrarMixin(InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement))))),
export class LionSelectRich extends ChoiceGroupMixin(
OverlayMixin(
FormRegistrarMixin(
InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement)))),
),
),
) {
static get properties() {
return {
checkedValue: {
type: Object,
},
disabled: {
type: Boolean,
reflect: true,
@ -70,10 +71,6 @@ export class LionSelectRich extends OverlayMixin(
attribute: 'interaction-mode',
},
modelValue: {
type: Array,
},
name: {
type: String,
},
@ -94,22 +91,6 @@ export class LionSelectRich extends OverlayMixin(
];
}
/**
* @override
*/
static _isPrefilled(modelValue) {
if (!modelValue) {
return false;
}
const checkedModelValue = modelValue.find(subModelValue => subModelValue.checked === true);
if (!checkedModelValue) {
return false;
}
const { value } = checkedModelValue;
return super._isPrefilled(value);
}
get slots() {
return {
...super.slots,
@ -132,16 +113,38 @@ export class LionSelectRich extends OverlayMixin(
return this._listboxNode.querySelector(`#${this._listboxActiveDescendant}`);
}
get checkedIndex() {
if (this.modelValue) {
return this.modelValue.findIndex(el => el.value === this.checkedValue);
get modelValue() {
const el = this.formElements.find(option => option.checked);
return el ? el.modelValue.value : '';
}
set modelValue(value) {
const el = this.formElements.find(option => option.modelValue.value === value);
if (el) {
el.checked = true;
} else {
// cache user set modelValue, and then try it again when registration is done
this.__cachedUserSetModelValue = value;
}
return -1;
this.__syncInvokerElement();
this.requestUpdate('modelValue');
}
get checkedIndex() {
let checkedIndex = -1;
this.formElements.forEach((option, i) => {
if (option.checked) {
checkedIndex = i;
}
});
return checkedIndex;
}
set checkedIndex(index) {
if (this.formElements[index]) {
this.formElements[index].checked = true;
if (this._listboxNode.children[index]) {
this._listboxNode.children[index].checked = true;
}
}
@ -169,8 +172,6 @@ export class LionSelectRich extends OverlayMixin(
this.interactionMode = 'auto';
this.disabled = false;
// for interaction states
// we use a different event as 'model-value-changed' would bubble up from all options
this._valueChangedEvent = 'select-model-value-changed';
this._listboxActiveDescendant = null;
this.__hasInitialSelectedFormElement = false;
@ -200,31 +201,19 @@ export class LionSelectRich extends OverlayMixin(
this.__setupInvokerNode();
this.__setupListboxNode();
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
formRegistrarManager.addEventListener('all-forms-open-for-registration', () => {
// Now that we have rendered + registered our listbox, try setting the user defined modelValue again
if (this.__cachedUserSetModelValue) {
this.modelValue = this.__cachedUserSetModelValue;
}
});
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
this.__toggleInvokerDisabled();
}
_requestUpdate(name, oldValue) {
super._requestUpdate(name, oldValue);
if (
name === 'checkedValue' &&
!this.__isSyncingCheckedAndModelValue &&
this.modelValue &&
this.modelValue.length > 0
) {
if (this.checkedIndex) {
// Necessary to sync the checkedIndex through the getter/setter explicitly
// eslint-disable-next-line no-self-assign
this.checkedIndex = this.checkedIndex;
}
}
if (name === 'modelValue') {
this.dispatchEvent(new CustomEvent('select-model-value-changed'));
this.__onModelValueChanged();
}
if (name === 'interactionMode') {
if (this.interactionMode === 'auto') {
this.interactionMode = detectInteractionMode();
@ -308,14 +297,11 @@ export class LionSelectRich extends OverlayMixin(
*
* @override
* @param {*} child
* @param {Number} indexToInsertAt
*/
addFormElement(passedChild) {
const child = passedChild;
addFormElement(child, indexToInsertAt) {
super.addFormElement(child, indexToInsertAt);
// Set the name property on the option elements ourselves, for form serialization
child.name = `${this.name}[]`;
super.addFormElement(child);
// we need to adjust the elements being registered
/* eslint-disable no-param-reassign */
child.id = child.id || `${this.localName}-option-${uuid()}`;
@ -323,6 +309,7 @@ export class LionSelectRich extends OverlayMixin(
if (this.disabled) {
child.makeRequestToBeDisabled();
}
// the first elements checked by default
if (!this.__hasInitialSelectedFormElement && (!child.disabled || this.disabled)) {
child.active = true;
@ -338,10 +325,6 @@ export class LionSelectRich extends OverlayMixin(
/* eslint-enable no-param-reassign */
}
_getFromAllFormElements(property) {
return this.formElements.map(e => e[property]);
}
__setupEventListeners() {
this.__onChildActiveChanged = this.__onChildActiveChanged.bind(this);
this.__onChildModelValueChanged = this.__onChildModelValueChanged.bind(this);
@ -391,24 +374,15 @@ export class LionSelectRich extends OverlayMixin(
formElement.checked = false;
}
});
this.modelValue = target.value;
}
this.modelValue = this._getFromAllFormElements('modelValue');
}
__onModelValueChanged() {
this.__isSyncingCheckedAndModelValue = true;
const foundChecked = this.modelValue.find(subModelValue => subModelValue.checked);
if (foundChecked && foundChecked.value !== this.checkedValue) {
this.checkedValue = foundChecked.value;
}
__syncInvokerElement() {
// sync to invoker
if (this._invokerNode) {
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
}
this.__isSyncingCheckedAndModelValue = false;
}
__getNextEnabledOption(currentIndex, offset = 1) {
@ -659,17 +633,6 @@ export class LionSelectRich extends OverlayMixin(
}
}
_isEmpty() {
const value = this.checkedValue;
if (typeof value === 'string' && value === '') {
return true;
}
if (value === undefined || value === null) {
return true;
}
return false;
}
/**
* @override Configures OverlayMixin
*/

View file

@ -1,10 +1,9 @@
import { expect, fixture, html, triggerFocusFor, triggerBlurFor } from '@open-wc/testing';
import './keyboardEventShimIE.js';
import { Required } from '@lion/validate';
import '@lion/option/lion-option.js';
import { Required } from '@lion/validate';
import { expect, fixture, html, triggerBlurFor, triggerFocusFor } from '@open-wc/testing';
import '../lion-options.js';
import '../lion-select-rich.js';
import './keyboardEventShimIE.js';
describe('lion-select-rich interactions', () => {
describe('values', () => {
@ -36,11 +35,7 @@ describe('lion-select-rich interactions', () => {
expect(el.querySelector('lion-option').checked).to.be.true;
expect(el.querySelector('lion-option').active).to.be.true;
expect(el.modelValue).to.deep.equal([
{ value: 10, checked: true },
{ value: 20, checked: false },
]);
expect(el.checkedValue).to.equal(10);
expect(el.modelValue).to.equal(10);
expect(el.checkedIndex).to.equal(0);
expect(el.activeIndex).to.equal(0);
@ -55,11 +50,7 @@ describe('lion-select-rich interactions', () => {
</lion-options>
</lion-select-rich>
`);
expect(el.modelValue).to.deep.equal([
{ value: null, checked: true },
{ value: 20, checked: false },
]);
expect(el.checkedValue).to.be.null;
expect(el.modelValue).to.be.null;
});
it('has the checked option as modelValue', async () => {
@ -71,27 +62,7 @@ describe('lion-select-rich interactions', () => {
</lion-options>
</lion-select-rich>
`);
expect(el.modelValue).to.deep.equal([
{ value: 10, checked: false },
{ value: 20, checked: true },
]);
expect(el.checkedValue).to.equal(20);
});
it('syncs checkedValue to modelValue', async () => {
const el = await fixture(html`
<lion-select-rich>
<lion-options slot="input">
<lion-option .choiceValue=${10}>Item 1</lion-option>
<lion-option .choiceValue=${20}>Item 2</lion-option>
</lion-options>
</lion-select-rich>
`);
el.checkedValue = 20;
expect(el.modelValue).to.deep.equal([
{ value: 10, checked: false },
{ value: 20, checked: true },
]);
expect(el.modelValue).to.equal(20);
});
it('has an activeIndex', async () => {
@ -139,13 +110,13 @@ describe('lion-select-rich interactions', () => {
</lion-options>
</lion-select-rich>
`);
expect(el.checkedValue).to.equal(30);
expect(el.modelValue).to.equal(30);
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' }));
expect(el.checkedValue).to.equal(10);
expect(el.modelValue).to.equal(10);
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
expect(el.checkedValue).to.equal(40);
expect(el.modelValue).to.equal(40);
});
// TODO: nice to have
@ -297,7 +268,7 @@ describe('lion-select-rich interactions', () => {
</lion-options>
</lion-select-rich>
`);
expect(el.checkedValue).to.equal(10);
expect(el.modelValue).to.equal(10);
});
it('cannot be navigated with keyboard if disabled', async () => {
@ -310,7 +281,7 @@ describe('lion-select-rich interactions', () => {
</lion-select-rich>
`);
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
expect(el.checkedValue).to.equal(10);
expect(el.modelValue).to.equal(10);
});
it('cannot be opened via click if disabled', async () => {
@ -477,10 +448,7 @@ describe('lion-select-rich interactions', () => {
`);
const option = el.querySelectorAll('lion-option')[1];
option.checked = true;
expect(el.modelValue).to.deep.equal([
{ value: 10, checked: false },
{ value: 20, checked: true },
]);
expect(el.modelValue).to.equal(20);
});
it('does not allow to set checkedIndex or activeIndex to be out of bound', async () => {
@ -543,7 +511,7 @@ describe('lion-select-rich interactions', () => {
</lion-select-rich>
`);
expect(el.dirty).to.be.false;
el.checkedValue = 20;
el.modelValue = 20;
expect(el.dirty).to.be.true;
});
@ -599,7 +567,7 @@ describe('lion-select-rich interactions', () => {
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).to.have.a.property('Required');
el.checkedValue = 20;
el.modelValue = 20;
expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('Required');

View file

@ -1,22 +1,116 @@
import {
expect,
fixture,
html,
aTimeout,
defineCE,
unsafeStatic,
nextFrame,
} from '@open-wc/testing';
import { LitElement } from '@lion/core';
import '@lion/option/lion-option.js';
import { OverlayController } from '@lion/overlays';
import './keyboardEventShimIE.js';
import { Required } from '@lion/validate';
import {
aTimeout,
defineCE,
expect,
fixture,
html,
nextFrame,
unsafeStatic,
} from '@open-wc/testing';
import { LionSelectRich } from '../index.js';
import '../lion-options.js';
import '../lion-select-rich.js';
import { LionSelectRich } from '../index.js';
import './keyboardEventShimIE.js';
describe('lion-select-rich', () => {
it('has a single modelValue representing the currently checked option', async () => {
const el = await fixture(html`
<lion-select-rich name="foo">
<lion-options slot="input">
<lion-option .choiceValue=${10} checked>Item 1</lion-option>
<lion-option .choiceValue=${20}>Item 2</lion-option>
</lion-options>
</lion-select-rich>
`);
expect(el.modelValue).to.equal(10);
});
it('automatically sets the name attribute of child checkboxes to its own name', async () => {
const el = await fixture(html`
<lion-select-rich name="foo">
<lion-options slot="input">
<lion-option .choiceValue=${10} checked>Item 1</lion-option>
<lion-option .choiceValue=${20}>Item 2</lion-option>
</lion-options>
</lion-select-rich>
`);
await nextFrame();
expect(el.formElementsArray[0].name).to.equal('foo');
expect(el.formElementsArray[1].name).to.equal('foo');
const validChild = await fixture(html`
<lion-option .choiceValue=${30}>Item 3</lion-option>
`);
el.appendChild(validChild);
expect(el.formElementsArray[2].name).to.equal('foo');
});
it('throws if a child element without a modelValue like { value: "foo", checked: false } tries to register', async () => {
const el = await fixture(html`
<lion-select-rich name="foo">
<lion-options slot="input">
<lion-option .choiceValue=${10} checked>Item 1</lion-option>
<lion-option .choiceValue=${20}>Item 2</lion-option>
</lion-options>
</lion-select-rich>
`);
await nextFrame();
const invalidChild = await fixture(html`
<lion-option .modelValue=${'Lara'}></lion-option>
`);
expect(() => {
el.addFormElement(invalidChild);
}).to.throw(
'The lion-select-rich name="foo" does not allow to register lion-option with .modelValue="Lara" - The modelValue should represent an Object { value: "foo", checked: false }',
);
});
it('throws if a child element with a different name than the group tries to register', async () => {
const el = await fixture(html`
<lion-select-rich name="gender">
<lion-options slot="input">
<lion-option .choiceValue=${'female'} checked></lion-option>
<lion-option .choiceValue=${'other'}></lion-option>
</lion-options>
</lion-select-rich>
`);
await nextFrame();
const invalidChild = await fixture(html`
<lion-option name="foo" .choiceValue=${'male'}></lion-option>
`);
expect(() => {
el.addFormElement(invalidChild);
}).to.throw(
'The lion-select-rich name="gender" does not allow to register lion-option with custom names (name="foo" given)',
);
});
it('can set initial modelValue on creation', async () => {
const el = await fixture(html`
<lion-select-rich name="gender" .modelValue=${'other'}>
<lion-options slot="input">
<lion-option .choiceValue=${'male'}></lion-option>
<lion-option .choiceValue=${'female'}></lion-option>
<lion-option .choiceValue=${'other'}></lion-option>
</lion-options>
</lion-select-rich>
`);
expect(el.modelValue).to.equal('other');
expect(el.formElementsArray[2].checked).to.be.true;
});
it(`has a fieldName based on the label`, async () => {
const el1 = await fixture(
html`
@ -77,8 +171,41 @@ describe('lion-select-rich', () => {
const optOne = el.querySelectorAll('lion-option')[0];
const optTwo = el.querySelectorAll('lion-option')[1];
expect(optOne.name).to.equal('foo[]');
expect(optTwo.name).to.equal('foo[]');
expect(optOne.name).to.equal('foo');
expect(optTwo.name).to.equal('foo');
});
it('supports validation', async () => {
const el = await fixture(html`
<lion-select-rich
id="color"
name="color"
label="Favorite color"
.validators="${[new Required()]}"
>
<lion-options slot="input">
<lion-option .choiceValue=${null}>select a color</lion-option>
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'} disabled>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
`);
expect(el.hasFeedbackFor.includes('error')).to.be.true;
expect(el.showsFeedbackFor.includes('error')).to.be.false;
el._listboxNode.children[1].checked = true;
// Set touched to true (needed for feedback show) because we simulate a user touching the select
el.touched = true;
await el.updateComplete;
expect(el.hasFeedbackFor.includes('error')).to.be.false;
expect(el.showsFeedbackFor.includes('error')).to.be.false;
el._listboxNode.children[0].checked = true;
await el.updateComplete;
expect(el.hasFeedbackFor.includes('error')).to.be.true;
expect(el.showsFeedbackFor.includes('error')).to.be.true;
});
describe('Invoker', () => {
@ -93,9 +220,9 @@ describe('lion-select-rich', () => {
expect(el._invokerNode.tagName).to.equal('LION-SELECT-INVOKER');
});
it('syncs the selected element to the invoker', async () => {
it('sets the first option as the selectedElement if no option is checked', async () => {
const el = await fixture(html`
<lion-select-rich>
<lion-select-rich name="foo">
<lion-options slot="input">
<lion-option .choiceValue=${10}>Item 1</lion-option>
<lion-option .choiceValue=${20}>Item 2</lion-option>
@ -103,10 +230,23 @@ describe('lion-select-rich', () => {
</lion-select-rich>
`);
const options = Array.from(el.querySelectorAll('lion-option'));
expect(el._invokerNode.selectedElement).to.equal(options[0]);
expect(el._invokerNode.selectedElement).dom.to.equal(options[0]);
});
el.checkedIndex = 1;
expect(el._invokerNode.selectedElement).to.equal(el.querySelectorAll('lion-option')[1]);
it('syncs the selected element to the invoker', async () => {
const el = await fixture(html`
<lion-select-rich name="foo">
<lion-options slot="input">
<lion-option .choiceValue=${10}>Item 1</lion-option>
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
</lion-options>
</lion-select-rich>
`);
const options = el.querySelectorAll('lion-option');
expect(el._invokerNode.selectedElement).dom.to.equal(options[1]);
el.checkedIndex = 0;
expect(el._invokerNode.selectedElement).dom.to.equal(options[0]);
});
it('delegates readonly to the invoker', async () => {
@ -199,7 +339,7 @@ describe('lion-select-rich', () => {
expect(options[1].checked).to.be.true;
});
it('stays closed on click if it disabled or readonly', async () => {
it('stays closed on click if it is disabled or readonly', async () => {
const elReadOnly = await fixture(html`
<lion-select-rich readonly>
<lion-options slot="input">
@ -449,7 +589,7 @@ describe('lion-select-rich', () => {
</lion-options>
</lion-select-rich>
`);
expect(el.checkedValue).to.deep.equal({
expect(el.modelValue).to.deep.equal({
type: 'mastercard',
label: 'Master Card',
amount: 12000,
@ -457,7 +597,7 @@ describe('lion-select-rich', () => {
});
el.checkedIndex = 1;
expect(el.checkedValue).to.deep.equal({
expect(el.modelValue).to.deep.equal({
type: 'visacard',
label: 'Visa Card',
amount: 0,
@ -476,7 +616,23 @@ describe('lion-select-rich', () => {
constructor() {
super();
this.colorList = [];
this.colorList = [
{
label: 'Red',
value: 'red',
checked: false,
},
{
label: 'Hotpink',
value: 'hotpink',
checked: true,
},
{
label: 'Teal',
value: 'teal',
checked: false,
},
];
}
render() {
@ -502,50 +658,21 @@ describe('lion-select-rich', () => {
<${mySelectContainerTag}></${mySelectContainerTag}>
`);
const colorList = [
{
label: 'Red',
value: 'red',
checked: false,
},
{
label: 'Hotpink',
value: 'hotpink',
checked: true,
},
{
label: 'Teal',
value: 'teal',
checked: false,
},
];
el.colorList = colorList;
el.requestUpdate();
await el.updateComplete;
const selectRich = el.shadowRoot.querySelector('lion-select-rich');
const invoker = selectRich._invokerNode;
// needed to properly set the checkedIndex and checkedValue
selectRich.requestUpdate();
await selectRich.updateComplete;
expect(selectRich.checkedIndex).to.equal(1);
expect(selectRich.checkedValue).to.equal('hotpink');
expect(selectRich.modelValue).to.equal('hotpink');
expect(invoker.selectedElement.value).to.equal('hotpink');
colorList.splice(1, 0, {
label: 'Blue',
value: 'blue',
checked: false,
});
el.requestUpdate();
await el.updateComplete;
const newOption = document.createElement('lion-option');
newOption.modelValue = { checked: false, value: 'blue' };
newOption.textContent = 'Blue';
const hotpinkEl = selectRich._listboxNode.children[1];
hotpinkEl.insertAdjacentElement('beforebegin', newOption);
expect(selectRich.checkedIndex).to.equal(2);
expect(selectRich.checkedValue).to.equal('hotpink');
expect(selectRich.modelValue).to.equal('hotpink');
expect(invoker.selectedElement.value).to.equal('hotpink');
});
});

2932
yarn.lock

File diff suppressed because it is too large Load diff