fix(field): implement FormRegistrarPortalMixin

This commit is contained in:
Thomas Allmer 2019-08-23 13:23:53 +02:00 committed by Thomas Allmer
parent f8a3c54f60
commit 2cb6dee6d9
3 changed files with 51 additions and 6 deletions

View file

@ -6,3 +6,4 @@ export { InteractionStateMixin } from './src/InteractionStateMixin.js'; // appli
export { LionField } from './src/LionField.js'; export { LionField } from './src/LionField.js';
export { FormRegisteringMixin } from './src/FormRegisteringMixin.js'; export { FormRegisteringMixin } from './src/FormRegisteringMixin.js';
export { FormRegistrarMixin } from './src/FormRegistrarMixin.js'; export { FormRegistrarMixin } from './src/FormRegistrarMixin.js';
export { FormRegistrarPortalMixin } from './src/FormRegistrarPortalMixin.js';

View file

@ -2,7 +2,16 @@ import { dedupeMixin } from '@lion/core';
import { formRegistrarManager } from './formRegistrarManager.js'; import { formRegistrarManager } from './formRegistrarManager.js';
/** /**
* This will forward * This allows to register fields within a form even though they are not within the same dom tree.
* It does that by redispatching the event on the registration target.
* Neither form or field need to know about the portal. It acts as if the field is part of the dom tree.
*
* @example
* <my-form></my-form>
* <my-portal .registrationTarget=${document.querySelector('my-form')}>
* <my-field></my-field>
* </my-portal>
* // my-field will be registered within my-form
*/ */
export const FormRegistrarPortalMixin = dedupeMixin( export const FormRegistrarPortalMixin = dedupeMixin(
superclass => superclass =>
@ -11,6 +20,7 @@ export const FormRegistrarPortalMixin = dedupeMixin(
constructor() { constructor() {
super(); super();
this.formElements = []; this.formElements = [];
this.registrationTarget = undefined;
this.__readyForRegistration = false; this.__readyForRegistration = false;
this.registrationReady = new Promise(resolve => { this.registrationReady = new Promise(resolve => {
this.__resolveRegistrationReady = resolve; this.__resolveRegistrationReady = resolve;
@ -21,13 +31,16 @@ export const FormRegistrarPortalMixin = dedupeMixin(
if (super.connectedCallback) { if (super.connectedCallback) {
super.connectedCallback(); super.connectedCallback();
} }
this.__checkRegistrationTarget();
formRegistrarManager.add(this); formRegistrarManager.add(this);
this.__redispatchEventForFormRegistrarPortalMixin = ev => { this.__redispatchEventForFormRegistrarPortalMixin = ev => {
ev.stopPropagation(); ev.stopPropagation();
// TODO: fire event with changed ev.target // TODO: change ev.target to original registering element
this.dispatchEvent( this.registrationTarget.dispatchEvent(
new CustomEvent('form-element-register', { new CustomEvent('form-element-register', {
detail: { element: ev.element }, detail: { element: ev.detail.element },
bubbles: true, bubbles: true,
}), }),
); );
@ -43,6 +56,10 @@ export const FormRegistrarPortalMixin = dedupeMixin(
super.disconnectedCallback(); super.disconnectedCallback();
} }
formRegistrarManager.remove(this); formRegistrarManager.remove(this);
this.removeEventListener(
'form-element-register',
this.__redispatchEventForFormRegistrarPortalMixin,
);
} }
firstUpdated(changedProperties) { firstUpdated(changedProperties) {
@ -51,5 +68,11 @@ export const FormRegistrarPortalMixin = dedupeMixin(
this.__readyForRegistration = true; this.__readyForRegistration = true;
formRegistrarManager.becomesReady(this); formRegistrarManager.becomesReady(this);
} }
__checkRegistrationTarget() {
if (!this.registrationTarget) {
throw new Error('A FormRegistrarPortal element requires a .registrationTarget');
}
}
}, },
); );

View file

@ -1,5 +1,6 @@
import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing'; import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing';
import { LitElement } from '@lion/core'; import { LitElement } from '@lion/core';
import sinon from 'sinon';
import { FormRegistrarMixin } from '../src/FormRegistrarMixin.js'; import { FormRegistrarMixin } from '../src/FormRegistrarMixin.js';
import { FormRegisteringMixin } from '../src/FormRegisteringMixin.js'; import { FormRegisteringMixin } from '../src/FormRegisteringMixin.js';
@ -17,6 +18,7 @@ export const runRegistrationSuite = customConfig => {
let parentTag; let parentTag;
let childTag; let childTag;
let portalTag; let portalTag;
let portalTagString;
before(async () => { before(async () => {
if (!cfg.parentTagString) { if (!cfg.parentTagString) {
@ -30,6 +32,7 @@ export const runRegistrationSuite = customConfig => {
} }
parentTag = unsafeStatic(cfg.parentTagString); parentTag = unsafeStatic(cfg.parentTagString);
portalTagString = cfg.portalTagString;
childTag = unsafeStatic(cfg.childTagString); childTag = unsafeStatic(cfg.childTagString);
portalTag = unsafeStatic(cfg.portalTagString); portalTag = unsafeStatic(cfg.portalTagString);
}); });
@ -126,8 +129,11 @@ export const runRegistrationSuite = customConfig => {
describe('FormRegistrarPortalMixin', () => { describe('FormRegistrarPortalMixin', () => {
it('throws if there is no .registrationTarget', async () => { it('throws if there is no .registrationTarget', async () => {
expect(async () => { // we test the private api directly as errors thrown from a web component are in a
await fixture(html`<${portalTag}></${portalTag}>`); // different context and we can not catch them here
const el = document.createElement(portalTagString);
expect(() => {
el.__checkRegistrationTarget();
}).to.throw('A FormRegistrarPortal element requires a .registrationTarget'); }).to.throw('A FormRegistrarPortal element requires a .registrationTarget');
}); });
@ -162,6 +168,21 @@ export const runRegistrationSuite = customConfig => {
expect(el.formElements.length).to.equal(1); expect(el.formElements.length).to.equal(1);
}); });
// find a proper way to do this on polyfilled browsers
it.skip('fires event "form-element-register" with the child as ev.target', async () => {
const registerSpy = sinon.spy();
const el = await fixture(
html`<${parentTag} @form-element-register=${registerSpy}></${parentTag}>`,
);
const portal = await fixture(html`
<${portalTag} .registrationTarget=${el}>
<${childTag}></${childTag}>
</${portalTag}>
`);
const childEl = portal.children[0];
expect(registerSpy.args[2][0].target.tagName).to.equal(childEl.tagName);
});
it('works for portals that have a delayed render', async () => { it('works for portals that have a delayed render', async () => {
const delayedPortalString = defineCE( const delayedPortalString = defineCE(
class extends FormRegistrarPortalMixin(LitElement) { class extends FormRegistrarPortalMixin(LitElement) {