diff --git a/packages/templating/deftemplate.js b/packages/templating/deftemplate.js
index 746791b439..2c3ec1b84b 100644
--- a/packages/templating/deftemplate.js
+++ b/packages/templating/deftemplate.js
@@ -9,7 +9,9 @@
var orig = Handlebars._default_helpers.each;
Handlebars._default_helpers.each = function (arg, options) {
- if (!(arg instanceof LocalCollection.Cursor))
+ // if arg isn't an observable (like LocalCollection.Cursor),
+ // don't use this reactive implementation of #each.
+ if (!(arg && 'observe' in arg))
return orig.call(this, arg, options);
return Spark.list(
diff --git a/packages/templating/templating_tests.html b/packages/templating/templating_tests.html
index f6e9d05d8d..587eb7b1e1 100644
--- a/packages/templating/templating_tests.html
+++ b/packages/templating/templating_tests.html
@@ -266,3 +266,15 @@
foobarbaz
+
+
+ {{#each entries}}
+ {{x}}
+ {{/each}}
+
+
+
+ {{#each entries}}
+ {{x}}
+ {{/each}}
+
diff --git a/packages/templating/templating_tests.js b/packages/templating/templating_tests.js
index 11c5cd9718..eae1493e17 100644
--- a/packages/templating/templating_tests.js
+++ b/packages/templating/templating_tests.js
@@ -734,3 +734,90 @@ Tinytest.add("templating - events", function (test) {
Meteor.flush();
});
+
+Tinytest.add("templating - #each render callback", function (test) {
+ // test that any list modification triggers a render callback on the
+ // enclosing template
+
+ var entries = new LocalCollection();
+ entries.insert({x:'a'});
+ entries.insert({x:'b'});
+ entries.insert({x:'c'});
+
+ var buf = [];
+
+ var tmpl = Template.test_template_eachrender_a;
+ tmpl.helpers({entries: function() {
+ return entries.find({}, {sort: ['x']}); }});
+ tmpl.render = function () {
+ buf.push(canonicalizeHtml(
+ DomUtils.rangeToHtml(this.firstNode, this.lastNode)).replace(/\s/g, ''));
+ };
+ var div = OnscreenDiv(Meteor.render(tmpl));
+ Meteor.flush();
+ test.equal(buf, ['
a
b
c
']);
+ buf.length = 0;
+
+ // added
+ entries.insert({x:'d'});
+ test.equal(buf, []);
+ Meteor.flush();
+ test.equal(buf, ['a
b
c
d
']);
+ buf.length = 0;
+
+ // removed
+ entries.remove({x:'a'});
+ test.equal(buf, []);
+ Meteor.flush();
+ test.expect_fail();
+ test.equal(buf, ['b
c
d
']);
+ buf.length = 0;
+
+ // moved/changed
+ entries.update({x:'b'}, {$set: {x: 'z'}});
+ test.equal(buf, []);
+ Meteor.flush();
+ test.equal(buf, ['c
d
z
']);
+ buf.length = 0;
+
+ // test pure "moved"
+
+ tmpl = Template.test_template_eachrender_b;
+ var cbks = [];
+ var xs = ['a','b','c'];
+ tmpl.helpers({entries: function() {
+ return { observe: function (callbacks) {
+ cbks.push(callbacks);
+ _.each(xs, function(x, i) {
+ callbacks.added({x:x}, i);
+ });
+ return {
+ stop: function () {
+ cbks = _.without(cbks, callbacks);
+ }
+ };
+ }};
+ }});
+ tmpl.render = function () {
+ buf.push(canonicalizeHtml(
+ DomUtils.rangeToHtml(this.firstNode, this.lastNode)).replace(/\s/g, ''));
+ };
+ buf = [];
+ var div = OnscreenDiv(Meteor.render(tmpl));
+ test.equal(buf, []);
+ Meteor.flush();
+ test.equal(buf, ['a
b
c
']);
+ buf.length = 0;
+
+ _.each(cbks, function (callbacks) {
+ callbacks.moved({x:'a'}, 0, 2);
+ });
+ test.equal(buf, []);
+ Meteor.flush();
+ test.equal(div.html().replace(/\s/g, ''),
+ 'b
c
a
');
+ test.expect_fail();
+ test.equal(buf, ['b
c
a
']);
+ buf.length = 0;
+
+});