From 344f5cc1b2d5cc52b6b741f726aefb4f9f9e808a Mon Sep 17 00:00:00 2001 From: Steven Orvell Date: Wed, 14 Oct 2015 11:50:11 -0700 Subject: [PATCH] ensure distribution observers see all changes that can come from attributes under native Shadow DOM; +minor factoring --- ...> dom-api-distributed-nodes-observer.html} | 20 +++-- ... => dom-api-effective-nodes-observer.html} | 30 +++++-- src/lib/dom-api.html | 28 +++--- src/mini/shady.html | 13 ++- test/unit/polymer-dom-observeNodes.html | 90 +++++++++++++++++++ 5 files changed, 143 insertions(+), 38 deletions(-) rename src/lib/{dom-api-observe-distributed-nodes.html => dom-api-distributed-nodes-observer.html} (74%) rename src/lib/{dom-api-observe-nodes.html => dom-api-effective-nodes-observer.html} (84%) diff --git a/src/lib/dom-api-observe-distributed-nodes.html b/src/lib/dom-api-distributed-nodes-observer.html similarity index 74% rename from src/lib/dom-api-observe-distributed-nodes.html rename to src/lib/dom-api-distributed-nodes-observer.html index e8fe88746f..00f6512222 100644 --- a/src/lib/dom-api-observe-distributed-nodes.html +++ b/src/lib/dom-api-distributed-nodes-observer.html @@ -16,20 +16,20 @@ var Settings = Polymer.Settings; /** - * DomApi.ObserveDistributedNodes notifies when the list returned by + * DomApi.DistributedNodesObserver notifies when the list returned by * a element's `getDistributedNodes()` may have changed. * It is not meant to be used directly; it is used by * `Polymer.dom(node).observeNodes(callback)` to observe changes to * `.getDistributedNodes()`. */ - DomApi.ObserveDistributedNodes = function(domApi) { - DomApi.ObserveNodes.call(this, domApi); + DomApi.DistributedNodesObserver = function(domApi) { + DomApi.EffectiveNodesObserver.call(this, domApi); }; - DomApi.ObserveDistributedNodes.prototype = - Object.create(DomApi.ObserveNodes.prototype); + DomApi.DistributedNodesObserver.prototype = + Object.create(DomApi.EffectiveNodesObserver.prototype); - Polymer.Base.extend(DomApi.ObserveDistributedNodes.prototype, { + Polymer.Base.extend(DomApi.DistributedNodesObserver.prototype, { // NOTE: ShadyDOM distribute provokes notification of these observers // so no setup is required. @@ -49,7 +49,7 @@ if (Settings.useShadow) { - Polymer.Base.extend(DomApi.ObserveDistributedNodes.prototype, { + Polymer.Base.extend(DomApi.DistributedNodesObserver.prototype, { // NOTE: Under ShadowDOM we must observe the host element for // changes. @@ -60,9 +60,13 @@ if (host) { this._observer = Polymer.dom(host).observeNodes( this._scheduleNotify.bind(this)); + // NOTE: we identify this listener as needed for + // notification so that enableShadowAttributeTracking + // can find these observers an ensure that we pass always + // pass notifications down. + this._observer._isContentListener = true; if (this._hasAttrSelect()) { Polymer.dom(host).observer.enableShadowAttributeTracking(); - this._observer._alwaysCallListener = true; } } } diff --git a/src/lib/dom-api-observe-nodes.html b/src/lib/dom-api-effective-nodes-observer.html similarity index 84% rename from src/lib/dom-api-observe-nodes.html rename to src/lib/dom-api-effective-nodes-observer.html index e8432a0adb..a4d41f70f4 100644 --- a/src/lib/dom-api-observe-nodes.html +++ b/src/lib/dom-api-effective-nodes-observer.html @@ -17,18 +17,19 @@ var hasDomApi = Polymer.DomApi.hasDomApi; /** - * DomApi.ObserveNodes tracks changes to an element's effective child nodes, - * the same list returned from `Polymer.dom(node).getEffectiveChildNodes()`. + * DomApi.EffectiveNodesObserver tracks changes to an element's + * effective child nodes, the same list returned from + * `Polymer.dom(node).getEffectiveChildNodes()`. * It is not meant to be used directly; it is used by * `Polymer.dom(node).observeNodes(callback)` to observe changes. */ - DomApi.ObserveNodes = function(domApi) { + DomApi.EffectiveNodesObserver = function(domApi) { this.domApi = domApi; this.node = this.domApi.node; this._listeners = []; }; - DomApi.ObserveNodes.prototype = { + DomApi.EffectiveNodesObserver.prototype = { addListener: function(callback) { if (!this._isSetup) { @@ -132,7 +133,7 @@ var nodes = this._getEffectiveNodes(); for (var i=0, o; (i < o$.length) && (o=o$[i]); i++) { var info = this._generateListenerInfo(o, nodes); - if (info || o._alwaysCallListener) { + if (info || o._alwaysNotify) { this._callListener(o, info); } } @@ -182,13 +183,13 @@ if (Settings.useShadow) { - var baseSetup = DomApi.ObserveNodes.prototype._setup; - var baseCleanup = DomApi.ObserveNodes.prototype._cleanup; + var baseSetup = DomApi.EffectiveNodesObserver.prototype._setup; + var baseCleanup = DomApi.EffectiveNodesObserver.prototype._cleanup; - var beforeCallListeners = DomApi.ObserveNodes + var beforeCallListeners = DomApi.EffectiveNodesObserver .prototype._beforeCallListeners; - Polymer.Base.extend(DomApi.ObserveNodes.prototype, { + Polymer.Base.extend(DomApi.EffectiveNodesObserver.prototype, { _setup: function() { if (!this._observer) { @@ -228,6 +229,10 @@ enableShadowAttributeTracking: function() { if (this._observer) { + // provoke all listeners needed for observation + // to always call listeners when no-op changes occur (which may + // affect lower distributions. + this._makeContentListenersAlwaysNotify(); this._observer.disconnect(); this._observer.observe(this.node, { childList: true, @@ -240,6 +245,13 @@ Polymer.dom(host).observer.enableShadowAttributeTracking(); } } + }, + + _makeContentListenersAlwaysNotify: function() { + for (var i=0, h; i < this._listeners.length ; i++) { + h = this._listeners[i]; + h._alwaysNotify = h._isContentListener; + } } }); diff --git a/src/lib/dom-api.html b/src/lib/dom-api.html index f85e7cf02c..9a7b43fea4 100644 --- a/src/lib/dom-api.html +++ b/src/lib/dom-api.html @@ -116,9 +116,7 @@ if (addedInsertionPoint) { this._updateInsertionPoints(root.host); } - if (this.observer) { - this.observer.notify(); - } + this.notifyObserver(); return node; }, @@ -142,9 +140,7 @@ nativeRemoveChild.call(container, node); } } - if (this.observer) { - this.observer.notify(); - } + this.notifyObserver(); return node; }, @@ -280,10 +276,7 @@ _removeNodeFromParent: function(node) { var parent = node._lightParent; if (parent && hasDomApi(parent)) { - var d = factory(parent); - if (d._observer) { - d._observer.removeNode(node); - } + factory(parent).notifyObserver(); } this._removeNodeFromHost(node, true); }, @@ -478,7 +471,10 @@ var c$ = this.childNodes; for (var i=0, l=c$.length, c; (i - - + + + + + + +
A
B
@@ -467,6 +480,38 @@ document.body.removeChild(el); }); + test('observe effective children changes when adding to another host', function() { + var el = document.createElement('test-content1'); + document.body.appendChild(el); + var recorded; + var handle = Polymer.dom(el.$.content).observeNodes(function(info) { + recorded = info; + }); + Polymer.dom.flush(); + // add + var d = document.createElement('div'); + var d1 = document.createElement('div'); + Polymer.dom(el).appendChild(d); + Polymer.dom(el).appendChild(d1); + Polymer.dom.flush(); + assert.equal(recorded.addedNodes.length, 2); + assert.equal(recorded.removedNodes.length, 0); + assert.equal(recorded.addedNodes[0], d); + assert.equal(recorded.addedNodes[1], d1); + // add somewhere else... we should see these as removes + Polymer.dom(document.body).appendChild(d); + Polymer.dom(document.body).appendChild(d1); + Polymer.dom.flush(); + assert.equal(recorded.addedNodes.length, 0); + assert.equal(recorded.removedNodes.length, 2); + assert.equal(recorded.removedNodes[0], d); + assert.equal(recorded.removedNodes[1], d1); + // cleanup + Polymer.dom(document.body).removeChild(d); + Polymer.dom(document.body).removeChild(d1); + document.body.removeChild(el); + }); + test('observe effective children inside deep distributing element', function() { var el = document.createElement('test-content3'); document.body.appendChild(el); @@ -651,6 +696,51 @@ }); }); + test('observe effective children attr changes inside deep distributing element without outer select (async)', function(done) { + var el = document.createElement('test-content-attr-inside'); + document.body.appendChild(el); + + var recorded; + var content = el.$.content.$.content.$.content.$.content; + var handle = Polymer.dom(content).observeNodes(function(info) { + recorded = info; + }, {attributes: true}); + Polymer.dom.flush(); + recorded = null; + // add + var d = document.createElement('div'); + var d1 = document.createElement('div'); + Polymer.dom(el).appendChild(d); + Polymer.dom(el).appendChild(d1); + Polymer.dom.flush(); + assert.equal(recorded, null); + Polymer.dom(d).setAttribute('a', ''); + Polymer.dom(d).setAttribute('b', ''); + setTimeout(function() { + assert.equal(recorded, null); + Polymer.dom(d).setAttribute('c', ''); + setTimeout(function() { + assert.equal(recorded.addedNodes.length, 1); + assert.equal(recorded.removedNodes.length, 0); + assert.equal(recorded.addedNodes[0], d); + Polymer.dom(d).removeAttribute('c'); + setTimeout(function() { + assert.equal(recorded.addedNodes.length, 0); + assert.equal(recorded.removedNodes.length, 1); + assert.equal(recorded.removedNodes[0], d); + recorded = null; + Polymer.dom(content).unobserveNodes(handle); + Polymer.dom(d).setAttribute('c', ''); + setTimeout(function() { + assert.equal(recorded, null); + document.body.removeChild(el); + done(); + }); + }); + }); + }); + }); + test('add/remove multiple observers', function() { var el = document.createElement('test-content1'); document.body.appendChild(el);