Skip to content

Commit 2a0d18b

Browse files
committed
fix(field): make sure RegistrationSystem works well with ShadyDom
1 parent d3599fd commit 2a0d18b

File tree

9 files changed

+363
-150
lines changed

9 files changed

+363
-150
lines changed

packages/field/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ export { FormatMixin } from './src/FormatMixin.js';
44
export { FormControlMixin } from './src/FormControlMixin.js';
55
export { InteractionStateMixin } from './src/InteractionStateMixin.js'; // applies FocusMixin
66
export { LionField } from './src/LionField.js';
7+
export { FormRegisteringMixin } from './src/FormRegisteringMixin.js';
8+
export { FormRegistrarMixin } from './src/FormRegistrarMixin.js';

packages/field/src/FormControlMixin.js

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { html, css, nothing, dedupeMixin, SlotMixin } from '@lion/core';
22
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
3+
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
34

45
/**
56
* #FormControlMixin :
@@ -14,7 +15,7 @@ import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
1415
export const FormControlMixin = dedupeMixin(
1516
superclass =>
1617
// eslint-disable-next-line no-shadow, no-unused-vars
17-
class FormControlMixin extends ObserverMixin(SlotMixin(superclass)) {
18+
class FormControlMixin extends FormRegisteringMixin(ObserverMixin(SlotMixin(superclass))) {
1819
static get properties() {
1920
return {
2021
...super.properties,
@@ -105,8 +106,6 @@ export const FormControlMixin = dedupeMixin(
105106
super.connectedCallback();
106107
this._enhanceLightDomClasses();
107108
this._enhanceLightDomA11y();
108-
this._registerFormElement();
109-
this._requestParentFormGroupUpdateOfResetModelValue();
110109
}
111110

112111
/**
@@ -150,42 +149,6 @@ export const FormControlMixin = dedupeMixin(
150149
this._enhanceLightDomA11yForAdditionalSlots();
151150
}
152151

153-
/**
154-
* Fires a registration event in the next frame.
155-
*
156-
* Why next frame?
157-
* if ShadyDOM is used and you add a listener and fire the event in the same frame
158-
* it will not bubble and there can not be cought by a parent element
159-
* for more details see: https://github.com/Polymer/lit-element/issues/658
160-
* will requires a `await nextFrame()` in tests
161-
*/
162-
_registerFormElement() {
163-
this.updateComplete.then(() => {
164-
this.dispatchEvent(
165-
new CustomEvent('form-element-register', {
166-
detail: { element: this },
167-
bubbles: true,
168-
}),
169-
);
170-
});
171-
}
172-
173-
/**
174-
* Makes sure our parentFormGroup has the most up to date resetModelValue
175-
* FormGroups will call the same on their parentFormGroup so the full tree gets the correct
176-
* values.
177-
*
178-
* Why next frame?
179-
* @see {@link this._registerFormElement}
180-
*/
181-
_requestParentFormGroupUpdateOfResetModelValue() {
182-
this.updateComplete.then(() => {
183-
if (this.__parentFormGroup) {
184-
this.__parentFormGroup._updateResetModelValue();
185-
}
186-
});
187-
}
188-
189152
/**
190153
* Enhances additional slots(prefix, suffix, before, after) defined by developer.
191154
*
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { dedupeMixin } from '@lion/core';
2+
import { formRegistrarManager } from './formRegistrarManager.js';
3+
4+
/**
5+
* #FormRegisteringMixin:
6+
*
7+
* This Mixin registers a form element to a Registrar
8+
*
9+
* @polymerMixin
10+
* @mixinFunction
11+
*/
12+
export const FormRegisteringMixin = dedupeMixin(
13+
superclass =>
14+
// eslint-disable-next-line no-shadow, no-unused-vars
15+
class FormRegisteringMixin extends superclass {
16+
connectedCallback() {
17+
if (super.connectedCallback) {
18+
super.connectedCallback();
19+
}
20+
this.__setupRegistrationHook();
21+
}
22+
23+
disconnectedCallback() {
24+
if (super.disconnectedCallback) {
25+
super.disconnectedCallback();
26+
}
27+
this._unregisterFormElement();
28+
}
29+
30+
__setupRegistrationHook() {
31+
if (formRegistrarManager.ready) {
32+
this._registerFormElement();
33+
} else {
34+
formRegistrarManager.addEventListener('all-forms-open-for-registration', () => {
35+
this._registerFormElement();
36+
});
37+
}
38+
}
39+
40+
_registerFormElement() {
41+
this._dispatchRegistration();
42+
this._requestParentFormGroupUpdateOfResetModelValue();
43+
}
44+
45+
_dispatchRegistration() {
46+
this.dispatchEvent(
47+
new CustomEvent('form-element-register', {
48+
detail: { element: this },
49+
bubbles: true,
50+
}),
51+
);
52+
}
53+
54+
_unregisterFormElement() {
55+
if (this.__parentFormGroup) {
56+
this.__parentFormGroup.removeFormElement(this);
57+
}
58+
}
59+
60+
/**
61+
* Makes sure our parentFormGroup has the most up to date resetModelValue
62+
* FormGroups will call the same on their parentFormGroup so the full tree gets the correct
63+
* values.
64+
*/
65+
_requestParentFormGroupUpdateOfResetModelValue() {
66+
if (this.__parentFormGroup && this.__parentFormGroup._updateResetModelValue) {
67+
this.__parentFormGroup._updateResetModelValue();
68+
}
69+
}
70+
},
71+
);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { dedupeMixin } from '@lion/core';
2+
import { formRegistrarManager } from './formRegistrarManager.js';
3+
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
4+
5+
/**
6+
* This allows an element to become the manager of a register
7+
*/
8+
export const FormRegistrarMixin = dedupeMixin(
9+
superclass =>
10+
// eslint-disable-next-line no-shadow, no-unused-vars
11+
class FormRegistrarMixin extends FormRegisteringMixin(superclass) {
12+
get formElements() {
13+
return this.__formElements;
14+
}
15+
16+
set formElements(value) {
17+
this.__formElements = value;
18+
}
19+
20+
get formElementsArray() {
21+
return this.__formElements;
22+
}
23+
24+
constructor() {
25+
super();
26+
this.formElements = [];
27+
this.__readyForRegistration = false;
28+
this.registrationReady = new Promise(resolve => {
29+
this.__resolveRegistrationReady = resolve;
30+
});
31+
formRegistrarManager.add(this);
32+
33+
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
34+
this.addEventListener('form-element-register', this._onRequestToAddFormElement);
35+
}
36+
37+
isRegisteredFormElement(el) {
38+
return this.formElementsArray.some(exitingEl => exitingEl === el);
39+
}
40+
41+
firstUpdated(changedProperties) {
42+
super.firstUpdated(changedProperties);
43+
this.__resolveRegistrationReady();
44+
this.__readyForRegistration = true;
45+
formRegistrarManager.becomesReady(this);
46+
}
47+
48+
addFormElement(child) {
49+
// This is a way to let the child element (a lion-fieldset or lion-field) know, about its parent
50+
// eslint-disable-next-line no-param-reassign
51+
child.__parentFormGroup = this;
52+
53+
this.formElements.push(child);
54+
}
55+
56+
removeFormElement(child) {
57+
const index = this.formElements.indexOf(child);
58+
if (index > -1) {
59+
this.formElements.splice(index, 1);
60+
}
61+
}
62+
63+
_onRequestToAddFormElement(ev) {
64+
const child = ev.detail.element;
65+
if (child === this) {
66+
// as we fire and listen - don't add ourselves
67+
return;
68+
}
69+
if (this.isRegisteredFormElement(child)) {
70+
// do not readd already existing elements
71+
return;
72+
}
73+
ev.stopPropagation();
74+
this.addFormElement(child);
75+
}
76+
77+
_onRequestToRemoveFormElement(ev) {
78+
const child = ev.detail.element;
79+
if (child === this) {
80+
// as we fire and listen - don't add ourselves
81+
return;
82+
}
83+
if (!this.isRegisteredFormElement(child)) {
84+
// do not readd already existing elements
85+
return;
86+
}
87+
ev.stopPropagation();
88+
89+
this.removeFormElement(child);
90+
}
91+
},
92+
);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Allows to align the timing for all Registrars (like form, fieldset).
3+
* e.g. it will only be ready once all Registrars have been fully rendered
4+
*
5+
* This is a requirement for ShadyDOM as otherwise forms can not catch registration events
6+
*/
7+
class FormRegistrarManager {
8+
constructor() {
9+
this.__elements = [];
10+
this._fakeExtendsEventTarget();
11+
this.ready = false;
12+
}
13+
14+
add(registrar) {
15+
this.__elements.push(registrar);
16+
this.ready = false;
17+
}
18+
19+
becomesReady() {
20+
if (this.__elements.every(el => el.__readyForRegistration === true)) {
21+
this.dispatchEvent(new Event('all-forms-open-for-registration'));
22+
this.ready = true;
23+
}
24+
}
25+
26+
// TODO: this method has to be removed when EventTarget polyfill is available on IE11
27+
// issue: https://gitlab.ing.net/TheGuideComponents/lion-element/issues/12
28+
_fakeExtendsEventTarget() {
29+
const delegate = document.createDocumentFragment();
30+
['addEventListener', 'dispatchEvent', 'removeEventListener'].forEach(funcName => {
31+
this[funcName] = (...args) => delegate[funcName](...args);
32+
});
33+
}
34+
}
35+
36+
export const formRegistrarManager = new FormRegistrarManager();

packages/field/test/FormControlMixin.test.js

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { expect, fixture, html, defineCE, unsafeStatic, nextFrame } from '@open-wc/testing';
2-
import sinon from 'sinon';
1+
import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing';
32
import { SlotMixin } from '@lion/core';
43
import { LionLitElement } from '@lion/core/src/LionLitElement.js';
54

@@ -26,42 +25,6 @@ describe('FormControlMixin', () => {
2625
tag = unsafeStatic(elem);
2726
});
2827

29-
it('dispatches event to register in Light DOM', async () => {
30-
const registerSpy = sinon.spy();
31-
await fixture(html`
32-
<div @form-element-register=${registerSpy}>
33-
<${tag}></${tag}>
34-
</div>
35-
`);
36-
await nextFrame();
37-
expect(registerSpy.callCount).to.equal(1);
38-
});
39-
40-
it('can by caught by listening in the appropriate dom', async () => {
41-
const registerSpy = sinon.spy();
42-
const testTag = unsafeStatic(
43-
defineCE(
44-
class extends LionLitElement {
45-
connectedCallback() {
46-
super.connectedCallback();
47-
this.shadowRoot.addEventListener('form-element-register', registerSpy);
48-
}
49-
50-
render() {
51-
return html`
52-
<${tag}></${tag}>
53-
`;
54-
}
55-
},
56-
),
57-
);
58-
await fixture(html`
59-
<${testTag}></${testTag}>
60-
`);
61-
await nextFrame();
62-
expect(registerSpy.callCount).to.equal(1);
63-
});
64-
6528
it('has the capability to override the help text', async () => {
6629
const lionFieldAttr = await fixture(html`
6730
<${tag} help-text="This email address is already taken">${inputSlot}</${tag}>

0 commit comments

Comments
 (0)