From be4413fd76cf853830f9c64e425bcc02e30b5aff Mon Sep 17 00:00:00 2001 From: "Scott J. Miles" Date: Fri, 13 Feb 2015 18:11:13 -0800 Subject: [PATCH 01/14] possible fix for [0.8-preview] #1192 --- src/lib/template/templatizer.html | 70 +++++++++++++++++++++---------- src/lib/template/x-repeat.html | 35 +++++++++------- 2 files changed, 68 insertions(+), 37 deletions(-) diff --git a/src/lib/template/templatizer.html b/src/lib/template/templatizer.html index 5f1feec182..bec168f427 100644 --- a/src/lib/template/templatizer.html +++ b/src/lib/template/templatizer.html @@ -15,50 +15,55 @@ Templatizer = { templatize: function(template) { - if (template._content && template._content._ctor) { + // TODO(sjmiles): supply _alternate_ content reference missing from root + // templates (not nested). `_content` exists to provide content sharing + // for nested templates. + if (!template._content) { + template._content = template.content; + } + // fast path if template's anonymous class has been memoized + if (template._content._ctor) { this.ctor = template._content._ctor; //console.log('Templatizer.templatize: using memoized archetype'); return; } - // - var host = this.host; + // `archetype` is the prototype of the anonymous + // class created by the templatizer var archetype = Object.create(Base); - // - archetype._template = template; + // normally Annotations.parseAnnotations(template) but + // archetypes do special caching this.customPrepAnnotations(archetype, template); + // setup accessors archetype._prepEffects(); + // late-binds archetype.listen to host.listen; h.l doesn't exist yet + archetype.listen = function() { + this.listen.apply(this, arguments); + }.bind(this.host); + /* + var host = this.host; archetype.listen = function() { host.listen.apply(host, arguments); }; - // + */ + // boilerplate code + archetype._notifyPath = this._notifyPathImpl; + // boilerplate code + var _constructor = this._constructorImpl; var ctor = function() { - this._setupConfigure(); - this.root = this.instanceTemplate(this._template); - this._marshalTemplateContent(); - this._marshalAnnotatedNodes(); - this._marshalInstanceEffects(); - this._marshalAnnotatedListeners(); - this._ready(); - this._notifyPath = function() { - var pd = this.pathDelegate; - if (pd) { - var args = Array.prototype.slice.call(arguments); - args.unshift(this); - pd._notifyDelegatePath.apply(pd, args); - } - }; + _constructor.call(this); }; - // + // standard references ctor.prototype = archetype; archetype.constructor = ctor; + // TODO(sjmiles): constructor cache? template._content._ctor = ctor; - // // TODO(sjmiles): choose less general name this.ctor = ctor; }, customPrepAnnotations: function(archetype, template) { if (template) { + archetype._template = template; var c = template._content; if (c) { archetype._annotes = c._annotes || @@ -74,6 +79,25 @@ } }, + _notifyPathImpl: function() { + var pd = this.pathDelegate; + if (pd) { + var args = Array.prototype.slice.call(arguments); + args.unshift(this); + pd._notifyDelegatePath.apply(pd, args); + } + }, + + _constructorImpl: function() { + this._setupConfigure(); + this.root = this.instanceTemplate(this._template); + this._marshalTemplateContent(); + this._marshalAnnotatedNodes(); + this._marshalInstanceEffects(); + this._marshalAnnotatedListeners(); + this._ready(); + }, + stamp: function(model) { var instance = new this.ctor(); if (model) { diff --git a/src/lib/template/x-repeat.html b/src/lib/template/x-repeat.html index dcfa48f562..60de27f0de 100644 --- a/src/lib/template/x-repeat.html +++ b/src/lib/template/x-repeat.html @@ -41,6 +41,10 @@ 'items.*': 'itemsChanged' }, + created: function() { + this.boundArrayObserver = this.arrayObserver.bind(this); + }, + attached: function() { if (!this.ctor) { this.templatize(this); @@ -55,22 +59,25 @@ itemsChanged: function(items, old, path) { if (this.isAttached) { - if (path) { - this.notifyElement(path, items); - } else { - if (old) { - this.unobserveArray(old); - } - if (items) { - this.observeArray(items); - } - this.render(); + this._itemsChanged(items, old, path); + } + }, + + _itemsChanged: function(items, old, path) { + if (path) { + this.notifyElement(path, items); + } else { + if (old) { + this.unobserveArray(old); + } + if (items) { + this.observeArray(items); } + this.render(); } }, observeArray: function(items) { - this.boundArrayObserver = this.boundArrayObserver || this.arrayObserver.bind(this); ArrayObserve.observe(items, this.boundArrayObserver); }, @@ -136,11 +143,11 @@ createRows: function(items, parent) { var rows = []; - for (var i=0, row, item; item=items[i]; i++) { - row = this.generateRow(i, item); + for (var i=0, l=items.length, row; i Date: Sat, 14 Feb 2015 00:18:07 -0800 Subject: [PATCH 02/14] Add collection-based x-repeat. --- src/lib/collection.html | 155 +++++++++++++++++++++++++ src/lib/template/x-repeat.html | 201 +++++++++++++++++++++------------ 2 files changed, 282 insertions(+), 74 deletions(-) create mode 100644 src/lib/collection.html diff --git a/src/lib/collection.html b/src/lib/collection.html new file mode 100644 index 0000000000..7cd5bfdec9 --- /dev/null +++ b/src/lib/collection.html @@ -0,0 +1,155 @@ + + + + + + diff --git a/src/lib/template/x-repeat.html b/src/lib/template/x-repeat.html index 60de27f0de..d133feae40 100644 --- a/src/lib/template/x-repeat.html +++ b/src/lib/template/x-repeat.html @@ -10,10 +10,11 @@ + \ No newline at end of file diff --git a/src/lib/template/x-repeat.html b/src/lib/template/x-repeat.html index 03de5ea8b0..00553f8c20 100644 --- a/src/lib/template/x-repeat.html +++ b/src/lib/template/x-repeat.html @@ -131,15 +131,11 @@ return; } if (this.host) { - // this.host.localDom.batch(function() { - // this._render(splices); - // }, this); this.host.localDom.batch(); this._render(splices); - debounceDistribute = Debounce.call(this, debounceDistribute, function() { - console.log('distribute'); - this.host.localDom.distribute(); - }.bind(this)); + this.host.debounce('distribute', function() { + this.localDom.distribute(); + }); } else { this._render(splices); } diff --git a/test/unit/content.html b/test/unit/content.html index b283e7edfc..cef2e55ca1 100644 --- a/test/unit/content.html +++ b/test/unit/content.html @@ -92,6 +92,16 @@ 'a b', '', 'ba'); + + testRender('select .b .a 2', + 'a b c', + '', + 'bca'); + + testRender('select [c] *', + 'abcd', + '', + 'cabd'); }); diff --git a/test/unit/projection-elements.html b/test/unit/projection-elements.html index 90e88d6016..75de597fae 100644 --- a/test/unit/projection-elements.html +++ b/test/unit/projection-elements.html @@ -21,7 +21,11 @@ diff --git a/test/unit/projection.js b/test/unit/projection.js index 138600a69f..5bae5235db 100644 --- a/test/unit/projection.js +++ b/test/unit/projection.js @@ -167,6 +167,21 @@ suite('projection', function() { assert.equal(rere.localDom.querySelectorAll('#local').length, 0); }); + test('localDom.insertBefore first element results in minimal change', function() { + var test = document.querySelector('x-test'); + var children = test.localDom.children(); + var rere = test.localDom.querySelector('x-rereproject'); + assert.equal(rere.attachedCount, 1); + var s = document.createElement('span'); + s.id = 'local-first'; + s.textContent = 'Local First'; + test.localDom.insertBefore(s, children[0]); + assert.equal(test.localDom.querySelector('#local-first'), s); + assert.equal(rere.attachedCount, 1); + test.localDom.removeChild(s); + assert.equal(rere.attachedCount, 1); + }); + test('localDom.appendChild (fragment)', function() { var test = document.querySelector('x-test'); var rere = test.localDom.querySelector('x-rereproject'); From 66d84056453d1c7cdafedfdcd46df39c500408c5 Mon Sep 17 00:00:00 2001 From: Kevin Schaaf Date: Sat, 21 Feb 2015 03:40:37 -0800 Subject: [PATCH 09/14] Improve add/remove render efficiency for array and view sort cases. --- src/lib/collection.html | 33 +++++-- src/lib/template/x-repeat.html | 172 +++++++++++++++++++++++++++------ 2 files changed, 169 insertions(+), 36 deletions(-) diff --git a/src/lib/collection.html b/src/lib/collection.html index 2e7768b4e5..5b77e39ef8 100644 --- a/src/lib/collection.html +++ b/src/lib/collection.html @@ -24,6 +24,8 @@ this.callbacks = []; this.debounce = null; this.map = null; + this.added = []; + this.removed = []; if (!noObserve) { ArrayObserve.observe(userArray, this.applySplices.bind(this)); @@ -48,30 +50,49 @@ } }, - add: function(item, batch) { + add: function(item, squelch) { var key = this.store.push(item) - 1; if (this.map) { this.map.set(item, key); } - if (!batch) { + if (!squelch) { + this.added.push(key); this.debounce = Debounce(this.debounce, this.notify.bind(this)); } return key; }, - remove: function(item, batch) { - var key = this.getKey(item); + removeKey: function(key) { + if (this.map) { + this.map.delete(this.store[key]); + } delete this.store[key]; + this.removed.push(key); + this.debounce = Debounce(this.debounce, this.notify.bind(this)); + }, + + remove: function(item, squelch) { + var key = this.getKey(item); if (this.map) { this.map.delete(item); } - if (!batch) { + delete this.store[key]; + if (!squelch) { + this.removed.push(key); this.debounce = Debounce(this.debounce, this.notify.bind(this)); } return key; }, notify: function(splices) { + if (!splices) { + splices = [{ + added: this.added, + removed: this.removed + }]; + this.added = []; + this.removed = []; + } this.callbacks.forEach(function(cb) { cb(splices); }, this); @@ -97,7 +118,7 @@ this.store[key] = value; }, - getItem: function(key) { + getItem: function(key) { return this.store[key]; }, diff --git a/src/lib/template/x-repeat.html b/src/lib/template/x-repeat.html index 00553f8c20..dabfffa762 100644 --- a/src/lib/template/x-repeat.html +++ b/src/lib/template/x-repeat.html @@ -143,56 +143,167 @@ _render: function(splices) { var c = this.collection; - var rowForKey = this._rowForKey = {}; - var keys; - if (!this._sortFn && splices) { - keys = this._orderedKeys; - this._applySplices(splices); + if (splices) { + if (this._sortFn) { + this._applySplicesViewSort(splices); + } else { + this._applySplicesArraySort(splices); + } } else { - keys = this._orderedKeys = c.getKeys(); - } - // Filter - if (this._filterFn) { - keys = keys.filter(function(a, b) { - return this._filterFn(c.getItem(a), c.getItem(b)); - }, this); + this._sortAndFilter(); } - // Sort - if (this._sortFn) { - keys.sort(function(a, b) { - return this._sortFn(c.getItem(a), c.getItem(b)); - }.bind(this)); - } - // Assign + var rowForKey = this._rowForKey = {}; + var keys = this._orderedKeys; + // Assign items and keys this.rows = this.rows || []; - for (var i=0; i=0 ; i--) { + var idx = removedRows[i]; + pool.push(this._detachRow(idx)); + rows.splice(idx, 1); + keys.splice(idx, 1); + } + } + if (addedKeys.length) { + // Filter added keys + if (this._filterFn) { + addedKeys = addedKeys.filter(function(a) { + return this._filterFn(c.getItem(a)); + }, this); + } + // Sort added keys + addedKeys.sort(function(a, b) { + return this._sortFn(c.getItem(a), c.getItem(b)); + }, this); + // Insert new rows using sort (from pool or newly created) + var start = 0; + for (i=0; i> 1; + var midKey = this._orderedKeys[mid]; + var cmp = this._sortFn(c.getItem(midKey), item); + if (cmp < 0) { + start = mid + 1; + } else if (cmp > 0) { + end = mid - 1; + } else { + idx = mid; + break; + } + } + if (idx < 0) { + idx = end + 1; + } + // Insert key & row at insertion point + this._orderedKeys.splice(idx, 0, key); + this.rows.splice(idx, 0, this._insertRow(idx, pool)); + return idx; + }, + + _applySplicesArraySort: function(splices) { + var keys = this._orderedKeys; + var pool = []; + splices.forEach(function(s) { + // Remove & pool rows first, to ensure we can fully reuse removed rows + for (var i=0; i Date: Mon, 23 Feb 2015 07:41:11 -0800 Subject: [PATCH 10/14] content optimizations --- src/features/mini/content.html | 48 +++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/features/mini/content.html b/src/features/mini/content.html index 3a5f0eaf33..10a57dbadd 100644 --- a/src/features/mini/content.html +++ b/src/features/mini/content.html @@ -54,12 +54,15 @@ _distributeContent: function() { // logically distribute self + this._distributionClean = false; this._prepareContent(); // now fully distribute/compose "clients" var c$ = this._getDistributionClients(); for (var i=0, l= c$.length, c; (i Date: Mon, 23 Feb 2015 07:58:53 -0800 Subject: [PATCH 11/14] content: slight factoring --- src/features/mini/content.html | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/features/mini/content.html b/src/features/mini/content.html index 10a57dbadd..8cf035a0bc 100644 --- a/src/features/mini/content.html +++ b/src/features/mini/content.html @@ -440,10 +440,9 @@ } if (container.__skipDistribute) { if (container === this.node) { - this.host.appendChild(node); - } else { - container.appendChild(node); + container === this.host; } + container.appendChild(node); } else { this._dirtyHost(); } @@ -482,10 +481,9 @@ } if (container.__skipDistribute) { if (container === this.node) { - this.host.insertBefore(node, ref_node); - } else { - container.insertBefore(node, ref_node); + container = this.host; } + container.insertBefore(node, ref_node); } else { this._dirtyHost(); } @@ -515,10 +513,9 @@ node.lightParent = null; if (container.__skipDistribute) { if (container === this.node) { - this.host.removeChild(node); - } else { - container.removeChild(node); + container = this.host; } + container.removeChild(node); } else { this._dirtyHost(); } From 7dfbe5d961e3c6c5b71e424ca30ea8b0dda198ea Mon Sep 17 00:00:00 2001 From: Steve Orvell Date: Mon, 23 Feb 2015 11:02:56 -0800 Subject: [PATCH 12/14] content: refine when distribution can be avoided. --- src/features/mini/content.html | 41 +++++++++++++++++++++++++++++----- src/features/mini/ready.html | 1 + 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/features/mini/content.html b/src/features/mini/content.html index 8cf035a0bc..a54699b6ca 100644 --- a/src/features/mini/content.html +++ b/src/features/mini/content.html @@ -54,7 +54,6 @@ _distributeContent: function() { // logically distribute self - this._distributionClean = false; this._prepareContent(); // now fully distribute/compose "clients" var c$ = this._getDistributionClients(); @@ -62,11 +61,12 @@ c._distributeContent(); } this._composeContent(); - this._distributionClean = true; }, _prepareContent: function() { if (this._useContent) { + //console.log(this.is, 'dirty!'); + this._distributionClean = false; // reset distributions this._resetDistribution(this.shadyRoot); // compute which nodes should be distributed where @@ -80,6 +80,8 @@ // compose self if (this._useContent) { this._composeTree(this); + this._distributionClean = true; + //console.log(this.is, 'clean!'); } else if (this.root !== this) { this.appendChild(this.root); this.root = this; @@ -421,8 +423,33 @@ this.host._distributionClean = false; }, + // Distribution can be avoided when: + // 1. a container is not a custom element or is a custom element without + // a shadyRoot. + // 2. a container has been run through distribution and marked as not + // needing distribution (not a parent of an insertion point). + // 3. a container expicitly has the `skipDistribute` attribute. NOTE: + // this is experimental and exists so that appending a doc fragment with + // no insertion points can be fast pathed. If this is a good idea, then + // it's definitely something the annotator should help with. + _skipDistribution: function(container) { + // custom elements must distribute unless they don't have shady + // (marked via _useContent) + if (container._distributeContent) { + return !container._useContent; + } else { + // marked via distribution system + return (container.__skipDistribute || + // marked explicitly + // TODO(sorvell): temporary, the annotator should help with this + (container.hasAttribute && + container.hasAttribute('skipDistribute'))); + } + }, + appendChild: function(node, container) { container = container || this.node; + this.host._elementAdd(node); if (this.host._useContent) { saveLightChildrenIfNeeded(container); var children = this.children(container); @@ -438,9 +465,9 @@ children.push(node); node.lightParent = container; } - if (container.__skipDistribute) { + if (this._skipDistribution(container)) { if (container === this.node) { - container === this.host; + container = this.host; } container.appendChild(node); } else { @@ -456,6 +483,7 @@ // TODO(sorvell): implement skipDistribute for other methods!! insertBefore: function(node, ref_node, container) { container = container || this.node; + this.host._elementAdd(node); if (ref_node) { if (this.host._useContent) { saveLightChildrenIfNeeded(container); @@ -479,7 +507,7 @@ children.splice(index, 0, node); node.lightParent = container; } - if (container.__skipDistribute) { + if (this._skipDistribution(container)) { if (container === this.node) { container = this.host; } @@ -503,6 +531,7 @@ */ removeChild: function(node, container) { container = container || this.node; + this.host._elementRemove(node); if (this.host._useContent) { var children = this.children(container); var index = children.indexOf(node); @@ -511,7 +540,7 @@ } children.splice(index, 1); node.lightParent = null; - if (container.__skipDistribute) { + if (this._skipDistribution(container)) { if (container === this.node) { container = this.host; } diff --git a/src/features/mini/ready.html b/src/features/mini/ready.html index 6c0a260d81..94e3ec2a32 100644 --- a/src/features/mini/ready.html +++ b/src/features/mini/ready.html @@ -82,6 +82,7 @@ return !this._readied && (!this.host || this.host._readied); }, + // TODO(sorvell): can this be collapsed with _distributeContent? _initializeContent: function() { // prepare root this._setupRoot(); From 440120e78bb5fae68ebe8cd276b0ed7cbf358547 Mon Sep 17 00:00:00 2001 From: Steve Orvell Date: Mon, 23 Feb 2015 11:03:35 -0800 Subject: [PATCH 13/14] Allow elements created via template repeat to have the proper host. --- src/lib/template/templatizer.html | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/lib/template/templatizer.html b/src/lib/template/templatizer.html index d7dd5ec8e8..cafa7dca8b 100644 --- a/src/lib/template/templatizer.html +++ b/src/lib/template/templatizer.html @@ -49,8 +49,8 @@ archetype._notifyPath = this._notifyPathImpl; // boilerplate code var _constructor = this._constructorImpl; - var ctor = function TemplateInstance(model) { - _constructor.call(this, model); + var ctor = function TemplateInstance(model, host) { + _constructor.call(this, model, host); }; // standard references ctor.prototype = archetype; @@ -88,9 +88,16 @@ } }, - _constructorImpl: function(model) { + _constructorImpl: function(model, host) { this._setupConfigure(model); + this.host = host; + if (this.host) { + this.host._beginHost(); + } this.root = this.instanceTemplate(this._template); + if (this.host) { + this.host._popHost(); + } this._marshalTemplateContent(); this._marshalAnnotatedNodes(); this._marshalInstanceEffects(); @@ -99,7 +106,7 @@ }, stamp: function(model) { - return new this.ctor(model); + return new this.ctor(model, this.host); } }; From d951894682f5774706eabdef3ccb5b95178d58b1 Mon Sep 17 00:00:00 2001 From: Kevin Schaaf Date: Mon, 23 Feb 2015 14:50:36 -0800 Subject: [PATCH 14/14] Allow items to be Collection. Fix typo bug. --- src/lib/template/x-repeat.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/template/x-repeat.html b/src/lib/template/x-repeat.html index dabfffa762..66bcd20e7f 100644 --- a/src/lib/template/x-repeat.html +++ b/src/lib/template/x-repeat.html @@ -114,7 +114,7 @@ }, _observeCollection: function(items) { - this.collection = Collection.get(items); + this.collection = Array.isArray(items) ? Collection.get(items) : items; this.collection.observe(this.boundCollectionObserver); }, @@ -286,7 +286,7 @@ addedKeys = addedKeys.filter(function(a) { return this._filterFn(c.getItem(a)); }, this); - filterDelta += (s.added.length - added.length); + filterDelta += (s.added.length - addedKeys.length); } var idx = s.index - filterDelta; // Apply splices to keys