fix(form-core): form groups support lazily rendered children
This commit is contained in:
parent
edb43c4e05
commit
15146bf9ce
3 changed files with 241 additions and 2 deletions
5
.changeset/nasty-scissors-fix.md
Normal file
5
.changeset/nasty-scissors-fix.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/form-core': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
form groups support lazily rendered children
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue