diff --git a/lib/mixins/element-mixin.html b/lib/mixins/element-mixin.html index 2e0e8c86ac..30af1ec1a7 100644 --- a/lib/mixins/element-mixin.html +++ b/lib/mixins/element-mixin.html @@ -549,6 +549,7 @@ */ _initializeProperties() { Polymer.telemetry.instanceCount++; + hostStack.registerHost(this); this.constructor.finalize(); const importPath = this.constructor.importPath; // note: finalize template when we have access to `localName` to @@ -598,7 +599,9 @@ if (window.ShadyCSS) { window.ShadyCSS.styleElement(this); } - this._flushProperties(); + if (!this.__dataInitialized) { + this._flushProperties(); + } } /** @@ -616,7 +619,9 @@ */ ready() { if (this._template) { + hostStack.beginHosting(this); this.root = this._stampTemplate(this._template); + hostStack.endHosting(this); } super.ready(); } @@ -745,6 +750,53 @@ return PolymerElement; }); + /** + * Helper api for enqueing client dom created by a host element. + * + * By default elements are flushed via `_flushProperties` when + * `connectedCallback` is called. Elements attach their client dom to + * themselves at `ready` time which results from this first flush. + * This provides an ordering guarantee that the client dom an element + * creates is flushed before the element itself (i.e. client `ready` + * fires before host `ready`). + * + * However, if `_flushProperties` is called *before* an element is connected, + * as for example `Templatize` does, this ordering guarantee cannot be + * satisfied because no elements are connected. (Note: Bound elements that + * receive data do become enqueued clients and are properly ordered but + * unbound elements are not.) + * + * To maintain the desired "client before host" ordering guarantee for this + * case we rely on the "host stack. Client nodes registers themselves with + * the creating host element when created. This ensures that all client dom + * is readied in the proper order, maintaining the desired guarantee. + * + * @private + */ + let hostStack = { + + stack: [], + + registerHost(inst) { + if (this.stack.length) { + let host = this.stack[this.stack.length-1]; + host._enqueueClient(inst); + } + }, + + beginHosting(inst) { + this.stack.push(inst); + }, + + endHosting(inst) { + let stackLen = this.stack.length; + if (stackLen && this.stack[stackLen-1] == inst) { + this.stack.pop(); + } + } + + } + /** * Provides basic tracking of element definitions (registrations) and * instance counts. diff --git a/test/runner.html b/test/runner.html index 86c18d4d2c..9a9f1b003a 100644 --- a/test/runner.html +++ b/test/runner.html @@ -46,6 +46,7 @@ 'unit/case-map.html', 'unit/configure.html', 'unit/ready-attached-order.html', + 'unit/ready-attached-order-class.html', 'unit/attributes.html', 'unit/async.html', 'unit/behaviors.html', diff --git a/test/smoke/ordering-test.html b/test/smoke/ordering-test.html index c46d9a0079..5400425ed8 100644 --- a/test/smoke/ordering-test.html +++ b/test/smoke/ordering-test.html @@ -40,8 +40,8 @@ } connectedCallback() { console.group(this.localName, 'connected'); - super.connectedCallback(); console.warn(this.localName, 'connected (user)', this.shadowRoot); + super.connectedCallback(); console.groupEnd(this.localName, 'connected'); } _flushProperties() { @@ -79,8 +79,8 @@ } connectedCallback() { console.group(this.localName, 'connected'); - super.connectedCallback(); console.warn(this.localName, 'connected (user)', this.shadowRoot); + super.connectedCallback(); console.groupEnd(this.localName, 'connected'); } _flushProperties() { @@ -113,8 +113,8 @@ } connectedCallback() { console.group(this.localName, 'connected'); - super.connectedCallback(); console.warn(this.localName, 'connected (user)', this.shadowRoot); + super.connectedCallback(); console.groupEnd(this.localName, 'connected'); } _flushProperties() { diff --git a/test/unit/ready-attached-order-class.html b/test/unit/ready-attached-order-class.html new file mode 100644 index 0000000000..b675de8e9d --- /dev/null +++ b/test/unit/ready-attached-order-class.html @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/unit/ready-attached-order.html b/test/unit/ready-attached-order.html index ab473f132f..5ac821ed07 100644 --- a/test/unit/ready-attached-order.html +++ b/test/unit/ready-attached-order.html @@ -112,7 +112,33 @@ HTMLImports.whenReady(function() { Polymer({ is: 'x-ready', - behaviors: [window.readyBehavior] + properties: { + foo: { + observer: '_fooChanged' + } + }, + behaviors: [window.readyBehavior], + _fooChanged: function() {} + }); + }); + + + + + + @@ -143,13 +169,59 @@ assert.includeMembers(b2.readyList, [b2.$.zot]); }); - test('element dom connected before element', function() { - assert.includeMembers(el.attachedList, [el.$.a, el.$.b, el.$.c, el.$.d, el.$.foo]); + // test('element dom connected before element', function() { + // assert.includeMembers(el.attachedList, [el.$.a, el.$.b, el.$.c, el.$.d, el.$.foo]); + // var foo = el.$.foo; + // assert.includeMembers(foo.attachedList, [foo.$.bar1, foo.$.bar2]); + // var b1 = foo.$.bar1, b2 = foo.$.bar2; + // assert.includeMembers(b1.attachedList, [b1.$.zot]); + // assert.includeMembers(b2.attachedList, [b2.$.zot]); + // }); + + test('shadowRoot available in ready, connected, observer', function() { + [el, el.$.a, el.$.b, el.$.c, el.$.d, el.$.foo, + el.$.foo.$.bar1, el.$.foo.$.bar1.$.zot, + el.$.foo.$.bar2, el.$.foo.$.bar2.$.zot].forEach((e) => { + assert.isTrue(e.observerShadowRoot); + assert.isTrue(e.readyShadowRoot); + assert.isTrue(e.attachedShadowRoot); + }); + }); + + test('element attached called after ready', function() { + assert.equal(window.actualReadyBeforeAtachedList.length, 0); + }); + + test('element has $ references at attached time', function() { + assert.sameMembers(el.attachedTime$Keys, ['a', 'b', 'c', 'd', 'foo']); + assert.sameMembers(el.$.foo.attachedTime$Keys, ['bar1', 'bar2']); + }) + + }); + + suite('templatized: ready and attached ordering', function() { + + let container, el; + + setup(function() { + window.clearTestLists(); + container = document.createElement('x-templatized'); + document.body.appendChild(container); + Polymer.flush(); + el = container.shadowRoot.querySelector('x-ready'); + }); + + teardown(function() { + document.body.removeChild(container); + }); + + test('element dom ready before element', function() { + assert.includeMembers(el.readyList, [el.$.a, el.$.b, el.$.c, el.$.d, el.$.foo]); var foo = el.$.foo; - assert.includeMembers(foo.attachedList, [foo.$.bar1, foo.$.bar2]); + assert.includeMembers(foo.readyList, [foo.$.bar1, foo.$.bar1.$.zot, foo.$.bar2, foo.$.bar2.$.zot]); var b1 = foo.$.bar1, b2 = foo.$.bar2; - assert.includeMembers(b1.attachedList, [b1.$.zot]); - assert.includeMembers(b2.attachedList, [b2.$.zot]); + assert.includeMembers(b1.readyList, [b1.$.zot]); + assert.includeMembers(b2.readyList, [b2.$.zot]); }); test('shadowRoot available in ready, connected, observer', function() { diff --git a/test/unit/shady-dynamic.html b/test/unit/shady-dynamic.html index 84837131b9..c935b92afa 100644 --- a/test/unit/shady-dynamic.html +++ b/test/unit/shady-dynamic.html @@ -131,7 +131,7 @@ - +