Merge pull request #241 from ing-bank/fix/formRegIssues

Fix/form reg issues
This commit is contained in:
Joren Broekema 2019-08-15 11:19:44 +02:00 committed by GitHub
commit dbff4bfc55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 390 additions and 332 deletions

View file

@ -43,6 +43,7 @@
"test:update-snapshots": "karma start --update-snapshots",
"test:prune-snapshots": "karma start --prune-snapshots",
"test:bs": "karma start karma.bs.config.js --coverage",
"test:bs:legacy": "karma start --legacy karma.bs.config.js --coverage",
"lint": "run-p lint:*",
"lint:eclint": "git ls-files | xargs eclint check",
"lint:eslint": "eslint --ext .js,.html .",

View file

@ -29,19 +29,14 @@ export const FormRegisteringMixin = dedupeMixin(
__setupRegistrationHook() {
if (formRegistrarManager.ready) {
this._registerFormElement();
this._dispatchRegistration();
} else {
formRegistrarManager.addEventListener('all-forms-open-for-registration', () => {
this._registerFormElement();
this._dispatchRegistration();
});
}
}
_registerFormElement() {
this._dispatchRegistration();
this._requestParentFormGroupUpdateOfResetModelValue();
}
_dispatchRegistration() {
this.dispatchEvent(
new CustomEvent('form-element-register', {
@ -56,16 +51,5 @@ export const FormRegisteringMixin = dedupeMixin(
this.__parentFormGroup.removeFormElement(this);
}
}
/**
* Makes sure our parentFormGroup has the most up to date resetModelValue
* FormGroups will call the same on their parentFormGroup so the full tree gets the correct
* values.
*/
_requestParentFormGroupUpdateOfResetModelValue() {
if (this.__parentFormGroup && this.__parentFormGroup._updateResetModelValue) {
this.__parentFormGroup._updateResetModelValue();
}
}
},
);

View file

@ -101,6 +101,11 @@ export class LionField extends FormControlMixin(
this.submitted = false;
}
firstUpdated(c) {
super.firstUpdated(c);
this._initialModelValue = this.modelValue;
}
connectedCallback() {
// TODO: Normally we put super calls on top for predictability,
// here we temporarily need to do attribute delegation before,
@ -117,14 +122,6 @@ export class LionField extends FormControlMixin(
disconnectedCallback() {
super.disconnectedCallback();
if (this.__parentFormGroup) {
const event = new CustomEvent('form-element-unregister', {
detail: { element: this },
bubbles: true,
});
this.__parentFormGroup.dispatchEvent(event);
}
this.inputElement.removeEventListener('change', this._onChange);
}
@ -164,6 +161,11 @@ export class LionField extends FormControlMixin(
this.submitted = false;
}
reset() {
this.modelValue = this._initialModelValue;
this.resetInteractionState();
}
clear() {
if (super.clear) {
// Let validationMixin and interactionStateMixin clear their

View file

@ -0,0 +1,121 @@
import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing';
import { LitElement } from '@lion/core';
import { FormRegistrarMixin } from '../src/FormRegistrarMixin.js';
import { FormRegisteringMixin } from '../src/FormRegisteringMixin.js';
import { formRegistrarManager } from '../src/formRegistrarManager.js';
export const runRegistrationSuite = customConfig => {
const cfg = {
baseElement: HTMLElement,
suffix: null,
...customConfig,
};
describe(`FormRegistrationMixins${cfg.suffix ? ` (${cfg.suffix})` : ''}`, () => {
let parentTag;
let childTag;
before(async () => {
if (!cfg.parentTagString) {
cfg.parentTagString = defineCE(class extends FormRegistrarMixin(cfg.baseElement) {});
}
if (!cfg.childTagString) {
cfg.childTagString = defineCE(class extends FormRegisteringMixin(cfg.baseElement) {});
}
parentTag = unsafeStatic(cfg.parentTagString);
childTag = unsafeStatic(cfg.childTagString);
});
it('can register a formElement', async () => {
const el = await fixture(html`
<${parentTag}>
<${childTag}></${childTag}>
</${parentTag}>
`);
await el.registrationReady;
expect(el.formElements.length).to.equal(1);
});
it('supports nested registration parents', async () => {
const el = await fixture(html`
<${parentTag}>
<${parentTag}>
<${childTag}></${childTag}>
<${childTag}></${childTag}>
</${parentTag}>
</${parentTag}>
`);
await el.registrationReady;
expect(el.formElements.length).to.equal(1);
expect(el.querySelector(cfg.parentTagString).formElements.length).to.equal(2);
});
it('forgets disconnected registrars', async () => {
const el = await fixture(html`
<${parentTag}>
<${parentTag}>
<${childTag}</${childTag}
</${parentTag}>
</${parentTag}>
`);
const secondRegistrar = await fixture(html`
<${parentTag}>
<${childTag}</${childTag}
</${parentTag}>
`);
el.appendChild(secondRegistrar);
expect(formRegistrarManager.__elements.length).to.equal(3);
el.removeChild(secondRegistrar);
expect(formRegistrarManager.__elements.length).to.equal(2);
});
it('works for components that have a delayed render', async () => {
const tagWrapperString = defineCE(
class extends FormRegistrarMixin(LitElement) {
async performUpdate() {
await new Promise(resolve => setTimeout(() => resolve(), 10));
await super.performUpdate();
}
render() {
return html`
<slot></slot>
`;
}
},
);
const tagWrapper = unsafeStatic(tagWrapperString);
const el = await fixture(html`
<${tagWrapper}>
<${childTag}></${childTag}>
</${tagWrapper}>
`);
await el.registrationReady;
expect(el.formElements.length).to.equal(1);
});
it('can dynamically add/remove elements', async () => {
const el = await fixture(html`
<${parentTag}>
<${childTag}></${childTag}>
</${parentTag}>
`);
const newField = await fixture(html`
<${childTag}></${childTag}>
`);
expect(el.formElements.length).to.equal(1);
el.appendChild(newField);
expect(el.formElements.length).to.equal(2);
el.removeChild(newField);
expect(el.formElements.length).to.equal(1);
});
});
};

View file

@ -1,129 +1,19 @@
import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing';
import sinon from 'sinon';
import { LitElement, UpdatingElement } from '@lion/core';
import { html } from '@open-wc/testing';
import { UpdatingElement, LitElement } from '@lion/core';
import { runRegistrationSuite } from '../test-suites/FormRegistrationMixins.suite.js';
import { formRegistrarManager } from '../src/formRegistrarManager.js';
import { FormRegisteringMixin } from '../src/FormRegisteringMixin.js';
import { FormRegistrarMixin } from '../src/FormRegistrarMixin.js';
describe('FormRegistrationMixins', () => {
before(async () => {
const FormRegistrarEl = class extends FormRegistrarMixin(UpdatingElement) {};
customElements.define('form-registrar', FormRegistrarEl);
const FormRegisteringEl = class extends FormRegisteringMixin(UpdatingElement) {};
customElements.define('form-registering', FormRegisteringEl);
});
it('can register a formElement', async () => {
const el = await fixture(html`
<form-registrar>
<form-registering></form-registering>
</form-registrar>
`);
await el.registrationReady;
expect(el.formElements.length).to.equal(1);
});
it('supports nested registrar', async () => {
const el = await fixture(html`
<form-registrar>
<form-registrar>
<form-registering></form-registering>
</form-registrar>
</form-registrar>
`);
await el.registrationReady;
expect(el.formElements.length).to.equal(1);
expect(el.querySelector('form-registrar').formElements.length).to.equal(1);
});
it('forgets disconnected registrars', async () => {
const el = await fixture(html`
<form-registrar>
<form-registrar>
<form-registering></form-registering>
</form-registrar>
</form-registrar>
`);
const secondRegistrar = await fixture(html`
<form-registrar>
<form-registering></form-registering>
</form-registrar>
`);
el.appendChild(secondRegistrar);
expect(formRegistrarManager.__elements.length).to.equal(3);
el.removeChild(secondRegistrar);
expect(formRegistrarManager.__elements.length).to.equal(2);
});
it('works for component that have a delayed render', async () => {
const tagWrapperString = defineCE(
class extends FormRegistrarMixin(LitElement) {
async performUpdate() {
await new Promise(resolve => setTimeout(() => resolve(), 10));
await super.performUpdate();
}
runRegistrationSuite({
suffix: 'with UpdatingElement',
baseElement: UpdatingElement,
});
runRegistrationSuite({
suffix: 'with LitElement, using shadow dom',
baseElement: class ShadowElement extends LitElement {
render() {
return html`
<slot></slot>
`;
}
},
);
const tagWrapper = unsafeStatic(tagWrapperString);
const registerSpy = sinon.spy();
const el = await fixture(html`
<${tagWrapper} @form-element-register=${registerSpy}>
<form-registering></form-registering>
</${tagWrapper}>
`);
await el.registrationReady;
expect(el.formElements.length).to.equal(1);
});
it('requests update of the resetModelValue function of its parent formGroup', async () => {
const ParentFormGroupClass = class extends FormRegistrarMixin(LitElement) {
_updateResetModelValue() {
this.resetModelValue = 'foo';
}
};
const ChildFormGroupClass = class extends FormRegisteringMixin(LitElement) {
constructor() {
super();
this.__parentFormGroup = this.parentNode;
}
};
const parentClass = defineCE(ParentFormGroupClass);
const formGroup = unsafeStatic(parentClass);
const childClass = defineCE(ChildFormGroupClass);
const childFormGroup = unsafeStatic(childClass);
const parentFormEl = await fixture(html`
<${formGroup}><${childFormGroup} id="child" name="child[]"></${childFormGroup}></${formGroup}>
`);
expect(parentFormEl.resetModelValue).to.equal('foo');
});
it('can dynamically add/remove elements', async () => {
const el = await fixture(html`
<form-registrar>
<form-registering></form-registering>
</form-registrar>
`);
const newField = await fixture(html`
<form-registering></form-registering>
`);
expect(el.formElements.length).to.equal(1);
el.appendChild(newField);
expect(el.formElements.length).to.equal(2);
el.removeChild(newField);
expect(el.formElements.length).to.equal(1);
});
});

View file

@ -31,12 +31,12 @@ beforeEach(() => {
describe('<lion-field>', () => {
it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => {
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`);
expect(el.$$slot('input').id).to.equal(el._inputId);
});
it('fires focus/blur event on host and native input if focused/blurred', async () => {
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`);
const cbFocusHost = sinon.spy();
el.addEventListener('focus', cbFocusHost);
const cbFocusNativeInput = sinon.spy();
@ -68,33 +68,30 @@ describe('<lion-field>', () => {
expect(cbBlurNativeInput.callCount).to.equal(2);
});
it('offers simple getter "this.focused" returning true/false for the current focus state', async () => {
const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`);
expect(el.focused).to.equal(false);
await triggerFocusFor(el);
expect(el.focused).to.equal(true);
await triggerBlurFor(el);
expect(el.focused).to.equal(false);
});
it('can be disabled via attribute', async () => {
const elDisabled = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
const elDisabled = await fixture(html`<${tag} disabled>${inputSlot}</${tag}>`);
expect(elDisabled.disabled).to.equal(true);
expect(elDisabled.inputElement.disabled).to.equal(true);
});
it('can be disabled via property', async () => {
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`);
el.disabled = true;
await el.updateComplete;
expect(el.inputElement.disabled).to.equal(true);
});
// classes are added only for backward compatibility - they are deprecated
it('sets a state-disabled class when disabled', async () => {
const el = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
await el.updateComplete;
expect(el.classList.contains('state-disabled')).to.equal(true);
el.disabled = false;
await el.updateComplete;
expect(el.classList.contains('state-disabled')).to.equal(false);
});
it('can be cleared which erases value, validation and interaction states', async () => {
const el = await fixture(
`<${tagString} value="Some value from attribute">${inputSlotString}</${tagString}>`,
);
const el = await fixture(html`<${tag} value="Some value from attribute">${inputSlot}</${tag}>`);
el.clear();
expect(el.value).to.equal('');
el.value = 'Some value from property';
@ -103,35 +100,34 @@ describe('<lion-field>', () => {
expect(el.value).to.equal('');
});
it('can be reset which restores original modelValue', async () => {
const el = await fixture(html`
<${tag} .modelValue="${'foo'}">
${inputSlot}
</${tag}>`);
expect(el._initialModelValue).to.equal('foo');
el.modelValue = 'bar';
el.reset();
expect(el.modelValue).to.equal('foo');
});
it('reads initial value from attribute value', async () => {
const el = await fixture(`<${tagString} value="one">${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag} value="one">${inputSlot}</${tag}>`);
expect(el.$$slot('input').value).to.equal('one');
});
it('delegates value property', async () => {
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`);
expect(el.$$slot('input').value).to.equal('');
el.value = 'one';
expect(el.value).to.equal('one');
expect(el.$$slot('input').value).to.equal('one');
});
it('has a name which is reflected to an attribute and is synced down to the native input', async () => {
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
expect(el.name).to.equal('');
expect(el.getAttribute('name')).to.equal('');
expect(el.inputElement.getAttribute('name')).to.equal('');
el.name = 'foo';
await el.updateComplete;
expect(el.getAttribute('name')).to.equal('foo');
expect(el.inputElement.getAttribute('name')).to.equal('foo');
});
// TODO: find out if we could put all listeners on this.value (instead of this.inputElement.value)
// and make it act on this.value again
it('has a class "state-filled" if this.value is filled', async () => {
const el = await fixture(`<${tagString} value="filled">${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag} value="filled">${inputSlot}</${tag}>`);
expect(el.classList.contains('state-filled')).to.equal(true);
el.value = '';
await el.updateComplete;
@ -142,7 +138,7 @@ describe('<lion-field>', () => {
});
it('preserves the caret position on value change for native text fields (input|textarea)', async () => {
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`);
await triggerFocusFor(el);
await el.updateComplete;
el.inputElement.value = 'hello world';
@ -155,7 +151,7 @@ describe('<lion-field>', () => {
// TODO: add pointerEvents test for disabled
it('has a class "state-disabled"', async () => {
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`);
expect(el.classList.contains('state-disabled')).to.equal(false);
expect(el.inputElement.hasAttribute('disabled')).to.equal(false);
@ -166,7 +162,7 @@ describe('<lion-field>', () => {
expect(el.classList.contains('state-disabled')).to.equal(true);
expect(el.inputElement.hasAttribute('disabled')).to.equal(true);
const disabledel = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
const disabledel = await fixture(html`<${tag} disabled>${inputSlot}</${tag}>`);
expect(disabledel.classList.contains('state-disabled')).to.equal(true);
expect(disabledel.inputElement.hasAttribute('disabled')).to.equal(true);
});
@ -186,12 +182,12 @@ describe('<lion-field>', () => {
<div slot="feedback" id="feedback-[id]">[feedback] </span>
</lion-field>
~~~`, async () => {
const el = await fixture(`<${tagString}>
const el = await fixture(html`<${tag}>
<label slot="label">My Name</label>
${inputSlotString}
${inputSlot}
<span slot="help-text">Enter your Name</span>
<span slot="feedback">No name entered</span>
</${tagString}>
</${tag}>
`);
const nativeInput = el.$$slot('input');
@ -202,13 +198,13 @@ describe('<lion-field>', () => {
it(`allows additional slots (prefix, suffix, before, after) to be included in labelledby
(via attribute data-label) and in describedby (via attribute data-description)`, async () => {
const el = await fixture(`<${tagString}>
${inputSlotString}
const el = await fixture(html`<${tag}>
${inputSlot}
<span slot="before" data-label>[before]</span>
<span slot="after" data-label>[after]</span>
<span slot="prefix" data-description>[prefix]</span>
<span slot="suffix" data-description>[suffix]</span>
</${tagString}>
</${tag}>
`);
const nativeInput = el.$$slot('input');
@ -223,45 +219,45 @@ describe('<lion-field>', () => {
// TODO: put this test on FormControlMixin test once there
it(`allows to add to aria description or label via addToAriaLabel() and
addToAriaDescription()`, async () => {
const wrapper = await fixture(`
const wrapper = await fixture(html`
<div id="wrapper">
<${tagString}>
${inputSlotString}
<${tag}>
${inputSlot}
<label slot="label">Added to label by default</label>
<div slot="feedback">Added to description by default</div>
</${tagString}>
</${tag}>
<div id="additionalLabel"> This also needs to be read whenever the input has focus</div>
<div id="additionalDescription"> Same for this </div>
</div>`);
const el = wrapper.querySelector(`${tagString}`);
const el = wrapper.querySelector(tagString);
// wait until the field element is done rendering
await el.updateComplete;
await el.updateComplete;
const { inputElement } = el;
const get = by => inputElement.getAttribute(`aria-${by}`);
// 1. addToAriaLabel()
// Check if the aria attr is filled initially
expect(get('labelledby')).to.contain(`label-${el._inputId}`);
expect(inputElement.getAttribute('aria-labelledby')).to.contain(`label-${el._inputId}`);
el.addToAriaLabel('additionalLabel');
// Now check if ids are added to the end (not overridden)
expect(get('labelledby')).to.contain(`label-${el._inputId}`);
expect(inputElement.getAttribute('aria-labelledby')).to.contain(`label-${el._inputId}`);
// Should be placed in the end
expect(
get('labelledby').indexOf(`label-${el._inputId}`) <
get('labelledby').indexOf('additionalLabel'),
inputElement.getAttribute('aria-labelledby').indexOf(`label-${el._inputId}`) <
inputElement.getAttribute('aria-labelledby').indexOf('additionalLabel'),
);
// 2. addToAriaDescription()
// Check if the aria attr is filled initially
expect(get('describedby')).to.contain(`feedback-${el._inputId}`);
expect(inputElement.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`);
el.addToAriaDescription('additionalDescription');
// Now check if ids are added to the end (not overridden)
expect(get('describedby')).to.contain(`feedback-${el._inputId}`);
expect(inputElement.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`);
// Should be placed in the end
expect(
get('describedby').indexOf(`feedback-${el._inputId}`) <
get('describedby').indexOf('additionalDescription'),
inputElement.getAttribute('aria-describedby').indexOf(`feedback-${el._inputId}`) <
inputElement.getAttribute('aria-describedby').indexOf('additionalDescription'),
);
});
});
@ -285,7 +281,7 @@ describe('<lion-field>', () => {
function hasX(str) {
return { hasX: str.indexOf('x') > -1 };
}
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`);
const feedbackEl = el._feedbackElement;
el.modelValue = 'a@b.nl';
@ -355,17 +351,17 @@ describe('<lion-field>', () => {
describe(`Content projection${nameSuffix}`, () => {
it('renders correctly all slot elements in light DOM', async () => {
const el = await fixture(`
<${tagString}>
const el = await fixture(html`
<${tag}>
<label slot="label">[label]</label>
${inputSlotString}
${inputSlot}
<span slot="help-text">[help-text]</span>
<span slot="before">[before]</span>
<span slot="after">[after]</span>
<span slot="prefix">[prefix]</span>
<span slot="suffix">[suffix]</span>
<span slot="feedback">[feedback]</span>
</${tagString}>
</${tag}>
`);
const names = [
@ -388,9 +384,9 @@ describe('<lion-field>', () => {
});
});
describe(`Delegation${nameSuffix}`, () => {
describe('Delegation', () => {
it('delegates property value', async () => {
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`);
expect(el.inputElement.value).to.equal('');
el.value = 'one';
expect(el.value).to.equal('one');

View file

@ -11,7 +11,7 @@ const pascalCase = str => str.charAt(0).toUpperCase() + str.slice(1);
/**
* LionFieldset: fieldset wrapper providing extra features and integration with lion-field elements.
*
* @customElement
* @customElement lion-fieldset
* @extends LionLitElement
*/
export class LionFieldset extends FormRegistrarMixin(
@ -176,7 +176,14 @@ export class LionFieldset extends FormRegistrarMixin(
}
resetGroup() {
this.modelValue = this.resetModelValue;
this.formElementsArray.forEach(child => {
if (typeof child.resetGroup === 'function') {
child.resetGroup();
} else if (typeof child.reset === 'function') {
child.reset();
}
});
this.resetInteractionState();
}
@ -245,7 +252,7 @@ export class LionFieldset extends FormRegistrarMixin(
}
/**
* Get's triggered by event 'validatin-done' which enabled us to handle 2 different situations
* Gets triggered by event 'validation-done' 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 validatity states have changed, so fieldset needs to update itself based on that
@ -348,23 +355,16 @@ export class LionFieldset extends FormRegistrarMixin(
}
/**
* Updates the resetModelValue of this fieldset and asks it's parent fieldset/group to also
* update.
* This is needed as the upgrade order is not guaranteed. We have 3 main cases:
* 1. if `street-name` gets updated last then `address` and `details` needs to update their
* resetModelValue to also incorporate the correct value of `street-name`/`address`.
* 2. If `address` get updated last then it already has the correct `street-name` so it
* requests an update only for `details`.
* 3. If `details` get updated last nothing happens here as all data are up to date
*
* @example
* <lion-fieldset name="details">
* <lion-fieldset name="address">
* <lion-input name="street-name" .modelValue=${'street 1'}>
* Gathers initial model values of all children. Used
* when resetGroup() is called.
*/
_updateResetModelValue() {
this.resetModelValue = this.modelValue;
this._requestParentFormGroupUpdateOfResetModelValue();
get _initialModelValue() {
return this._getFromAllFormElements('_initialModelValue');
}
/** @deprecated */
get resetModelValue() {
return this._initialModelValue;
}
/**

View file

@ -14,12 +14,14 @@ import '../lion-fieldset.js';
const tagString = 'lion-fieldset';
const tag = unsafeStatic(tagString);
const inputSlotString = `
<lion-input name="gender[]"></lion-input>
<lion-input name="gender[]"></lion-input>
<lion-input name="color"></lion-input>
<lion-input name="hobbies[]"></lion-input>
<lion-input name="hobbies[]"></lion-input>
const childTagString = 'lion-input';
const childTag = unsafeStatic(childTagString);
const inputSlots = html`
<${childTag} name="gender[]"></${childTag}>
<${childTag} name="gender[]"></${childTag}>
<${childTag} name="color"></${childTag}>
<${childTag} name="hobbies[]"></${childTag}>
<${childTag} name="hobbies[]"></${childTag}>
`;
const nonPrefilledModelValue = '';
const prefilledModelValue = 'prefill';
@ -30,7 +32,7 @@ beforeEach(() => {
describe('<lion-fieldset>', () => {
it(`${tagString} has an up to date list of every form element in #formElements`, async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame();
expect(Object.keys(fieldset.formElements).length).to.equal(3);
expect(fieldset.formElements['hobbies[]'].length).to.equal(2);
@ -40,12 +42,12 @@ describe('<lion-fieldset>', () => {
});
it(`supports in html wrapped form elements`, async () => {
const el = await fixture(`
<lion-fieldset>
const el = await fixture(html`
<${tag}>
<div>
<lion-input name="foo"></lion-input>
<${childTag} name="foo"></${childTag}>
</div>
</lion-fieldset>
</${tag}>
`);
await nextFrame();
expect(el.formElementsArray.length).to.equal(1);
@ -54,7 +56,7 @@ describe('<lion-fieldset>', () => {
});
it('handles names with ending [] as an array', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame();
fieldset.formElements['gender[]'][0].modelValue = { value: 'male' };
fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
@ -75,7 +77,7 @@ describe('<lion-fieldset>', () => {
console.info = () => {};
let error = false;
const el = await fixture(`<lion-fieldset></lion-fieldset>`);
const el = await fixture(html`<${tag}></${tag}>`);
try {
// we test the api directly as errors thrown from a web component are in a
// different context and we can not catch them here => register fake elements
@ -94,7 +96,7 @@ describe('<lion-fieldset>', () => {
console.info = () => {};
let error = false;
const el = await fixture(`<lion-fieldset name="foo"></lion-fieldset>`);
const el = await fixture(html`<${tag} name="foo"></${tag}>`);
try {
// we test the api directly as errors thrown from a web component are in a
// different context and we can not catch them here => register fake elements
@ -113,7 +115,7 @@ describe('<lion-fieldset>', () => {
console.info = () => {};
let error = false;
const el = await fixture(`<lion-fieldset></lion-fieldset>`);
const el = await fixture(html`<${tag}></${tag}>`);
try {
// we test the api directly as errors thrown from a web component are in a
// different context and we can not catch them here => register fake elements
@ -132,8 +134,8 @@ describe('<lion-fieldset>', () => {
/* eslint-enable no-console */
it('can dynamically add/remove elements', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const newField = await fixture(`<lion-input name="lastName"></lion-input>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
const newField = await fixture(html`<${childTag} name="lastName"></${childTag}>`);
expect(Object.keys(fieldset.formElements).length).to.equal(3);
@ -145,11 +147,11 @@ describe('<lion-fieldset>', () => {
});
it('can read/write all values (of every input) via this.modelValue', async () => {
const fieldset = await fixture(`
<lion-fieldset>
<lion-input name="lastName"></lion-input>
<${tagString} name="newfieldset">${inputSlotString}</${tagString}>
</lion-fieldset>
const fieldset = await fixture(html`
<${tag}>
<${childTag} name="lastName"></${childTag}>
<${tag} name="newfieldset">${inputSlots}</${tag}>
</${tag}>
`);
await fieldset.registrationReady;
const newFieldset = fieldset.querySelector('lion-fieldset');
@ -190,10 +192,10 @@ describe('<lion-fieldset>', () => {
it('does not throw if setter data of this.modelValue can not be handled', async () => {
const el = await fixture(html`
<lion-fieldset>
<lion-input name="firstName" .modelValue=${'foo'}></lion-input>
<lion-input name="lastName" .modelValue=${'bar'}></lion-input>
</lion-fieldset>
<${tag}>
<${childTag} name="firstName" .modelValue=${'foo'}></${childTag}>
<${childTag} name="lastName" .modelValue=${'bar'}></${childTag}>
</${tag}>
`);
await nextFrame();
const initState = {
@ -210,7 +212,7 @@ describe('<lion-fieldset>', () => {
});
it('disables/enables all its formElements if it becomes disabled/enabled', async () => {
const el = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag} disabled>${inputSlots}</${tag}>`);
await nextFrame();
expect(el.formElements.color.disabled).to.equal(true);
expect(el.formElements['hobbies[]'][0].disabled).to.equal(true);
@ -225,7 +227,7 @@ describe('<lion-fieldset>', () => {
it('does not propagate/override initial disabled value on nested form elements', async () => {
const el = await fixture(
`<${tagString}><${tagString} name="sub" disabled>${inputSlotString}</${tagString}></${tagString}>`,
html`<${tag}><${tag} name="sub" disabled>${inputSlots}</${tag}></${tag}>`,
);
await el.updateComplete;
expect(el.disabled).to.equal(false);
@ -237,7 +239,7 @@ describe('<lion-fieldset>', () => {
// classes are added only for backward compatibility - they are deprecated
it('sets a state-disabled class when disabled', async () => {
const el = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
const el = await fixture(html`<${tag} disabled>${inputSlots}</${tag}>`);
await nextFrame();
expect(el.classList.contains('state-disabled')).to.equal(true);
el.disabled = false;
@ -252,10 +254,10 @@ describe('<lion-fieldset>', () => {
}
const el = await fixture(html`
<${tag}>
<lion-input name="color"
<${childTag} name="color"
.errorValidators=${[[isCat]]}
.modelValue=${'blue'}
></lion-input>
></${childTag}>
</${tag}>
`);
await nextFrame();
@ -263,7 +265,7 @@ describe('<lion-fieldset>', () => {
});
it('validates when a value changes', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame();
const spy = sinon.spy(fieldset, 'validate');
fieldset.formElements.color.modelValue = { checked: true, value: 'red' };
@ -277,10 +279,10 @@ describe('<lion-fieldset>', () => {
const el = await fixture(html`
<${tag}>
<lion-input name="color"
<${childTag} name="color"
.errorValidators=${[[isCat]]}
.modelValue=${'blue'}
></lion-input>
></${childTag}>
</${tag}>
`);
await nextFrame();
@ -297,12 +299,12 @@ describe('<lion-fieldset>', () => {
}
const el = await fixture(html`
<${tag} .errorValidators=${[[hasEvenNumberOfChildren]]}>
<lion-input id="c1" name="c1"></lion-input>
<${childTag} id="c1" name="c1"></${childTag}>
</${tag}>
`);
const child2 = await fixture(
html`
<lion-input name="c2"></lion-input>
<${childTag} name="c2"></${childTag}>
`,
);
@ -326,7 +328,7 @@ describe('<lion-fieldset>', () => {
describe('interaction states', () => {
it('has false states (dirty, touched, prefilled) on init', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame();
expect(fieldset.dirty).to.equal(false, 'dirty');
expect(fieldset.touched).to.equal(false, 'touched');
@ -334,14 +336,14 @@ describe('<lion-fieldset>', () => {
});
it('sets dirty when value changed', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame();
fieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'football' };
expect(fieldset.dirty).to.equal(true);
});
it('sets touched when field left after focus', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame();
await triggerFocusFor(fieldset.formElements['gender[]'][0].inputElement);
await triggerBlurFor(fieldset.formElements['gender[]'][0].inputElement);
@ -349,7 +351,7 @@ describe('<lion-fieldset>', () => {
});
it('sets a class "state-(touched|dirty)"', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame();
fieldset.formElements.color.touched = true;
await fieldset.updateComplete;
@ -364,7 +366,7 @@ describe('<lion-fieldset>', () => {
});
it('sets prefilled when field left and value non-empty', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
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: 'football' };
@ -385,17 +387,17 @@ describe('<lion-fieldset>', () => {
it('sets prefilled once instantiated', async () => {
// no prefilled when nothing has value
const fieldsetNotPrefilled = await fixture(html`<${tag}>${inputSlotString}</${tag}>`);
const fieldsetNotPrefilled = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
expect(fieldsetNotPrefilled.prefilled).to.equal(false, 'not prefilled on init');
// prefilled when at least one child has value
const fieldsetPrefilled = await fixture(html`
<${tag}>
<lion-input name="gender[]" .modelValue=${prefilledModelValue}></lion-input>
<lion-input name="gender[]"></lion-input>
<lion-input name="color"></lion-input>
<lion-input name="hobbies[]"></lion-input>
<lion-input name="hobbies[]"></lion-input>
<${childTag} name="gender[]" .modelValue=${prefilledModelValue}></${childTag}>
<${childTag} name="gender[]"></${childTag}>
<${childTag} name="color"></${childTag}>
<${childTag} name="hobbies[]"></${childTag}>
<${childTag} name="hobbies[]"></${childTag}>
</${tag}>
`);
await nextFrame();
@ -405,7 +407,7 @@ describe('<lion-fieldset>', () => {
describe('serialize', () => {
it('use form elements serializedValue', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame();
fieldset.formElements['hobbies[]'][0].serializer = v => `${v.value}-serialized`;
fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'Bar' };
@ -422,7 +424,7 @@ describe('<lion-fieldset>', () => {
});
it('form elements which are not disabled', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame();
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
fieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'football' };
@ -445,11 +447,11 @@ describe('<lion-fieldset>', () => {
});
it('allows for nested fieldsets', async () => {
const fieldset = await fixture(`
<lion-fieldset name="userData">
<lion-input name="comment"></lion-input>
<${tagString} name="newfieldset">${inputSlotString}</${tagString}>
</lion-fieldset>
const fieldset = await fixture(html`
<${tag} name="userData">
<${childTag} name="comment"></${childTag}>
<${tag} name="newfieldset">${inputSlots}</${tag}>
</${tag}>
`);
await nextFrame();
const newFieldset = fieldset.querySelector('lion-fieldset');
@ -472,11 +474,11 @@ describe('<lion-fieldset>', () => {
});
it('will exclude form elements within an disabled fieldset', async () => {
const fieldset = await fixture(`
<lion-fieldset name="userData">
<lion-input name="comment"></lion-input>
<${tagString} name="newfieldset">${inputSlotString}</${tagString}>
</lion-fieldset>
const fieldset = await fixture(html`
<${tag} name="userData">
<${childTag} name="comment"></${childTag}>
<${tag} name="newfieldset">${inputSlots}</${tag}>
</${tag}>
`);
await nextFrame();
@ -509,7 +511,7 @@ describe('<lion-fieldset>', () => {
});
it('treats names with ending [] as arrays', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
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' };
@ -524,11 +526,11 @@ describe('<lion-fieldset>', () => {
});
it('does not serialize undefined values (nb radios/checkboxes are always serialized)', async () => {
const fieldset = await fixture(`
<lion-fieldset>
<lion-input name="custom[]"></lion-input>
<lion-input name="custom[]"></lion-input>
</lion-fieldset>
const fieldset = await fixture(html`
<${tag}>
<${childTag} name="custom[]"></${childTag}>
<${childTag} name="custom[]"></${childTag}>
</${tag}>
`);
await nextFrame();
fieldset.formElements['custom[]'][0].modelValue = 'custom 1';
@ -543,9 +545,9 @@ describe('<lion-fieldset>', () => {
describe('reset', () => {
it('restores default values if changes were made', async () => {
const el = await fixture(html`
<lion-fieldset>
<lion-input id="firstName" name="firstName" .modelValue="${'Foo'}"></lion-input>
</lion-fieldset>
<${tag}>
<${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"></${childTag}>
</${tag}>
`);
await el.querySelector('lion-input').updateComplete;
@ -562,9 +564,9 @@ describe('<lion-fieldset>', () => {
it('restores default values of arrays if changes were made', async () => {
const el = await fixture(html`
<lion-fieldset>
<lion-input id="firstName" name="firstName[]" .modelValue="${'Foo'}"></lion-input>
</lion-fieldset>
<${tag}>
<${childTag} id="firstName" name="firstName[]" .modelValue="${'Foo'}"></${childTag}>
</${tag}>
`);
await el.querySelector('lion-input').updateComplete;
@ -581,11 +583,11 @@ describe('<lion-fieldset>', () => {
it('restores default values of a nested fieldset if changes were made', async () => {
const el = await fixture(html`
<lion-fieldset>
<lion-fieldset id="name" name="name[]">
<lion-input id="firstName" name="firstName" .modelValue="${'Foo'}"></lion-input>
</lion-fieldset>
</lion-fieldset>
<${tag}>
<${tag} id="name" name="name[]">
<${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"></${childTag}>
</${tag}>
</${tag}>
`);
await Promise.all([
el.querySelector('lion-fieldset').updateComplete,
@ -607,7 +609,7 @@ describe('<lion-fieldset>', () => {
});
it('clears interaction state', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame();
// Safety check initially
fieldset._setValueForAllFormElements('dirty', true);
@ -636,7 +638,7 @@ describe('<lion-fieldset>', () => {
});
it('clears submitted state', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame();
fieldset.submitted = true;
fieldset.resetGroup();
@ -656,8 +658,8 @@ describe('<lion-fieldset>', () => {
const el = await fixture(html`
<${tag} .errorValidators=${[[containsA]]}>
<lion-input name="color" .errorValidators=${[[isCat]]}></lion-input>
<lion-input name="color2"></lion-input>
<${childTag} name="color" .errorValidators=${[[isCat]]}></${childTag}>
<${childTag} name="color2"></${childTag}>
</${tag}>
`);
await el.registrationReady;
@ -678,6 +680,68 @@ describe('<lion-fieldset>', () => {
expect(el.error.containsA).to.be.true;
expect(el.formElements.color.errorState).to.be.false;
});
it('has access to `_initialModelValue` based on initial children states', async () => {
const el = await fixture(html`
<${tag}>
<${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}>
<${childTag} name="child[]" .modelValue="${'bar1'}">
</${childTag}>
</${tag}>
`);
await el.updateComplete;
el.modelValue['child[]'] = ['foo2', 'bar2'];
expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']);
});
it('does not wrongly recompute `_initialModelValue` after dynamic changes of children', async () => {
const el = await fixture(html`
<${tag}>
<${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}>
</${tag}>
`);
el.modelValue['child[]'] = ['foo2'];
const childEl = await fixture(html`
<${childTag} name="child[]" .modelValue="${'bar1'}">
</${childTag}>
`);
el.appendChild(childEl);
expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']);
});
describe('resetGroup method', () => {
it('calls resetGroup on children fieldsets', async () => {
const el = await fixture(html`
<${tag} name="parentFieldset">
<${tag} name="childFieldset">
<${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}>
</${tag}>
</${tag}>
`);
const childFieldsetEl = el.querySelector(tagString);
const resetGroupSpy = sinon.spy(childFieldsetEl, 'resetGroup');
el.resetGroup();
expect(resetGroupSpy.callCount).to.equal(1);
});
it('calls reset on children fields', async () => {
const el = await fixture(html`
<${tag} name="parentFieldset">
<${tag} name="childFieldset">
<${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}>
</${tag}>
</${tag}>
`);
const childFieldsetEl = el.querySelector(childTagString);
const resetSpy = sinon.spy(childFieldsetEl, 'reset');
el.resetGroup();
expect(resetSpy.callCount).to.equal(1);
});
});
});
describe('a11y', () => {
@ -686,7 +750,7 @@ describe('<lion-fieldset>', () => {
// });
it('has role="group" set', async () => {
const fieldset = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
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' };
@ -698,11 +762,11 @@ describe('<lion-fieldset>', () => {
});
it('has an aria-labelledby from element with slot="label"', async () => {
const el = await fixture(`
<${tagString}>
const el = await fixture(html`
<${tag}>
<label slot="label">My Label</label>
${inputSlotString}
</${tagString}>
${inputSlots}
</${tag}>
`);
const label = el.querySelector('[slot="label"]');
expect(el.hasAttribute('aria-labelledby')).to.equal(true);
@ -724,40 +788,40 @@ describe('<lion-fieldset>', () => {
childAriaFixture = async (
msgSlotType = 'feedback', // eslint-disable-line no-shadow
) => {
const dom = await fixture(`
<lion-fieldset name="l1_g">
<lion-input name="l1_fa">
const dom = await fixture(html`
<${tag} name="l1_g">
<${childTag} name="l1_fa">
<div slot="${msgSlotType}" id="msg_l1_fa"></div>
<!-- field referred by: #msg_l1_fa (local), #msg_l1_g (parent/group) -->
</lion-input>
</${childTag}>
<lion-input name="l1_fb">
<${childTag} name="l1_fb">
<div slot="${msgSlotType}" id="msg_l1_fb"></div>
<!-- field referred by: #msg_l1_fb (local), #msg_l1_g (parent/group) -->
</lion-input>
</${childTag}>
<!-- [ INNER FIELDSET ] -->
<lion-fieldset name="l2_g">
<lion-input name="l2_fa">
<${tag} name="l2_g">
<${childTag} name="l2_fa">
<div slot="${msgSlotType}" id="msg_l2_fa"></div>
<!-- field referred by: #msg_l2_fa (local), #msg_l2_g (parent/group), #msg_l1_g (grandparent/group.group) -->
</lion-input>
</${childTag}>
<lion-input name="l2_fb">
<${childTag} name="l2_fb">
<div slot="${msgSlotType}" id="msg_l2_fb"></div>
<!-- field referred by: #msg_l2_fb (local), #msg_l2_g (parent/group), #msg_l1_g (grandparent/group.group) -->
</lion-input>
</${childTag}>
<div slot="${msgSlotType}" id="msg_l2_g"></div>
<!-- group referred by: #msg_l2_g (local), #msg_l1_g (parent/group) -->
</lion-fieldset>
</${tag}>
<!-- [ / INNER FIELDSET ] -->
<div slot="${msgSlotType}" id="msg_l1_g"></div>
<!-- group referred by: #msg_l1_g (local) -->
</lion-fieldset>
</${tag}>
`);
return dom;
};