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; };