diff --git a/packages/field/src/FormRegistrarPortalMixin.js b/packages/field/src/FormRegistrarPortalMixin.js new file mode 100644 index 000000000..f0e800c56 --- /dev/null +++ b/packages/field/src/FormRegistrarPortalMixin.js @@ -0,0 +1,55 @@ +import { dedupeMixin } from '@lion/core'; +import { formRegistrarManager } from './formRegistrarManager.js'; + +/** + * This will forward + */ +export const FormRegistrarPortalMixin = dedupeMixin( + superclass => + // eslint-disable-next-line no-shadow, no-unused-vars + class FormRegistrarPortalMixin extends superclass { + constructor() { + super(); + this.formElements = []; + this.__readyForRegistration = false; + this.registrationReady = new Promise(resolve => { + this.__resolveRegistrationReady = resolve; + }); + } + + connectedCallback() { + if (super.connectedCallback) { + super.connectedCallback(); + } + formRegistrarManager.add(this); + this.__redispatchEventForFormRegistrarPortalMixin = ev => { + ev.stopPropagation(); + // TODO: fire event with changed ev.target + this.dispatchEvent( + new CustomEvent('form-element-register', { + detail: { element: ev.element }, + bubbles: true, + }), + ); + }; + this.addEventListener( + 'form-element-register', + this.__redispatchEventForFormRegistrarPortalMixin, + ); + } + + disconnectedCallback() { + if (super.disconnectedCallback) { + super.disconnectedCallback(); + } + formRegistrarManager.remove(this); + } + + firstUpdated(changedProperties) { + super.firstUpdated(changedProperties); + this.__resolveRegistrationReady(); + this.__readyForRegistration = true; + formRegistrarManager.becomesReady(this); + } + }, +); diff --git a/packages/field/test-suites/FormRegistrationMixins.suite.js b/packages/field/test-suites/FormRegistrationMixins.suite.js index 5db8bb5dd..cfbc97b91 100644 --- a/packages/field/test-suites/FormRegistrationMixins.suite.js +++ b/packages/field/test-suites/FormRegistrationMixins.suite.js @@ -3,6 +3,7 @@ import { LitElement } from '@lion/core'; import { FormRegistrarMixin } from '../src/FormRegistrarMixin.js'; import { FormRegisteringMixin } from '../src/FormRegisteringMixin.js'; +import { FormRegistrarPortalMixin } from '../src/FormRegistrarPortalMixin.js'; import { formRegistrarManager } from '../src/formRegistrarManager.js'; export const runRegistrationSuite = customConfig => { @@ -15,6 +16,7 @@ export const runRegistrationSuite = customConfig => { describe(`FormRegistrationMixins${cfg.suffix ? ` (${cfg.suffix})` : ''}`, () => { let parentTag; let childTag; + let portalTag; before(async () => { if (!cfg.parentTagString) { @@ -23,9 +25,13 @@ export const runRegistrationSuite = customConfig => { if (!cfg.childTagString) { cfg.childTagString = defineCE(class extends FormRegisteringMixin(cfg.baseElement) {}); } + if (!cfg.portalTagString) { + cfg.portalTagString = defineCE(class extends FormRegistrarPortalMixin(cfg.baseElement) {}); + } parentTag = unsafeStatic(cfg.parentTagString); childTag = unsafeStatic(cfg.childTagString); + portalTag = unsafeStatic(cfg.portalTagString); }); it('can register a formElement', async () => { @@ -117,5 +123,72 @@ export const runRegistrationSuite = customConfig => { el.removeChild(newField); expect(el.formElements.length).to.equal(1); }); + + describe('FormRegistrarPortalMixin', () => { + it('throws if there is no .registrationTarget', async () => { + expect(async () => { + await fixture(html`<${portalTag}>`); + }).to.throw('A FormRegistrarPortal element requires a .registrationTarget'); + }); + + it('forwards registrations to the .registrationTarget', async () => { + const el = await fixture(html`<${parentTag}>`); + await fixture(html` + <${portalTag} .registrationTarget=${el}> + <${childTag}> + + `); + + expect(el.formElements.length).to.equal(1); + }); + + it('can dynamically add/remove elements', async () => { + const el = await fixture(html`<${parentTag}>`); + const portal = await fixture(html` + <${portalTag} .registrationTarget=${el}> + <${childTag}> + + `); + const newField = await fixture(html` + <${childTag}> + `); + + expect(el.formElements.length).to.equal(1); + + portal.appendChild(newField); + expect(el.formElements.length).to.equal(2); + + portal.removeChild(newField); + expect(el.formElements.length).to.equal(1); + }); + + it('works for portals that have a delayed render', async () => { + const delayedPortalString = defineCE( + class extends FormRegistrarPortalMixin(LitElement) { + async performUpdate() { + await new Promise(resolve => setTimeout(() => resolve(), 10)); + await super.performUpdate(); + } + + render() { + return html` + + `; + } + }, + ); + const delayedPortalTag = unsafeStatic(delayedPortalString); + + const el = await fixture(html`<${parentTag}>`); + await fixture(html` + <${delayedPortalTag} .registrationTarget=${el}> + <${childTag}> + + `); + + await el.registrationReady; + expect(el.formElements.length).to.equal(1); + }); + }); }); };