From bb90576a0009c3d8758171f3958160c69c1e9466 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Tue, 13 Aug 2013 16:11:38 -0400 Subject: [PATCH] Make attribute dirtying smarter --- src/ShadowRenderer.js | 106 ++++++++++++++++++++++++--------- src/wrappers/Element.js | 49 +++++++++++++-- test/js/test.js | 129 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 33 deletions(-) diff --git a/src/ShadowRenderer.js b/src/ShadowRenderer.js index af5c8f1..87a1f4a 100644 --- a/src/ShadowRenderer.js +++ b/src/ShadowRenderer.js @@ -164,33 +164,6 @@ } } - // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#dfn-distribution-algorithm - function distribute(tree, pool) { - var anyRemoved = false; - - visit(tree, isActiveInsertionPoint, - function(insertionPoint) { - resetDistributedChildNodes(insertionPoint); - for (var i = 0; i < pool.length; i++) { // 1.2 - var node = pool[i]; // 1.2.1 - if (node === undefined) // removed - continue; - if (matchesCriteria(node, insertionPoint)) { // 1.2.2 - distributeChildToInsertionPoint(node, insertionPoint); // 1.2.2.1 - pool[i] = undefined; // 1.2.2.2 - anyRemoved = true; - } - } - }); - - if (!anyRemoved) - return pool; - - return pool.filter(function(item) { - return item !== undefined; - }); - } - // Matching Insertion Points // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#matching-insertion-points @@ -278,6 +251,7 @@ function ShadowRenderer(host) { this.host = host; this.dirty = false; + this.invalidateAttributes(); this.associateNode(host); } @@ -290,14 +264,18 @@ return renderer; } + ShadowRenderer.prototype = { + // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#rendering-shadow-trees render: function() { if (!this.dirty) return; - var host = this.host; + this.invalidateAttributes(); this.treeComposition(); + + var host = this.host; var shadowDOM = host.shadowRoot; if (!shadowDOM) return; @@ -389,6 +367,76 @@ }, this); }, + + /** + * Invalidates the attributes used to keep track of which attributes may + * cause the renderer to be invalidated. + */ + invalidateAttributes: function() { + this.attributes = Object.create(null); + }, + + /** + * Parses the selector and makes this renderer dependent on the attribute + * being used in the selector. + * @param {string} selector + */ + updateDependentAttributes: function(selector) { + if (!selector) + return; + + var attributes = this.attributes; + + // .class + if (/\.\w+/.test(selector)) + attributes['class'] = true; + + // #id + if (/#\w+/.test(selector)) + attributes['id'] = true; + + selector.replace(/\[\s*([^\s=\|~\]]+)/g, function(_, name) { + attributes[name] = true; + }); + + // Pseudo selectors have been removed from the spec. + }, + + dependsOnAttribute: function(name) { + return this.attributes[name]; + }, + + // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#dfn-distribution-algorithm + distribute: function(tree, pool) { + var anyRemoved = false; + var self = this; + + visit(tree, isActiveInsertionPoint, + function(insertionPoint) { + resetDistributedChildNodes(insertionPoint); + self.updateDependentAttributes( + insertionPoint.getAttribute('select')); + + for (var i = 0; i < pool.length; i++) { // 1.2 + var node = pool[i]; // 1.2.1 + if (node === undefined) // removed + continue; + if (matchesCriteria(node, insertionPoint)) { // 1.2.2 + distributeChildToInsertionPoint(node, insertionPoint); // 1.2.2.1 + pool[i] = undefined; // 1.2.2.2 + anyRemoved = true; + } + } + }); + + if (!anyRemoved) + return pool; + + return pool.filter(function(item) { + return item !== undefined; + }); + }, + // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#dfn-tree-composition treeComposition: function () { var shadowHost = this.host; @@ -417,7 +465,7 @@ }); point = shadowInsertionPoint; - pool = distribute(tree, pool); // 4.2. + pool = this.distribute(tree, pool); // 4.2. if (point) { // 4.3. var nextOlderTree = tree.olderShadowRoot; // 4.3.1. if (!nextOlderTree) { diff --git a/src/wrappers/Element.js b/src/wrappers/Element.js index ea64d41..f340707 100644 --- a/src/wrappers/Element.js +++ b/src/wrappers/Element.js @@ -24,6 +24,21 @@ OriginalElement.prototype.msMatchesSelector || OriginalElement.prototype.webkitMatchesSelector; + + function invalidateRendererBasedOnAttribute(element, name) { + // Only invalidate if parent node is a shadow host. + var p = element.parentNode; + if (!p) + return; + + var renderer = scope.getRendererForHost(p); + if (!renderer) + return; + + if (renderer.dependsOnAttribute(name)) + renderer.invalidate(); + } + function Element(node) { Node.call(this, node); } @@ -46,10 +61,12 @@ setAttribute: function(name, value) { this.impl.setAttribute(name, value); - // This is a bit agressive. We need to invalidate if it affects - // the rendering content[select] or if it effects the value of a content - // select. - this.invalidateShadowRenderer(); + invalidateRendererBasedOnAttribute(this, name); + }, + + removeAttribute: function(name) { + this.impl.removeAttribute(name); + invalidateRendererBasedOnAttribute(this, name); }, matches: function(selector) { @@ -62,6 +79,28 @@ Element.prototype.createShadowRoot; } + /** + * Useful for generating the accessor pair for a property that reflects an + * attribute. + */ + function setterDirtiesAttribute(prototype, propertyName, opt_attrName) { + var attrName = opt_attrName || propertyName; + Object.defineProperty(prototype, propertyName, { + get: function() { + return this.impl[propertyName]; + }, + set: function(v) { + this.impl[propertyName] = v; + invalidateRendererBasedOnAttribute(this, attrName); + }, + configurable: true, + enumerable: true + }); + } + + setterDirtiesAttribute(Element.prototype, 'id'); + setterDirtiesAttribute(Element.prototype, 'className', 'class'); + mixin(Element.prototype, ChildNodeInterface); mixin(Element.prototype, GetElementsByInterface); mixin(Element.prototype, ParentNodeInterface); @@ -69,5 +108,7 @@ registerWrapper(OriginalElement, Element); + // TODO(arv): Export setterDirtiesAttribute and apply it to more bindings + // that reflect attributes. scope.wrappers.Element = Element; })(this.ShadowDOMPolyfill); diff --git a/test/js/test.js b/test/js/test.js index c79a370..5152055 100644 --- a/test/js/test.js +++ b/test/js/test.js @@ -6,6 +6,7 @@ suite('Shadow DOM', function() { + var getRendererForHost = ShadowDOMPolyfill.getRendererForHost; var unwrap = ShadowDOMPolyfill.unwrap; function getVisualInnerHtml(el) { @@ -258,4 +259,132 @@ suite('Shadow DOM', function() { }); + suite('Tracking attributes', function() { + + test('attribute selector', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + + var calls = 0; + var renderer = getRendererForHost(host); + var originalRender = renderer.render; + renderer.render = function() { + calls++; + originalRender.call(this); + }; + + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 1); + + a.setAttribute('foo', 'bar'); + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 2); + + a.setAttribute('foo', ''); + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 3); + + a.removeAttribute('foo'); + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 4); + + a.setAttribute('bar', ''); + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 4); + }); + + test('id selector', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + + var calls = 0; + var renderer = getRendererForHost(host); + var originalRender = renderer.render; + renderer.render = function() { + calls++; + originalRender.call(this); + }; + + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 1); + + a.setAttribute('foo', 'bar'); + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 1); + + a.setAttribute('id', 'a'); + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 2); + + a.removeAttribute('foo'); + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 2); + + a.id = 'b'; + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 3); + + a.id = 'a'; + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 4); + + a.id = null; + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 5); + }); + + test('class selector', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + + var calls = 0; + var renderer = getRendererForHost(host); + var originalRender = renderer.render; + renderer.render = function() { + calls++; + originalRender.call(this); + }; + + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 1); + + a.setAttribute('foo', 'bar'); + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 1); + + a.setAttribute('class', 'a'); + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 2); + + a.removeAttribute('foo'); + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 2); + + a.className = 'b'; + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 3); + + a.className = 'a'; + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 4); + + a.className = null; + assert.equal(getVisualInnerHtml(host), ''); + assert.equal(calls, 5); + }); + + }); + }); \ No newline at end of file