diff --git a/src/TemplateBinding.js b/src/TemplateBinding.js index 8d32284..5d85427 100644 --- a/src/TemplateBinding.js +++ b/src/TemplateBinding.js @@ -1001,6 +1001,14 @@ return instanceNodes; }, + getDelegateFunction: function(delegate, name, template) { + if (!delegate || typeof delegate[name] !== 'function') + return null; + + var fn = delegate[name](template); + return typeof fn === 'function' ? fn : null; + }, + handleSplices: function(splices) { if (this.closed) return; @@ -1014,10 +1022,15 @@ } if (this.instanceModelFn_ === undefined) { - if (delegate && typeof delegate.prepareInstanceModel === 'function') - this.instanceModelFn_ = delegate.prepareInstanceModel(template); - if (typeof this.instanceModelFn_ !== 'function') - this.instanceModelFn_ = false; + this.instanceModelFn_ = this.getDelegateFunction(delegate, + 'prepareInstanceModel', + template); + } + + if (this.instancePositionChangedFn_ === undefined) { + this.instancePositionChangedFn_ = + this.getDelegateFunction(delegate, 'prepareInstancePositionChanged', + template); } var instanceCache = new Map; @@ -1061,6 +1074,55 @@ instanceCache.forEach(function(instanceNodes) { this.closeInstanceBindings(instanceNodes.bound); }, this); + + if (this.instancePositionChangedFn_) + this.reportInstancesMoved(splices); + }, + + reportInstanceMoved: function(index) { + var previousTerminator = this.getTerminatorAt(index - 1); + var terminator = this.getTerminatorAt(index); + if (previousTerminator === terminator) + return; // instance has zero nodes. + + // We must use the first node of the instance, because any subsequent + // nodes may have been generated by sub-templates. + // TODO(rafaelw): This is brittle WRT instance mutation -- e.g. if the + // first node was removed by script. + var templateInstance = previousTerminator.nextSibling.templateInstance; + this.instancePositionChangedFn_(templateInstance, index); + }, + + reportInstancesMoved: function(splices) { + var index = 0; + var offset = 0; + for (var i = 0; i < splices.length; i++) { + var splice = splices[i]; + if (offset != 0) { + while (index < splice.index) { + this.reportInstanceMoved(index); + index++; + } + } else { + index = splice.index; + } + + while (index < splice.index + splice.addedCount) { + this.reportInstanceMoved(index); + index++; + } + + offset += splice.addedCount - splice.removed.length; + } + + if (offset == 0) + return; + + var length = this.terminators.length / 2; + while (index < length) { + this.reportInstanceMoved(index); + index++; + } }, closeInstanceBindings: function(bound) { diff --git a/tests/tests.js b/tests/tests.js index 6672c42..692dae8 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -1859,7 +1859,7 @@ suite('Template Instantiation', function() { var template = div.firstChild; - var model = { + var model = { show: true, items: [1] }; @@ -1890,7 +1890,7 @@ suite('Binding Delegate API', function() { teardown(doTeardown); - test('Registration', function() { + test('prepareBinding', function() { var model = { foo: 'bar'}; var prepareBindData = { type: 'prepare', @@ -1967,7 +1967,7 @@ suite('Binding Delegate API', function() { assert.strictEqual(0, testData.length); }); - test('getInstanceModel', function() { + test('prepareInstanceModel', function() { var model = [{ foo: 1 }, { foo: 2 }, { foo: 3 }]; var div = createTestHtml( @@ -2017,7 +2017,7 @@ suite('Binding Delegate API', function() { assert.strictEqual(0, testData.length); }); - test('getInstanceModel - reorder instances', function() { + test('prepareInstanceModel - reorder instances', function() { var model = [0, 1, 2]; var div = createTestHtml( @@ -2048,6 +2048,64 @@ suite('Binding Delegate API', function() { assert.strictEqual(3, callCount); }); + test('prepareInstancePositionChanged', function() { + var model = ['a', 'b', 'c']; + + var div = createTestHtml( + ''); + var template = div.firstChild; + + var testData = [ + { + template: template, + }, + { + model: model[0], + index: 0 + }, + { + model: model[1], + index: 1 + }, + { + model: model[2], + index: 2 + }, + // After splice + { + model: model[2], + index: 1 + } + ]; + + var delegate = { + prepareInstancePositionChanged: function(template) { + var data = testData.shift(); + assert.strictEqual(data.template, template); + + return function(templateInstance, index) { + data = testData.shift(); + assert.strictEqual(data.model, templateInstance.model); + assert.strictEqual(data.index, index); + } + } + }; + + recursivelySetTemplateModel(div, model, delegate); + Platform.performMicrotaskCheckpoint(); + assert.strictEqual(4, div.childNodes.length); + assert.strictEqual('TEMPLATE', div.childNodes[0].tagName); + assert.strictEqual('a', div.childNodes[1].textContent); + assert.strictEqual('b', div.childNodes[2].textContent); + assert.strictEqual('c', div.childNodes[3].textContent); + + model.splice(1, 1); + Platform.performMicrotaskCheckpoint(); + + assert.strictEqual(0, testData.length); + }); + test('Basic', function() { var model = { foo: 2, bar: 4 };