From d2035e6a3f9fb573e908e2c5420e6aff1dad1f3d Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Tue, 13 Aug 2019 08:38:39 +0200 Subject: [PATCH 1/5] feat(field): add reset method and capture inital model value --- packages/field/src/LionField.js | 10 +++ packages/field/test/lion-field.test.js | 118 ++++++++++++------------- 2 files changed, 68 insertions(+), 60 deletions(-) diff --git a/packages/field/src/LionField.js b/packages/field/src/LionField.js index f9abd5f2d..90b8b2ecb 100644 --- a/packages/field/src/LionField.js +++ b/packages/field/src/LionField.js @@ -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, @@ -164,6 +169,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 diff --git a/packages/field/test/lion-field.test.js b/packages/field/test/lion-field.test.js index 053da22e9..19dd00d00 100644 --- a/packages/field/test/lion-field.test.js +++ b/packages/field/test/lion-field.test.js @@ -31,12 +31,12 @@ beforeEach(() => { describe('', () => { it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => { - const el = await fixture(`<${tagString}>${inputSlotString}`); + const el = await fixture(html`<${tag}>${inputSlot}`); 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}`); + const el = await fixture(html`<${tag}>${inputSlot}`); const cbFocusHost = sinon.spy(); el.addEventListener('focus', cbFocusHost); const cbFocusNativeInput = sinon.spy(); @@ -68,32 +68,31 @@ describe('', () => { 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}`); + 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}`); + const elDisabled = await fixture(html`<${tag} disabled>${inputSlot}`); 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}`); + const el = await fixture(html`<${tag}>${inputSlot}`); 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}`); - 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}`, + html`<${tag} value="Some value from attribute">${inputSlot}`, ); el.clear(); expect(el.value).to.equal(''); @@ -103,35 +102,34 @@ describe('', () => { expect(el.value).to.equal(''); }); + it('can be reset which restores original modelValue', async () => { + const el = await fixture(html` + <${tag} .modelValue="${'foo'}"> + ${inputSlot} + `); + 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}`); + const el = await fixture(html`<${tag} value="one">${inputSlot}`); expect(el.$$slot('input').value).to.equal('one'); }); it('delegates value property', async () => { - const el = await fixture(`<${tagString}>${inputSlotString}`); + const el = await fixture(html`<${tag}>${inputSlot}`); 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}`); - 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}`); + const el = await fixture(html`<${tag} value="filled">${inputSlot}`); expect(el.classList.contains('state-filled')).to.equal(true); el.value = ''; await el.updateComplete; @@ -142,7 +140,7 @@ describe('', () => { }); it('preserves the caret position on value change for native text fields (input|textarea)', async () => { - const el = await fixture(`<${tagString}>${inputSlotString}`); + const el = await fixture(html`<${tag}>${inputSlot}`); await triggerFocusFor(el); await el.updateComplete; el.inputElement.value = 'hello world'; @@ -155,7 +153,7 @@ describe('', () => { // TODO: add pointerEvents test for disabled it('has a class "state-disabled"', async () => { - const el = await fixture(`<${tagString}>${inputSlotString}`); + const el = await fixture(html`<${tag}>${inputSlot}`); expect(el.classList.contains('state-disabled')).to.equal(false); expect(el.inputElement.hasAttribute('disabled')).to.equal(false); @@ -166,7 +164,7 @@ describe('', () => { expect(el.classList.contains('state-disabled')).to.equal(true); expect(el.inputElement.hasAttribute('disabled')).to.equal(true); - const disabledel = await fixture(`<${tagString} disabled>${inputSlotString}`); + const disabledel = await fixture(html`<${tag} disabled>${inputSlot}`); expect(disabledel.classList.contains('state-disabled')).to.equal(true); expect(disabledel.inputElement.hasAttribute('disabled')).to.equal(true); }); @@ -186,12 +184,12 @@ describe('', () => {
[feedback] ~~~`, async () => { - const el = await fixture(`<${tagString}> + const el = await fixture(html`<${tag}> - ${inputSlotString} + ${inputSlot} Enter your Name No name entered - + `); const nativeInput = el.$$slot('input'); @@ -202,13 +200,13 @@ describe('', () => { 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} [before] [after] [prefix] [suffix] - + `); const nativeInput = el.$$slot('input'); @@ -223,45 +221,45 @@ describe('', () => { // 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`
- <${tagString}> - ${inputSlotString} + <${tag}> + ${inputSlot}
Added to description by default
- +
This also needs to be read whenever the input has focus
Same for this
`); - 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 +283,7 @@ describe('', () => { function hasX(str) { return { hasX: str.indexOf('x') > -1 }; } - const el = await fixture(`<${tagString}>${inputSlotString}`); + const el = await fixture(html`<${tag}>${inputSlot}`); const feedbackEl = el._feedbackElement; el.modelValue = 'a@b.nl'; @@ -355,17 +353,17 @@ describe('', () => { 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}> - ${inputSlotString} + ${inputSlot} [help-text] [before] [after] [prefix] [suffix] [feedback] - + `); const names = [ @@ -388,9 +386,9 @@ describe('', () => { }); }); - describe(`Delegation${nameSuffix}`, () => { + describe('Delegation', () => { it('delegates property value', async () => { - const el = await fixture(`<${tagString}>${inputSlotString}`); + const el = await fixture(html`<${tag}>${inputSlot}`); expect(el.inputElement.value).to.equal(''); el.value = 'one'; expect(el.value).to.equal('one'); From 995e8f99dea8ad2c82e9720967c4336235eb13c6 Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Tue, 13 Aug 2019 08:45:26 +0200 Subject: [PATCH 2/5] chore(field): created test suite for form registrations --- .../FormRegistrationMixins.suite.js | 149 ++++++++++++++++++ .../field/test/FormRegistrationMixins.test.js | 144 ++--------------- 2 files changed, 166 insertions(+), 127 deletions(-) create mode 100644 packages/field/test-suites/FormRegistrationMixins.suite.js diff --git a/packages/field/test-suites/FormRegistrationMixins.suite.js b/packages/field/test-suites/FormRegistrationMixins.suite.js new file mode 100644 index 000000000..948e6dc17 --- /dev/null +++ b/packages/field/test-suites/FormRegistrationMixins.suite.js @@ -0,0 +1,149 @@ +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}> + + `); + 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}> + + + `); + 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} + + `); + + const secondRegistrar = await fixture(html` + <${parentTag}> + <${childTag} + `); + + 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` + + `; + } + }, + ); + const tagWrapper = unsafeStatic(tagWrapperString); + const el = await fixture(html` + <${tagWrapper}> + <${childTag}> + + `); + await el.registrationReady; + expect(el.formElements.length).to.equal(1); + }); + + it('can dynamically add/remove elements', async () => { + const el = await fixture(html` + <${parentTag}> + <${childTag}> + + `); + const newField = await fixture(html` + <${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); + }); + + describe('Unregister', () => { + it.skip('requests update of the resetModelValue function of its parent formGroup on unregister', async () => { + const ParentFormGroupClass = class extends FormRegistrarMixin(LitElement) { + _updateResetModelValue() { + this.resetModelValue = this.formElements.length; + } + }; + const ChildFormGroupClass = class extends FormRegisteringMixin(LitElement) { + constructor() { + super(); + this.__parentFormGroup = this.parentNode; + } + }; + + const formGroupTag = unsafeStatic(defineCE(ParentFormGroupClass)); + const childFormGroupTag = unsafeStatic(defineCE(ChildFormGroupClass)); + const parentFormEl = await fixture(html` + <${formGroupTag}> + <${childFormGroupTag} name="child[]"> + <${childFormGroupTag} name="child[]"> + + `); + expect(parentFormEl.resetModelValue.length).to.equal(2); + parentFormEl.removeChild(parentFormEl.children[0]); + expect(parentFormEl.resetModelValue.length).to.equal(1); + }); + }); + }); +}; diff --git a/packages/field/test/FormRegistrationMixins.test.js b/packages/field/test/FormRegistrationMixins.test.js index 19802c592..932d7d6b8 100644 --- a/packages/field/test/FormRegistrationMixins.test.js +++ b/packages/field/test/FormRegistrationMixins.test.js @@ -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` - - - - `); - await el.registrationReady; - expect(el.formElements.length).to.equal(1); - }); - - it('supports nested registrar', async () => { - const el = await fixture(html` - - - - - - `); - 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` - - - - - - `); - - const secondRegistrar = await fixture(html` - - - - `); - - 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(); - } - - render() { - return html` - - `; - } - }, - ); - const tagWrapper = unsafeStatic(tagWrapperString); - const registerSpy = sinon.spy(); - const el = await fixture(html` - <${tagWrapper} @form-element-register=${registerSpy}> - - - `); - 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[]"> - `); - expect(parentFormEl.resetModelValue).to.equal('foo'); - }); - - it('can dynamically add/remove elements', async () => { - const el = await fixture(html` - - - - `); - const newField = await fixture(html` - - `); - - 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); - }); +runRegistrationSuite({ + suffix: 'with UpdatingElement', + baseElement: UpdatingElement, +}); + +runRegistrationSuite({ + suffix: 'with LitElement, using shadow dom', + baseElement: class ShadowElement extends LitElement { + render() { + return html` + + `; + } + }, }); From 085895ee9416244777457607a7abbfc4a368a54c Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Tue, 13 Aug 2019 08:48:01 +0200 Subject: [PATCH 3/5] fix(fieldset): reset / inital modelValue always accurate --- packages/fieldset/src/LionFieldset.js | 38 +-- packages/fieldset/test/lion-fieldset.test.js | 260 ++++++++++++------- 2 files changed, 181 insertions(+), 117 deletions(-) diff --git a/packages/fieldset/src/LionFieldset.js b/packages/fieldset/src/LionFieldset.js index c958f11f8..531a092b5 100644 --- a/packages/fieldset/src/LionFieldset.js +++ b/packages/fieldset/src/LionFieldset.js @@ -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 - * - * - * + * 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; } /** diff --git a/packages/fieldset/test/lion-fieldset.test.js b/packages/fieldset/test/lion-fieldset.test.js index ae9b25219..c5c436d6e 100644 --- a/packages/fieldset/test/lion-fieldset.test.js +++ b/packages/fieldset/test/lion-fieldset.test.js @@ -14,12 +14,14 @@ import '../lion-fieldset.js'; const tagString = 'lion-fieldset'; const tag = unsafeStatic(tagString); -const inputSlotString = ` - - - - - +const childTagString = 'lion-input'; +const childTag = unsafeStatic(childTagString); +const inputSlots = html` + <${childTag} name="gender[]"> + <${childTag} name="gender[]"> + <${childTag} name="color"> + <${childTag} name="hobbies[]"> + <${childTag} name="hobbies[]"> `; const nonPrefilledModelValue = ''; const prefilledModelValue = 'prefill'; @@ -30,7 +32,7 @@ beforeEach(() => { describe('', () => { it(`${tagString} has an up to date list of every form element in #formElements`, async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); expect(Object.keys(fieldset.formElements).length).to.equal(3); expect(fieldset.formElements['hobbies[]'].length).to.equal(2); @@ -40,12 +42,12 @@ describe('', () => { }); it(`supports in html wrapped form elements`, async () => { - const el = await fixture(` - + const el = await fixture(html` + <${tag}>
- + <${childTag} name="foo">
-
+ `); await nextFrame(); expect(el.formElementsArray.length).to.equal(1); @@ -54,7 +56,7 @@ describe('', () => { }); it('handles names with ending [] as an array', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); fieldset.formElements['gender[]'][0].modelValue = { value: 'male' }; fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; @@ -75,7 +77,7 @@ describe('', () => { console.info = () => {}; let error = false; - const el = await fixture(``); + const el = await fixture(html`<${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('', () => { console.info = () => {}; let error = false; - const el = await fixture(``); + const el = await fixture(html`<${tag} name="foo">`); 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('', () => { console.info = () => {}; let error = false; - const el = await fixture(``); + const el = await fixture(html`<${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('', () => { /* eslint-enable no-console */ it('can dynamically add/remove elements', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); - const newField = await fixture(``); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); + const newField = await fixture(html`<${childTag} name="lastName">`); expect(Object.keys(fieldset.formElements).length).to.equal(3); @@ -145,11 +147,11 @@ describe('', () => { }); it('can read/write all values (of every input) via this.modelValue', async () => { - const fieldset = await fixture(` - - - <${tagString} name="newfieldset">${inputSlotString} - + const fieldset = await fixture(html` + <${tag}> + <${childTag} name="lastName"> + <${tag} name="newfieldset">${inputSlots} + `); await fieldset.registrationReady; const newFieldset = fieldset.querySelector('lion-fieldset'); @@ -190,10 +192,10 @@ describe('', () => { it('does not throw if setter data of this.modelValue can not be handled', async () => { const el = await fixture(html` - - - - + <${tag}> + <${childTag} name="firstName" .modelValue=${'foo'}> + <${childTag} name="lastName" .modelValue=${'bar'}> + `); await nextFrame(); const initState = { @@ -210,7 +212,7 @@ describe('', () => { }); it('disables/enables all its formElements if it becomes disabled/enabled', async () => { - const el = await fixture(`<${tagString} disabled>${inputSlotString}`); + const el = await fixture(html`<${tag} disabled>${inputSlots}`); await nextFrame(); expect(el.formElements.color.disabled).to.equal(true); expect(el.formElements['hobbies[]'][0].disabled).to.equal(true); @@ -225,7 +227,7 @@ describe('', () => { it('does not propagate/override initial disabled value on nested form elements', async () => { const el = await fixture( - `<${tagString}><${tagString} name="sub" disabled>${inputSlotString}`, + html`<${tag}><${tag} name="sub" disabled>${inputSlots}`, ); await el.updateComplete; expect(el.disabled).to.equal(false); @@ -237,7 +239,7 @@ describe('', () => { // 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}`); + const el = await fixture(html`<${tag} disabled>${inputSlots}`); await nextFrame(); expect(el.classList.contains('state-disabled')).to.equal(true); el.disabled = false; @@ -252,10 +254,10 @@ describe('', () => { } const el = await fixture(html` <${tag}> - + > `); await nextFrame(); @@ -263,7 +265,7 @@ describe('', () => { }); it('validates when a value changes', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); const spy = sinon.spy(fieldset, 'validate'); fieldset.formElements.color.modelValue = { checked: true, value: 'red' }; @@ -277,10 +279,10 @@ describe('', () => { const el = await fixture(html` <${tag}> - + > `); await nextFrame(); @@ -297,12 +299,12 @@ describe('', () => { } const el = await fixture(html` <${tag} .errorValidators=${[[hasEvenNumberOfChildren]]}> - + <${childTag} id="c1" name="c1"> `); const child2 = await fixture( html` - + <${childTag} name="c2"> `, ); @@ -326,7 +328,7 @@ describe('', () => { describe('interaction states', () => { it('has false states (dirty, touched, prefilled) on init', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); expect(fieldset.dirty).to.equal(false, 'dirty'); expect(fieldset.touched).to.equal(false, 'touched'); @@ -334,14 +336,14 @@ describe('', () => { }); it('sets dirty when value changed', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); 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}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); await triggerFocusFor(fieldset.formElements['gender[]'][0].inputElement); await triggerBlurFor(fieldset.formElements['gender[]'][0].inputElement); @@ -349,7 +351,7 @@ describe('', () => { }); it('sets a class "state-(touched|dirty)"', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); fieldset.formElements.color.touched = true; await fieldset.updateComplete; @@ -364,7 +366,7 @@ describe('', () => { }); it('sets prefilled when field left and value non-empty', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); 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('', () => { it('sets prefilled once instantiated', async () => { // no prefilled when nothing has value - const fieldsetNotPrefilled = await fixture(html`<${tag}>${inputSlotString}`); + const fieldsetNotPrefilled = await fixture(html`<${tag}>${inputSlots}`); expect(fieldsetNotPrefilled.prefilled).to.equal(false, 'not prefilled on init'); // prefilled when at least one child has value const fieldsetPrefilled = await fixture(html` <${tag}> - - - - - + <${childTag} name="gender[]" .modelValue=${prefilledModelValue}> + <${childTag} name="gender[]"> + <${childTag} name="color"> + <${childTag} name="hobbies[]"> + <${childTag} name="hobbies[]"> `); await nextFrame(); @@ -405,7 +407,7 @@ describe('', () => { describe('serialize', () => { it('use form elements serializedValue', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); 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('', () => { }); it('form elements which are not disabled', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); fieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; fieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'football' }; @@ -445,11 +447,11 @@ describe('', () => { }); it('allows for nested fieldsets', async () => { - const fieldset = await fixture(` - - - <${tagString} name="newfieldset">${inputSlotString} - + const fieldset = await fixture(html` + <${tag} name="userData"> + <${childTag} name="comment"> + <${tag} name="newfieldset">${inputSlots} + `); await nextFrame(); const newFieldset = fieldset.querySelector('lion-fieldset'); @@ -472,11 +474,11 @@ describe('', () => { }); it('will exclude form elements within an disabled fieldset', async () => { - const fieldset = await fixture(` - - - <${tagString} name="newfieldset">${inputSlotString} - + const fieldset = await fixture(html` + <${tag} name="userData"> + <${childTag} name="comment"> + <${tag} name="newfieldset">${inputSlots} + `); await nextFrame(); @@ -509,7 +511,7 @@ describe('', () => { }); it('treats names with ending [] as arrays', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); 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('', () => { }); it('does not serialize undefined values (nb radios/checkboxes are always serialized)', async () => { - const fieldset = await fixture(` - - - - + const fieldset = await fixture(html` + <${tag}> + <${childTag} name="custom[]"> + <${childTag} name="custom[]"> + `); await nextFrame(); fieldset.formElements['custom[]'][0].modelValue = 'custom 1'; @@ -543,9 +545,9 @@ describe('', () => { describe('reset', () => { it('restores default values if changes were made', async () => { const el = await fixture(html` - - - + <${tag}> + <${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"> + `); await el.querySelector('lion-input').updateComplete; @@ -562,9 +564,9 @@ describe('', () => { it('restores default values of arrays if changes were made', async () => { const el = await fixture(html` - - - + <${tag}> + <${childTag} id="firstName" name="firstName[]" .modelValue="${'Foo'}"> + `); await el.querySelector('lion-input').updateComplete; @@ -581,11 +583,11 @@ describe('', () => { it('restores default values of a nested fieldset if changes were made', async () => { const el = await fixture(html` - - - - - + <${tag}> + <${tag} id="name" name="name[]"> + <${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"> + + `); await Promise.all([ el.querySelector('lion-fieldset').updateComplete, @@ -607,7 +609,7 @@ describe('', () => { }); it('clears interaction state', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); // Safety check initially fieldset._setValueForAllFormElements('dirty', true); @@ -636,7 +638,7 @@ describe('', () => { }); it('clears submitted state', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); fieldset.submitted = true; fieldset.resetGroup(); @@ -656,8 +658,8 @@ describe('', () => { const el = await fixture(html` <${tag} .errorValidators=${[[containsA]]}> - - + <${childTag} name="color" .errorValidators=${[[isCat]]}> + <${childTag} name="color2"> `); await el.registrationReady; @@ -678,6 +680,68 @@ describe('', () => { 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} name="child[]" .modelValue="${'bar1'}"> + + + `); + 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'}"> + + + `); + el.modelValue['child[]'] = ['foo2']; + const childEl = await fixture(html` + <${childTag} name="child[]" .modelValue="${'bar1'}"> + + `); + 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'}"> + + + + `); + 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'}"> + + + + `); + 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('', () => { // }); it('has role="group" set', async () => { - const fieldset = await fixture(`<${tagString}>${inputSlotString}`); + const fieldset = await fixture(html`<${tag}>${inputSlots}`); 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('', () => { }); it('has an aria-labelledby from element with slot="label"', async () => { - const el = await fixture(` - <${tagString}> + const el = await fixture(html` + <${tag}> - ${inputSlotString} - + ${inputSlots} + `); const label = el.querySelector('[slot="label"]'); expect(el.hasAttribute('aria-labelledby')).to.equal(true); @@ -724,40 +788,40 @@ describe('', () => { childAriaFixture = async ( msgSlotType = 'feedback', // eslint-disable-line no-shadow ) => { - const dom = await fixture(` - - + const dom = await fixture(html` + <${tag} name="l1_g"> + <${childTag} name="l1_fa">
-
+ - + <${childTag} name="l1_fb">
-
+ - - + <${tag} name="l2_g"> + <${childTag} name="l2_fa">
-
+ - + <${childTag} name="l2_fb">
-
+
-
+
-
+ `); return dom; }; From 95a9ce7b0beb45815931f3bca20d51c712009079 Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Tue, 13 Aug 2019 09:00:49 +0200 Subject: [PATCH 4/5] fix(field): cleaned up old register code --- packages/field/src/FormRegisteringMixin.js | 20 ++----------- packages/field/src/LionField.js | 8 ------ .../FormRegistrationMixins.suite.js | 28 ------------------- 3 files changed, 2 insertions(+), 54 deletions(-) diff --git a/packages/field/src/FormRegisteringMixin.js b/packages/field/src/FormRegisteringMixin.js index a5b92e975..700cf2a7d 100644 --- a/packages/field/src/FormRegisteringMixin.js +++ b/packages/field/src/FormRegisteringMixin.js @@ -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(); - } - } }, ); diff --git a/packages/field/src/LionField.js b/packages/field/src/LionField.js index 90b8b2ecb..ba4cf1bf3 100644 --- a/packages/field/src/LionField.js +++ b/packages/field/src/LionField.js @@ -122,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); } diff --git a/packages/field/test-suites/FormRegistrationMixins.suite.js b/packages/field/test-suites/FormRegistrationMixins.suite.js index 948e6dc17..5db8bb5dd 100644 --- a/packages/field/test-suites/FormRegistrationMixins.suite.js +++ b/packages/field/test-suites/FormRegistrationMixins.suite.js @@ -117,33 +117,5 @@ export const runRegistrationSuite = customConfig => { el.removeChild(newField); expect(el.formElements.length).to.equal(1); }); - - describe('Unregister', () => { - it.skip('requests update of the resetModelValue function of its parent formGroup on unregister', async () => { - const ParentFormGroupClass = class extends FormRegistrarMixin(LitElement) { - _updateResetModelValue() { - this.resetModelValue = this.formElements.length; - } - }; - const ChildFormGroupClass = class extends FormRegisteringMixin(LitElement) { - constructor() { - super(); - this.__parentFormGroup = this.parentNode; - } - }; - - const formGroupTag = unsafeStatic(defineCE(ParentFormGroupClass)); - const childFormGroupTag = unsafeStatic(defineCE(ChildFormGroupClass)); - const parentFormEl = await fixture(html` - <${formGroupTag}> - <${childFormGroupTag} name="child[]"> - <${childFormGroupTag} name="child[]"> - - `); - expect(parentFormEl.resetModelValue.length).to.equal(2); - parentFormEl.removeChild(parentFormEl.children[0]); - expect(parentFormEl.resetModelValue.length).to.equal(1); - }); - }); }); }; From a91840f9c1cb4f12b53d547e9da05b2eb3189a67 Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Wed, 14 Aug 2019 17:36:28 +0200 Subject: [PATCH 5/5] chore: add command legacy karma browserstack --- package.json | 1 + packages/field/test/lion-field.test.js | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7e5f1c4ad..fff8bef5b 100644 --- a/package.json +++ b/package.json @@ -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 .", diff --git a/packages/field/test/lion-field.test.js b/packages/field/test/lion-field.test.js index 19dd00d00..1377359ff 100644 --- a/packages/field/test/lion-field.test.js +++ b/packages/field/test/lion-field.test.js @@ -91,9 +91,7 @@ describe('', () => { }); it('can be cleared which erases value, validation and interaction states', async () => { - const el = await fixture( - html`<${tag} value="Some value from attribute">${inputSlot}`, - ); + const el = await fixture(html`<${tag} value="Some value from attribute">${inputSlot}`); el.clear(); expect(el.value).to.equal(''); el.value = 'Some value from property';