Skip to content

Commit 8467a69

Browse files
author
Steven Orvell
committed
Fixes #4447. Re-introduce the hostStack in order to maintain “client before host” ordering when _flushProperties is called before connectedCallback (e.g. as Templatize does).
1 parent 7836e6c commit 8467a69

6 files changed

Lines changed: 415 additions & 13 deletions

File tree

‎lib/mixins/element-mixin.html‎

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@
549549
*/
550550
_initializeProperties() {
551551
Polymer.telemetry.instanceCount++;
552+
hostStack.registerHost(this);
552553
this.constructor.finalize();
553554
const importPath = this.constructor.importPath;
554555
// note: finalize template when we have access to `localName` to
@@ -598,7 +599,9 @@
598599
if (window.ShadyCSS) {
599600
window.ShadyCSS.styleElement(this);
600601
}
601-
this._flushProperties();
602+
if (!this.__dataInitialized) {
603+
this._flushProperties();
604+
}
602605
}
603606

604607
/**
@@ -616,7 +619,9 @@
616619
*/
617620
ready() {
618621
if (this._template) {
622+
hostStack.beginHosting(this);
619623
this.root = this._stampTemplate(this._template);
624+
hostStack.endHosting(this);
620625
}
621626
super.ready();
622627
}
@@ -745,6 +750,53 @@
745750
return PolymerElement;
746751
});
747752

753+
/**
754+
* Helper api for enqueing client dom created by a host element.
755+
*
756+
* By default elements are flushed via `_flushProperties` when
757+
* `connectedCallback` is called. Elements attach their client dom to
758+
* themselves at `ready` time which results from this first flush.
759+
* This provides an ordering guarantee that the client dom an element
760+
* creates is flushed before the element itself (i.e. client `ready`
761+
* fires before host `ready`).
762+
*
763+
* However, if `_flushProperties` is called *before* an element is connected,
764+
* as for example `Templatize` does, this ordering guarantee cannot be
765+
* satisfied because no elements are connected. (Note: Bound elements that
766+
* receive data do become enqueued clients and are properly ordered but
767+
* unbound elements are not.)
768+
*
769+
* To maintain the desired "client before host" ordering guarantee for this
770+
* case we rely on the "host stack. Client nodes registers themselves with
771+
* the creating host element when created. This ensures that all client dom
772+
* is readied in the proper order, maintaining the desired guarantee.
773+
*
774+
* @private
775+
*/
776+
let hostStack = {
777+
778+
stack: [],
779+
780+
registerHost(inst) {
781+
if (this.stack.length) {
782+
let host = this.stack[this.stack.length-1];
783+
host._enqueueClient(inst);
784+
}
785+
},
786+
787+
beginHosting(inst) {
788+
this.stack.push(inst);
789+
},
790+
791+
endHosting(inst) {
792+
let stackLen = this.stack.length;
793+
if (stackLen && this.stack[stackLen-1] == inst) {
794+
this.stack.pop();
795+
}
796+
}
797+
798+
}
799+
748800
/**
749801
* Provides basic tracking of element definitions (registrations) and
750802
* instance counts.

‎test/runner.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
'unit/case-map.html',
4747
'unit/configure.html',
4848
'unit/ready-attached-order.html',
49+
'unit/ready-attached-order-class.html',
4950
'unit/attributes.html',
5051
'unit/async.html',
5152
'unit/behaviors.html',

‎test/smoke/ordering-test.html‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
}
4141
connectedCallback() {
4242
console.group(this.localName, 'connected');
43-
super.connectedCallback();
4443
console.warn(this.localName, 'connected (user)', this.shadowRoot);
44+
super.connectedCallback();
4545
console.groupEnd(this.localName, 'connected');
4646
}
4747
_flushProperties() {
@@ -79,8 +79,8 @@
7979
}
8080
connectedCallback() {
8181
console.group(this.localName, 'connected');
82-
super.connectedCallback();
8382
console.warn(this.localName, 'connected (user)', this.shadowRoot);
83+
super.connectedCallback();
8484
console.groupEnd(this.localName, 'connected');
8585
}
8686
_flushProperties() {
@@ -113,8 +113,8 @@
113113
}
114114
connectedCallback() {
115115
console.group(this.localName, 'connected');
116-
super.connectedCallback();
117116
console.warn(this.localName, 'connected (user)', this.shadowRoot);
117+
super.connectedCallback();
118118
console.groupEnd(this.localName, 'connected');
119119
}
120120
_flushProperties() {
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
<!doctype html>
2+
<!--
3+
@license
4+
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
5+
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
6+
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
7+
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
8+
Code distributed by Google as part of the polymer project is also
9+
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
10+
-->
11+
<html>
12+
<head>
13+
<meta charset="utf-8">
14+
<script src="../../../webcomponentsjs/webcomponents-lite.js"></script>
15+
<script src="../../../web-component-tester/browser.js"></script>
16+
<link rel="import" href="../../polymer.html">
17+
</head>
18+
<body>
19+
20+
<script>
21+
window.clearTestLists = function() {
22+
window.actualAttachedList = [];
23+
window.actualReadyList = [];
24+
window.actualReadyBeforeAtachedList = [];
25+
}
26+
27+
window.clearTestLists();
28+
29+
window.readyMixin = function(base) {
30+
return class readyMixin extends base {
31+
static get properties() {
32+
return {
33+
prop: {
34+
value: true,
35+
observer: '_propChanged'
36+
}
37+
}
38+
}
39+
40+
_propChanged() {
41+
this.observerShadowRoot = Boolean(this.shadowRoot);
42+
}
43+
ready() {
44+
super.ready();
45+
this._readied = true;
46+
this.readyList = window.actualReadyList.slice();
47+
this.readyShadowRoot = Boolean(this.shadowRoot);
48+
window.actualReadyList.push(this);
49+
}
50+
51+
connectedCallback() {
52+
this._eventList = [];
53+
this.addEventListener('e', (e) => {
54+
this._eventList.push(e.composedPath()[0]);
55+
});
56+
super.connectedCallback();
57+
this.dispatchEvent(new Event('e', {composed: true, bubbles: true}))
58+
this.attachedShadowRoot = Boolean(this.shadowRoot);
59+
this.attachedTime$Keys = Object.keys(this.$);
60+
this.attachedList = window.actualAttachedList.slice();
61+
window.actualAttachedList.push(this);
62+
if (!this._readied) {
63+
window.actualReadyBeforeAtachedList.push(this);
64+
}
65+
}
66+
}
67+
};
68+
</script>
69+
70+
<dom-module id="x-zot">
71+
<template>
72+
x-zot<slot></slot>
73+
</template>
74+
<script>
75+
HTMLImports.whenReady(function() {
76+
class El extends window.readyMixin(Polymer.Element) {
77+
static get is() { return 'x-zot' }
78+
}
79+
customElements.define(El.is, El);
80+
});
81+
</script>
82+
</dom-module>
83+
84+
<dom-module id="x-bar">
85+
<template>
86+
<x-zot id="zot"></x-zot>
87+
</template>
88+
<script>
89+
HTMLImports.whenReady(function() {
90+
class El extends window.readyMixin(Polymer.Element) {
91+
static get is() { return 'x-bar' }
92+
}
93+
customElements.define(El.is, El);
94+
});
95+
</script>
96+
</dom-module>
97+
98+
<dom-module id="x-foo">
99+
<template>
100+
<x-bar id="bar1"></x-bar>
101+
<x-bar id="bar2"></x-bar>
102+
</template>
103+
<script>
104+
HTMLImports.whenReady(function() {
105+
class El extends window.readyMixin(Polymer.Element) {
106+
static get is() { return 'x-foo' }
107+
}
108+
customElements.define(El.is, El);
109+
});
110+
</script>
111+
</dom-module>
112+
113+
<dom-module id="x-ready">
114+
<template>
115+
<x-zot id="a">
116+
<x-zot id="b"></x-zot>
117+
<x-zot id="c">
118+
<x-zot id="d"></x-zot>
119+
</x-zot>
120+
</x-zot>
121+
<x-foo id="foo"></x-foo>
122+
</template>
123+
<script>
124+
HTMLImports.whenReady(function() {
125+
class El extends window.readyMixin(Polymer.Element) {
126+
static get is() { return 'x-ready' }
127+
static get properties() {
128+
return {
129+
foo: {
130+
observer: '_fooChanged'
131+
}
132+
}
133+
}
134+
_fooChanged() {}
135+
}
136+
customElements.define(El.is, El);
137+
138+
});
139+
</script>
140+
</dom-module>
141+
142+
<dom-module id="x-templatized">
143+
<template>
144+
<template is="dom-if" if>
145+
<x-ready foo="[[foo]]"></x-ready>
146+
</template>
147+
</template>
148+
<script>
149+
HTMLImports.whenReady(function() {
150+
class El extends Polymer.Element {
151+
static get is() { return 'x-templatized' }
152+
static get properties() {
153+
return {
154+
foo: {
155+
value: 'foo'
156+
}
157+
}
158+
}
159+
}
160+
customElements.define(El.is, El);
161+
});
162+
</script>
163+
</dom-module>
164+
165+
<script>
166+
167+
suite('ready and attached ordering', function() {
168+
169+
let el;
170+
171+
setup(function() {
172+
window.clearTestLists();
173+
el = document.createElement('x-ready');
174+
document.body.appendChild(el);
175+
});
176+
177+
teardown(function() {
178+
document.body.removeChild(el);
179+
});
180+
181+
test('element dom ready before element', function() {
182+
assert.includeMembers(el.readyList, [el.$.a, el.$.b, el.$.c, el.$.d, el.$.foo]);
183+
var foo = el.$.foo;
184+
assert.includeMembers(foo.readyList, [foo.$.bar1, foo.$.bar1.$.zot, foo.$.bar2, foo.$.bar2.$.zot]);
185+
var b1 = foo.$.bar1, b2 = foo.$.bar2;
186+
assert.includeMembers(b1.readyList, [b1.$.zot]);
187+
assert.includeMembers(b2.readyList, [b2.$.zot]);
188+
});
189+
190+
test('can listen to events fired by element dom in connected', function() {
191+
assert.includeMembers(el._eventList, [el.$.a, el.$.b, el.$.c, el.$.d, el.$.foo]);
192+
var foo = el.$.foo;
193+
assert.includeMembers(foo._eventList, [foo.$.bar1, foo.$.bar2]);
194+
var b1 = foo.$.bar1, b2 = foo.$.bar2;
195+
assert.includeMembers(b1._eventList, [b1.$.zot]);
196+
assert.includeMembers(b2._eventList, [b2.$.zot]);
197+
});
198+
199+
test('shadowRoot available in ready, connected, observer', function() {
200+
[el, el.$.a, el.$.b, el.$.c, el.$.d, el.$.foo,
201+
el.$.foo.$.bar1, el.$.foo.$.bar1.$.zot,
202+
el.$.foo.$.bar2, el.$.foo.$.bar2.$.zot].forEach((e) => {
203+
assert.isTrue(e.observerShadowRoot);
204+
assert.isTrue(e.readyShadowRoot);
205+
assert.isTrue(e.attachedShadowRoot);
206+
});
207+
});
208+
209+
test('element attached called after ready', function() {
210+
assert.equal(window.actualReadyBeforeAtachedList.length, 0);
211+
});
212+
213+
test('element has $ references at attached time', function() {
214+
assert.sameMembers(el.attachedTime$Keys, ['a', 'b', 'c', 'd', 'foo']);
215+
assert.sameMembers(el.$.foo.attachedTime$Keys, ['bar1', 'bar2']);
216+
})
217+
218+
});
219+
220+
suite('templatized: ready and attached ordering', function() {
221+
222+
let container, el;
223+
224+
setup(function() {
225+
window.clearTestLists();
226+
container = document.createElement('x-templatized');
227+
document.body.appendChild(container);
228+
Polymer.flush();
229+
el = container.shadowRoot.querySelector('x-ready');
230+
});
231+
232+
teardown(function() {
233+
document.body.removeChild(container);
234+
});
235+
236+
test('element dom ready before element', function() {
237+
assert.includeMembers(el.readyList, [el.$.a, el.$.b, el.$.c, el.$.d, el.$.foo]);
238+
var foo = el.$.foo;
239+
assert.includeMembers(foo.readyList, [foo.$.bar1, foo.$.bar1.$.zot, foo.$.bar2, foo.$.bar2.$.zot]);
240+
var b1 = foo.$.bar1, b2 = foo.$.bar2;
241+
assert.includeMembers(b1.readyList, [b1.$.zot]);
242+
assert.includeMembers(b2.readyList, [b2.$.zot]);
243+
});
244+
245+
test('can listen to events fired by element dom in connected', function() {
246+
assert.includeMembers(el._eventList, [el.$.a, el.$.b, el.$.c, el.$.d, el.$.foo]);
247+
var foo = el.$.foo;
248+
assert.includeMembers(foo._eventList, [foo.$.bar1, foo.$.bar2]);
249+
var b1 = foo.$.bar1, b2 = foo.$.bar2;
250+
assert.includeMembers(b1._eventList, [b1.$.zot]);
251+
assert.includeMembers(b2._eventList, [b2.$.zot]);
252+
});
253+
254+
test('shadowRoot available in ready, connected, observer', function() {
255+
[el, el.$.a, el.$.b, el.$.c, el.$.d, el.$.foo,
256+
el.$.foo.$.bar1, el.$.foo.$.bar1.$.zot,
257+
el.$.foo.$.bar2, el.$.foo.$.bar2.$.zot].forEach((e) => {
258+
assert.isTrue(e.observerShadowRoot);
259+
assert.isTrue(e.readyShadowRoot);
260+
assert.isTrue(e.attachedShadowRoot);
261+
});
262+
});
263+
264+
test('element attached called after ready', function() {
265+
assert.equal(window.actualReadyBeforeAtachedList.length, 0);
266+
});
267+
268+
test('element has $ references at attached time', function() {
269+
assert.sameMembers(el.attachedTime$Keys, ['a', 'b', 'c', 'd', 'foo']);
270+
assert.sameMembers(el.$.foo.attachedTime$Keys, ['bar1', 'bar2']);
271+
})
272+
273+
});
274+
275+
</script>
276+
</body>
277+
</html>

0 commit comments

Comments
 (0)