fix(form-core): form groups support lazily rendered children

This commit is contained in:
Thijs Louisse 2021-04-19 11:03:00 +02:00
parent edb43c4e05
commit 15146bf9ce
3 changed files with 241 additions and 2 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/form-core': patch
---
form groups support lazily rendered children

View file

@ -148,6 +148,9 @@ const FormGroupMixinImplementation = superclass =>
this.defaultValidators = [new FormElementsHaveNoError()]; this.defaultValidators = [new FormElementsHaveNoError()];
this.__descriptionElementsInParentChain = new Set(); this.__descriptionElementsInParentChain = new Set();
/** @type {{modelValue?:{[key:string]: any}, serializedValue?:{[key:string]: any}}} */
this.__pendingValues = { modelValue: {}, serializedValue: {} };
} }
connectedCallback() { connectedCallback() {
@ -349,6 +352,8 @@ const FormGroupMixinImplementation = superclass =>
} }
if (this.formElements[name]) { if (this.formElements[name]) {
this.formElements[name][property] = values[name]; this.formElements[name][property] = values[name];
} else {
this.__pendingValues[property][name] = values[name];
} }
}); });
} }
@ -485,7 +490,7 @@ const FormGroupMixinImplementation = superclass =>
* @override of FormRegistrarMixin. * @override of FormRegistrarMixin.
* @desc Connects ValidateMixin and DisabledMixin * @desc Connects ValidateMixin and DisabledMixin
* On top of this, error messages of children are linked to their parents * 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 * @param {number} indexToInsertAt
*/ */
addFormElement(child, indexToInsertAt) { addFormElement(child, indexToInsertAt) {
@ -502,6 +507,16 @@ const FormGroupMixinImplementation = superclass =>
if (typeof child.addToAriaLabelledBy === 'function' && this._labelNode) { if (typeof child.addToAriaLabelledBy === 'function' && this._labelNode) {
child.addToAriaLabelledBy(this._labelNode, { reorder: false }); 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];
}
}
} }
/** /**

View file

@ -1,4 +1,4 @@
import { LitElement } from '@lion/core'; import { LitElement, ifDefined } from '@lion/core';
import { localizeTearDown } from '@lion/localize/test-helpers'; import { localizeTearDown } from '@lion/localize/test-helpers';
import { import {
defineCE, defineCE,
@ -1165,5 +1165,224 @@ export function runFormGroupMixinSuite(cfg = {}) {
expect(el.getAttribute('aria-labelledby')).contains(label.id); 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}"></${childTag}>`;
}
return html`<${childTag} name="${field}"></${childTag}>`;
})}
</${tag}>
`;
}
}
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' }}"
>
</${dynamicChildrenTag}>
`));
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' }}"
>
</${dynamicChildrenTag}>
`));
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' }}">
</${dynamicChildrenTag}>
`));
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' }}">
</${dynamicChildrenTag}>
`));
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',
}}">
</${dynamicChildrenTag}>
`));
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',
}}">
</${dynamicChildrenTag}>
`));
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',
}}">
</${dynamicChildrenTag}>
`));
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',
}}">
</${dynamicChildrenTag}>
`));
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',
}}">
</${dynamicChildrenTag}>
`));
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',
}}">
</${dynamicChildrenTag}>
`));
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');
});
});
}); });
} }