From 15146bf9ce40a85a40dcb4739ac7e3d2159c218c Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Mon, 19 Apr 2021 11:03:00 +0200 Subject: [PATCH] fix(form-core): form groups support lazily rendered children --- .changeset/nasty-scissors-fix.md | 5 + .../src/form-group/FormGroupMixin.js | 17 +- .../form-group/FormGroupMixin.suite.js | 221 +++++++++++++++++- 3 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 .changeset/nasty-scissors-fix.md diff --git a/.changeset/nasty-scissors-fix.md b/.changeset/nasty-scissors-fix.md new file mode 100644 index 000000000..ff900d9d9 --- /dev/null +++ b/.changeset/nasty-scissors-fix.md @@ -0,0 +1,5 @@ +--- +'@lion/form-core': patch +--- + +form groups support lazily rendered children diff --git a/packages/form-core/src/form-group/FormGroupMixin.js b/packages/form-core/src/form-group/FormGroupMixin.js index aecdf93d4..f97720c78 100644 --- a/packages/form-core/src/form-group/FormGroupMixin.js +++ b/packages/form-core/src/form-group/FormGroupMixin.js @@ -148,6 +148,9 @@ const FormGroupMixinImplementation = superclass => this.defaultValidators = [new FormElementsHaveNoError()]; this.__descriptionElementsInParentChain = new Set(); + + /** @type {{modelValue?:{[key:string]: any}, serializedValue?:{[key:string]: any}}} */ + this.__pendingValues = { modelValue: {}, serializedValue: {} }; } connectedCallback() { @@ -349,6 +352,8 @@ const FormGroupMixinImplementation = superclass => } if (this.formElements[name]) { this.formElements[name][property] = values[name]; + } else { + this.__pendingValues[property][name] = values[name]; } }); } @@ -485,7 +490,7 @@ const FormGroupMixinImplementation = superclass => * @override of FormRegistrarMixin. * @desc Connects ValidateMixin and DisabledMixin * On top of this, error messages of children are linked to their parents - * @param {FormControl} child + * @param {FormControl & {serializedValue:string|object}} child * @param {number} indexToInsertAt */ addFormElement(child, indexToInsertAt) { @@ -502,6 +507,16 @@ const FormGroupMixinImplementation = superclass => if (typeof child.addToAriaLabelledBy === 'function' && this._labelNode) { child.addToAriaLabelledBy(this._labelNode, { reorder: false }); } + if (!child.modelValue) { + const pVals = this.__pendingValues; + if (pVals.modelValue && pVals.modelValue[child.name]) { + // eslint-disable-next-line no-param-reassign + child.modelValue = pVals.modelValue[child.name]; + } else if (pVals.serializedValue && pVals.serializedValue[child.name]) { + // eslint-disable-next-line no-param-reassign + child.serializedValue = pVals.serializedValue[child.name]; + } + } } /** diff --git a/packages/form-core/test-suites/form-group/FormGroupMixin.suite.js b/packages/form-core/test-suites/form-group/FormGroupMixin.suite.js index acc0d9fef..9e9929633 100644 --- a/packages/form-core/test-suites/form-group/FormGroupMixin.suite.js +++ b/packages/form-core/test-suites/form-group/FormGroupMixin.suite.js @@ -1,4 +1,4 @@ -import { LitElement } from '@lion/core'; +import { LitElement, ifDefined } from '@lion/core'; import { localizeTearDown } from '@lion/localize/test-helpers'; import { defineCE, @@ -1165,5 +1165,224 @@ export function runFormGroupMixinSuite(cfg = {}) { expect(el.getAttribute('aria-labelledby')).contains(label.id); }); }); + + describe('Dynamically rendered children', () => { + class DynamicCWrapper extends LitElement { + static get properties() { + return { + fields: { type: Array }, + }; + } + + constructor() { + super(); + /** @type {any[]} */ + this.fields = []; + /** @type {object|undefined} */ + this.modelValue = undefined; + /** @type {object|undefined} */ + this.serializedValue = undefined; + } + + render() { + return html` + <${tag} + .modelValue=${ifDefined(this.modelValue)} + .serializedValue=${ifDefined(this.serializedValue)}> + ${this.fields.map(field => { + if (typeof field === 'object') { + return html`<${childTag} name="${field.name}" .modelValue="${field.value}">`; + } + return html`<${childTag} name="${field}">`; + })} + + `; + } + } + const dynamicChildrenTagString = defineCE(DynamicCWrapper); + const dynamicChildrenTag = unsafeStatic(dynamicChildrenTagString); + + it(`when rendering children right from the start, sets their values correctly + based on prefilled model/seriazedValue`, async () => { + const el = /** @type {DynamicCWrapper} */ (await fixture(html` + <${dynamicChildrenTag} + .fields="${['firstName', 'lastName']}" + .modelValue="${{ firstName: 'foo', lastName: 'bar' }}" + > + + `)); + await el.updateComplete; + const fieldset = /** @type {FormGroup} */ ( + /** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString) + ); + expect(fieldset.formElements[0].modelValue).to.equal('foo'); + expect(fieldset.formElements[1].modelValue).to.equal('bar'); + + const el2 = /** @type {DynamicCWrapper} */ (await fixture(html` + <${dynamicChildrenTag} + .fields="${['firstName', 'lastName']}" + .serializedValue="${{ firstName: 'foo', lastName: 'bar' }}" + > + + `)); + await el2.updateComplete; + const fieldset2 = /** @type {FormGroup} */ ( + /** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString) + ); + expect(fieldset2.formElements[0].serializedValue).to.equal('foo'); + expect(fieldset2.formElements[1].serializedValue).to.equal('bar'); + }); + + it(`when rendering children delayed, sets their values + correctly based on prefilled model/seriazedValue`, async () => { + const el = /** @type {DynamicCWrapper} */ (await fixture(html` + <${dynamicChildrenTag} .modelValue="${{ firstName: 'foo', lastName: 'bar' }}"> + + `)); + await el.updateComplete; + const fieldset = /** @type {FormGroup} */ ( + /** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString) + ); + el.fields = ['firstName', 'lastName']; + await el.updateComplete; + expect(fieldset.formElements[0].modelValue).to.equal('foo'); + expect(fieldset.formElements[1].modelValue).to.equal('bar'); + + const el2 = /** @type {DynamicCWrapper} */ (await fixture(html` + <${dynamicChildrenTag} .serializedValue="${{ firstName: 'foo', lastName: 'bar' }}"> + + `)); + await el2.updateComplete; + const fieldset2 = /** @type {FormGroup} */ ( + /** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString) + ); + el2.fields = ['firstName', 'lastName']; + await el2.updateComplete; + expect(fieldset2.formElements[0].serializedValue).to.equal('foo'); + expect(fieldset2.formElements[1].serializedValue).to.equal('bar'); + }); + + it(`when rendering children partly delayed, sets their values correctly based on + prefilled model/seriazedValue`, async () => { + const el = /** @type {DynamicCWrapper} */ (await fixture(html` + <${dynamicChildrenTag} .fields="${['firstName']}" .modelValue="${{ + firstName: 'foo', + lastName: 'bar', + }}"> + + `)); + await el.updateComplete; + const fieldset = /** @type {FormGroup} */ ( + /** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString) + ); + el.fields = ['firstName', 'lastName']; + await el.updateComplete; + expect(fieldset.formElements[0].modelValue).to.equal('foo'); + expect(fieldset.formElements[1].modelValue).to.equal('bar'); + + const el2 = /** @type {DynamicCWrapper} */ (await fixture(html` + <${dynamicChildrenTag} .fields="${['firstName']}" .serializedValue="${{ + firstName: 'foo', + lastName: 'bar', + }}"> + + `)); + await el2.updateComplete; + const fieldset2 = /** @type {FormGroup} */ ( + /** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString) + ); + el2.fields = ['firstName', 'lastName']; + await el2.updateComplete; + expect(fieldset2.formElements[0].serializedValue).to.equal('foo'); + expect(fieldset2.formElements[1].serializedValue).to.equal('bar'); + }); + + it(`does not change interaction states when values set for delayed children`, async () => { + function expectInteractionStatesToBeCorrectFor(/** @type {FormChild|FormGroup} */ elm) { + expect(Boolean(elm.submitted)).to.be.false; + expect(elm.dirty).to.be.false; + expect(elm.touched).to.be.false; + expect(elm.prefilled).to.be.true; + } + + const el = /** @type {DynamicCWrapper} */ (await fixture(html` + <${dynamicChildrenTag} .fields="${['firstName']}" .modelValue="${{ + firstName: 'foo', + lastName: 'bar', + }}"> + + `)); + await el.updateComplete; + const fieldset = /** @type {FormGroup} */ ( + /** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString) + ); + await fieldset.registrationComplete; + + el.fields = ['firstName', 'lastName']; + await el.updateComplete; + expectInteractionStatesToBeCorrectFor(fieldset.formElements[0]); + expectInteractionStatesToBeCorrectFor(fieldset.formElements[1]); + expectInteractionStatesToBeCorrectFor(fieldset); + + const el2 = /** @type {DynamicCWrapper} */ (await fixture(html` + <${dynamicChildrenTag} .fields="${['firstName']}" .serializedValue="${{ + firstName: 'foo', + lastName: 'bar', + }}"> + + `)); + await el2.updateComplete; + const fieldset2 = /** @type {FormGroup} */ ( + /** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString) + ); + await fieldset2.registrationComplete; + + el2.fields = ['firstName', 'lastName']; + await el2.updateComplete; + expectInteractionStatesToBeCorrectFor(fieldset2.formElements[0]); + expectInteractionStatesToBeCorrectFor(fieldset2.formElements[1]); + expectInteractionStatesToBeCorrectFor(fieldset2); + }); + + it(`prefilled children values take precedence over parent values`, async () => { + const el = /** @type {DynamicCWrapper} */ (await fixture(html` + <${dynamicChildrenTag} .modelValue="${{ + firstName: 'foo', + lastName: 'bar', + }}"> + + `)); + await el.updateComplete; + const fieldset = /** @type {FormGroup} */ ( + /** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString) + ); + el.fields = [ + { name: 'firstName', value: 'wins' }, + { name: 'lastName', value: 'winsAsWell' }, + ]; + await el.updateComplete; + expect(fieldset.formElements[0].modelValue).to.equal('wins'); + expect(fieldset.formElements[1].modelValue).to.equal('winsAsWell'); + + const el2 = /** @type {DynamicCWrapper} */ (await fixture(html` + <${dynamicChildrenTag} .serializedValue="${{ + firstName: 'foo', + lastName: 'bar', + }}"> + + `)); + await el2.updateComplete; + const fieldset2 = /** @type {FormGroup} */ ( + /** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString) + ); + el2.fields = [ + { name: 'firstName', value: 'wins' }, + { name: 'lastName', value: 'winsAsWell' }, + ]; + await el2.updateComplete; + expect(fieldset2.formElements[0].serializedValue).to.equal('wins'); + expect(fieldset2.formElements[1].serializedValue).to.equal('winsAsWell'); + }); + }); }); }