diff --git a/HISTORY.md b/HISTORY.md
index 3f4cf5b..ded00e9 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,5 +1,9 @@
## vNEXT
+* Fix wrapper with no args' renderFunc using `with` data as args
+* famousEach, in a totally new and extensible way
+* Some internal changes in how nodes are dismounted
+
## v1.0.0
PLEASE NOTE, this is not a "1.0.0 production quality release". Meteor packages
diff --git a/README.md b/README.md
index 5f85b11..506328f 100644
--- a/README.md
+++ b/README.md
@@ -325,3 +325,7 @@ Besides the underlying Famous API and how we deal with it:
subtemplating. In the new version we just use `_onRender` and `_eventMap`
attributes, provided from the enclosing Templates single helper. Feedback
welcome. You can still use regular inclusion `{{>template}}` like usual.
+
+* In v0 a MeteorFamousView was (usually) a kind of renderNode too. In v1
+ we maintain a separatation, we maintain our own tree (like we had to
+ in v0 too) but add famous nodes directly to the scene graph.
\ No newline at end of file
diff --git a/lib/famous-views.js b/lib/famous-views.js
index 8b16397..f0277ae 100644
--- a/lib/famous-views.js
+++ b/lib/famous-views.js
@@ -24,3 +24,10 @@ FView.ready(function() {
// XXX
FamousEngine.init();
});
+
+unusedDiv = null;
+FView.ready(function() {
+ // this will always be empty, but Blaze needs a real div to render to.
+ // let's make sure we only ping the dom once and don't garbage collect
+ unusedDiv = FView.unusedDiv = document.createElement('div');
+});
diff --git a/lib/famousEach.js b/lib/famousEach.js
new file mode 100644
index 0000000..121b67e
--- /dev/null
+++ b/lib/famousEach.js
@@ -0,0 +1,123 @@
+var OP = {
+ NULL: 0,
+ ADDED_AT: 1,
+ REMOVED_AT: 2
+};
+
+var currentOp = { type: OP.NULL };
+
+function famousEachRender(eachView, template, argFunc) {
+ var fview = eachView._fview;
+ var children = fview.eachChildren = []; // each contentBlock instance
+
+ // For Blaze.currentView (see blaze/builtins.js#each)
+ eachView.argVar = new Blaze.ReactiveVar();
+ eachView.autorun(function () {
+ eachView.argVar.set(argFunc());
+ }, eachView.parentView);
+
+ eachView.stopHandle = ObserveSequence.observe(function () {
+ return eachView.argVar.get();
+ }, {
+ addedAt: function (id, item, index) {
+ // Deferred since could trigger any time and Blaze stuff takes time
+ FView.defer(function() {
+ var newItemView = Blaze.With(item, function() {
+ return template.constructView();
+ });
+
+ /*
+ * During Blaze.render(), newNode.addToParent() calls
+ * FamousEach.addChild() which calls GrandParent.addedAt()
+ * using the stored data below.
+ */
+ currentOp.type = OP.ADDED_AT;
+ currentOp.index = index;
+ currentOp.id = id;
+ currentOp.item = item;
+ children.splice(index, 0, newItemView);
+ Blaze.render(newItemView, unusedDiv, eachView);
+ currentOp.type = OP.NULL;
+ });
+ },
+ removedAt: function (id, item, index) {
+ // Deferred since could trigger any time and Blaze stuff takes time
+ FView.defer(function() {
+ // same deal as addedAt
+ currentOp.type = OP.REMOVED_AT;
+ currentOp.index = index;
+ currentOp.id = id;
+ currentOp.item = item;
+ Blaze.remove(children[index]);
+ children.splice(index, 1);
+ currentOp.type = OP.NULL;
+ });
+ },
+ changedAt: function (id, newItem, oldItem, index) {
+ // Deferred only to maintain order with addedAt/removedAt
+ FView.defer(function() {
+ children[index].dataVar.set(newItem);
+ fview.parent.fvClass.changedAt.call(fview.parent,
+ id, newItem, oldItem, index);
+ });
+ },
+ movedTo: function (id, doc, fromIndex, toIndex) {
+ // Deferred only to maintain order with addedAt/removedAt
+ FView.defer(function() {
+ item = children.splice(fromIndex, 1)[0];
+ children.splice(toIndex, 0, item);
+ fview.parent.fvClass.movedTo.call(fview.parent,
+ id, doc, fromIndex, toIndex);
+ });
+ }
+ });
+}
+
+var famousEachClass = {
+
+ addChild: function(child) {
+ if (currentOp.type !== OP.ADDED_AT)
+ throw new Error("we broke :(");
+ // this = fview
+ // need to diff famousEach position in child list
+ this.parent.fvClass.addedAt.call(this.parent,
+ currentOp.id, child, currentOp.index);
+ },
+
+ removeChild: function(child) {
+ if (currentOp.type !== OP.REMOVED_AT)
+ throw new Error("we broke :(");
+ // this = fview
+ // need to diff famousEach position in child list
+ this.parent.fvClass.removedAt.call(this.parent,
+ currentOp.id, child, currentOp.index);
+ }
+
+};
+
+function famousEachCreated() {
+ var blazeView = this.view;
+ var fview = new MeteorFamousView(fviewParentFromBlazeView(blazeView),
+ null /* id */, 'FamousEach', parentDetails(blazeView));
+ blazeView._fview = fview;
+ fview.fvClass = famousEachClass;
+
+ // Contents of {{#famousEach}}block{{/famousEach}}
+ if (blazeView.templateContentBlock)
+ famousEachRender(blazeView, blazeView.templateContentBlock, function() {
+ return Blaze.getData(blazeView);
+ });
+}
+
+function famousEachDestroyed() {
+ this.view._fview.destroy(true);
+}
+
+var famousEachView = new Blaze.Template(
+ 'FamousEach',
+ function() { return null; }
+);
+famousEachView.created = famousEachCreated;
+famousEachView.destroyed = famousEachDestroyed;
+
+Blaze.registerHelper('famousEach', famousEachView);
diff --git a/lib/meteorFamousView.js b/lib/meteorFamousView.js
index f8475d8..e770e53 100644
--- a/lib/meteorFamousView.js
+++ b/lib/meteorFamousView.js
@@ -9,7 +9,8 @@ var fviewCount = FView._fviewCount = 0;
* @property {MeteorFamousView} parent
* @property {array} children
*/
-MeteorFamousView = FView._MeteorFamousView = function(parent, id, type, source) {
+MeteorFamousView = FView._MeteorFamousView =
+ function MeteorFamousView(parent, id, type, source) {
this.id = id || fviewCount;
fviews[this.id] = this;
fviewCount++;
@@ -37,6 +38,12 @@ MeteorFamousView.prototype.destroy = function() {
log.debug("Destroying " + this.type + " (#" + this.id + ") from " + this._source);
// TODO children, etc.
+
+ // remove from parent
+ if (this.parent)
+ this.parent.children.splice(this.parent.children.indexOf(this), 1);
+
+ this.fvClass.dismount.call(this);
};
FView.byId = function(id) {
@@ -44,7 +51,7 @@ FView.byId = function(id) {
};
fviewParentFromBlazeView = FView._fviewParentFromBlazeView = function (blazeView) {
- while ((blazeView = blazeView.parentView) && blazeView.name.substr(0,7) !== 'Famous.');
+ while ((blazeView = blazeView.parentView) && blazeView.name.substr(0,6) !== 'Famous');
return blazeView && blazeView._fview;
};
diff --git a/lib/wrappers/Components.js b/lib/wrappers/Components.js
index a1f8a23..5c3f994 100644
--- a/lib/wrappers/Components.js
+++ b/lib/wrappers/Components.js
@@ -24,11 +24,14 @@ var ComponentClass = {
// Note, we can't re-use Node's renderFunc, since this is a COMPONENT *on* the node
// However, we should modularize common code.
renderFunc: function() {
+ var blazeView = this;
+ if (blazeView.parentView && !blazeView.parentView.__isTemplateWith)
+ return null; // No arguments, data is from a `with`
+
var data = Blaze.getData(this);
if (!data)
- return null;
+ return null; // No data context, i.e. no arguments
- var blazeView = this;
var tpl = blazeView._templateInstance;
//var fview = blazeView._fview;
var fview = fviewParentFromBlazeView(blazeView);
diff --git a/lib/wrappers/Nodes.js b/lib/wrappers/Nodes.js
index a4c5147..e4af941 100644
--- a/lib/wrappers/Nodes.js
+++ b/lib/wrappers/Nodes.js
@@ -2,21 +2,74 @@ var NodeClass = FView._NodeClass = {
name: 'UnnamedNode',
famousClass: null,
+ /*
+ * Create a new Famous class instance, e.g. var node = new Node();
+ */
newInstance: function(fview) {
- var fview = this;
- return new fview.fvClass.famousClass();
+ // var fview = this;
+ return new this.fvClass.famousClass();
},
+ /*
+ * Add a Famous child to the current fview
+ */
addChild: function(child) {
- var fview = this;
- fview.node.addChild(child);
+ // var fview = this;
+ this.node.addChild(child);
},
+ /*
+ * How we are added to our parent
+ */
addToParent: function() {
- var fview = this;
- fview.parent.fvClass.addChild.call(fview.parent, fview.node)
+ // var fview = this;
+ this.parent.fvClass.addChild.call(this.parent, this.node)
+ },
+
+ /*
+ * For observes, an add relative to a sequence
+ */
+ addedAt: function(id, child, index) {
+ // var fview = this;
+ this.fvClass.addChild.call(this, child);
+ },
+
+ /*
+ * How to remove a famous child
+ */
+ removeChild: function(child) {
+ // var fview = this;
+ this.node.removeChild(child);
+ },
+
+ /*
+ * What to do with our node when an fview instance is destroyed (usually
+ * via Template destroy). We call parent.removeChild() rather than
+ * the node's dismount() directly, to let the parent decide what to do.
+ *
+ */
+ dismount: function() {
+ // var fview = this;
+ this.parent.fvClass.removeChild.call(this.parent, this.node);
+ },
+
+ /*
+ * Called when removed from an observe sequence
+ */
+ removedAt: function(id, child, index) {
+ // var fview = this;
+ this.fvClass.removeChild.call(this, child);
},
+ /*
+ * Regular nodes don't care about this, views/layouts will
+ */
+ changedAt: function() {},
+ movedTo: function() {},
+
+ /*
+ * How to handle reactive attribute changes
+ */
/*
attrUpdate: function(key, value, oldValue, data, firstTime) {
var fview = this;
@@ -27,6 +80,9 @@ var NodeClass = FView._NodeClass = {
},
*/
+ /*
+ * How to construct our template
+ */
makeTemplate: function(fvClass) {
var template = new Blaze.Template('Famous.' + fvClass.name,
fvClass.renderFunc);
@@ -36,12 +92,18 @@ var NodeClass = FView._NodeClass = {
return template;
},
+ /*
+ * The Blaze "renderFunc" that reruns reactively, used to call attrUpdate
+ */
renderFunc: function() {
+ var blazeView = this;
+ if (blazeView.parentView && !blazeView.parentView.__isTemplateWith)
+ return null; // No arguments, data is from a `with`
+
var data = Blaze.getData(this);
if (!data)
- return null;
+ return null; // No data context, i.e. no arguments
- var blazeView = this;
var tpl = blazeView._templateInstance;
var fview = blazeView._fview;
var fvClass = blazeView.template._fviewClass;
@@ -69,6 +131,9 @@ var NodeClass = FView._NodeClass = {
return null;
},
+ /*
+ * What to do on a Blaze template created lifeycle callback
+ */
templateCreated: function() {
var blazeView = this.view;
var data = this.data || {};
@@ -96,7 +161,7 @@ var NodeClass = FView._NodeClass = {
viewToRender = fview.blazeView.templateContentBlock;
// This materializes the Node's spacebars contents; div is never used
- Blaze.render(viewToRender, document.createElement('div'), fview.blazeView);
+ Blaze.render(viewToRender, unusedDiv, fview.blazeView);
if (data._onRender) {
var onRender = getHelperFunc(blazeView, data._onRender);
@@ -108,10 +173,11 @@ var NodeClass = FView._NodeClass = {
}
},
+ /*
+ * What to do on a Blaze template destroyed lifecycle callback
+ */
templateDestroyed: function() {
- var fview = FView.from(this);
- fview.destroy();
- fview.node.dismount();
+ this.view._fview.destroy(true);
}
};
@@ -138,6 +204,9 @@ function argsFromVecTransitionCB(vec, transition, callback) {
return args;
}
+/*
+ * Default attrUpdate for a typical node
+ */
NodeClass.attrUpdate = function(key, value, oldValue, data, firstTime) {
var fview = this;
diff --git a/lib/wrappers/Nodes/Scene.js b/lib/wrappers/Nodes/Scene.js
index bed3139..96c1b25 100644
--- a/lib/wrappers/Nodes/Scene.js
+++ b/lib/wrappers/Nodes/Scene.js
@@ -18,6 +18,10 @@ FView.wrap('Scene', null, {
return template;
},
+ dismount: function() {
+ this.node.dismount();
+ },
+
renderFunc: function() {
// There are no reactive attributes on a Scene
return null;
@@ -77,7 +81,7 @@ FView.wrap('Scene', null, {
viewToRender = fview.blazeView.templateContentBlock;
// This materializes the Node's spacebars contents; div is never used
- Blaze.render(viewToRender, document.createElement('div'), fview.blazeView);
+ Blaze.render(viewToRender, unusedDiv, fview.blazeView);
if (data._onRender) {
var onRender = getHelperFunc(blazeView, data._onRender);
diff --git a/package.js b/package.js
index 51fbe38..52d8811 100644
--- a/package.js
+++ b/package.js
@@ -9,7 +9,7 @@ var client = 'client';
function common(api) {
// Meteor core packages
- api.use(['blaze', 'htmljs', 'ejson', 'tracker'], client);
+ api.use(['blaze', 'htmljs', 'ejson', 'tracker', 'observe-sequence'], client);
// 3rd-party included in core
api.use(['underscore'], client);
@@ -28,6 +28,7 @@ function common(api) {
'lib/utilities.js',
'lib/meteorFamousView.js',
'lib/defer.js',
+ 'lib/famousEach.js',
'lib/wrappers/wrap.js',
'lib/wrappers/Nodes.js',
@@ -57,7 +58,7 @@ Package.onUse(function(api) {
Package.onTest(function(api) {
api.use('tinytest');
- api.use(['templating', 'random', 'reactive-var'], client);
+ api.use(['templating', 'random', 'reactive-var', 'mongo'], client);
//api.use('gadicohen:famous-views');
common(api);
@@ -73,6 +74,8 @@ Package.onTest(function(api) {
'tests/wrappers/Node.html',
'tests/wrappers/Node.js',
'tests/wrappers/DOMElement.html',
- 'tests/wrappers/DOMElement.js'
+ 'tests/wrappers/DOMElement.js',
+ 'tests/famousEach.html',
+ 'tests/famousEach.js',
], 'client');
});
diff --git a/tests/famousEach.html b/tests/famousEach.html
new file mode 100644
index 0000000..a60c729
--- /dev/null
+++ b/tests/famousEach.html
@@ -0,0 +1,7 @@
+
+ {{#famousEachContainer}}
+ {{#famousEach items}}
+ {{#Node}}{{/Node}}
+ {{/famousEach}}
+ {{/famousEachContainer}}
+
diff --git a/tests/famousEach.js b/tests/famousEach.js
new file mode 100644
index 0000000..7554d4b
--- /dev/null
+++ b/tests/famousEach.js
@@ -0,0 +1,86 @@
+var lastCmd = null;
+var callback = null;
+var runCallback = function() {
+ callback.apply(this, arguments);
+}
+
+var overrides = { newInstance: noop, addToParent: noop };
+_.each(['addedAt', 'removedAt', 'changedAt', 'movedTo'], function(what) {
+ overrides[what] = function() {
+ this.lastCmd = what;
+ callback.apply(this, arguments);
+ }
+});
+FView.wrap('famousEachContainer', null, overrides);
+
+var Items = new Mongo.Collection(null);
+Template.famousEachTest.helpers({
+ items: function() { return Items.find({}, { sort: { name: 1 }}); }
+});
+
+Tinytest.add('famous-views - famousEach - setup', function(test) {
+ // maintain order
+ Blaze.render(Template.famousEachTest, commonDiv);
+});
+
+Tinytest.addAsync('famous-views - famousEach - addedAt append', function(test, complete) {
+ var newItemId = Items.insert({ name:'a' });
+ callback = function(id, child, index) {
+ test.equal(this.lastCmd, 'addedAt');
+ test.equal(id, newItemId);
+ test.equal(index, 0);
+
+ // for next test ("addedAt insert")
+ Items.insert({ name:'c' });
+ callback = function() {
+ complete();
+ }
+ };
+});
+
+Tinytest.addAsync('famous-views - famousEach - addedAt insert', function(test, complete) {
+ var newItemId = Items.insert({name:'b'});
+ callback = function(id, child, index) {
+ test.equal(this.lastCmd, 'addedAt');
+ test.equal(id, newItemId);
+ test.equal(index, 1);
+ complete();
+ };
+});
+
+Tinytest.addAsync('famous-views - famousEach - changedAt', function(test, complete) {
+ var itemId = Items.findOne({name:'b'})._id;
+ Items.update(itemId, { $set: { newInfo: 1 }} );
+ callback = function(id, newItem, oldItem, index, child) {
+ test.equal(this.lastCmd, 'changedAt');
+ test.equal(id, itemId);
+ test.equal(newItem.newInfo, 1);
+ complete();
+ };
+});
+
+Tinytest.addAsync('famous-views - famousEach - movedTo', function(test, complete) {
+ var itemId = Items.findOne({name:'b'})._id;
+ Items.update(itemId, { $set: { name: 'd' }} );
+ // changedAt will trigger first
+ callback = function() {
+ callback = function(id, doc, fromIndex, toIndex) {
+ test.equal(this.lastCmd, 'movedTo');
+ test.equal(id, itemId);
+ test.equal(fromIndex, 1);
+ test.equal(toIndex, 2);
+ complete();
+ };
+ };
+});
+
+Tinytest.addAsync('famous-views - famousEach - removedAt', function(test, complete) {
+ var itemId = Items.findOne({name:'d'})._id;
+ Items.remove(itemId);
+ callback = function(id, child, index) {
+ test.equal(this.lastCmd, 'removedAt');
+ test.equal(id, itemId);
+ test.equal(index, 2);
+ complete();
+ };
+});
diff --git a/tests/lib/prepare.js b/tests/lib/prepare.js
index 2568bc1..c3534a0 100644
--- a/tests/lib/prepare.js
+++ b/tests/lib/prepare.js
@@ -4,3 +4,5 @@ Meteor.startup(function() {
commonDiv.style.display = 'none';
document.body.appendChild(commonDiv);
});
+
+noop = function() {};
\ No newline at end of file
diff --git a/tests/wrappers/Node.html b/tests/wrappers/Node.html
index 1d8e691..321e8e8 100644
--- a/tests/wrappers/Node.html
+++ b/tests/wrappers/Node.html
@@ -11,3 +11,19 @@
{{/Node}}
{{/Scene}}
+
+
+ {{#Scene id="node3_scene"}}
+ {{#Node id="node3_node"}}
+ {{/Node}}
+ {{/Scene}}
+
+
+
+ {{#Scene id="node4_scene"}}
+ {{#if x}}
+ {{#Node id="node4_node"}}
+ {{/Node}}
+ {{/if}}
+ {{/Scene}}
+
diff --git a/tests/wrappers/Node.js b/tests/wrappers/Node.js
index 3113f19..33e8df5 100644
--- a/tests/wrappers/Node.js
+++ b/tests/wrappers/Node.js
@@ -67,4 +67,38 @@ Tinytest.addAsync('famous-views - Wrappers - Node - _onRender', function(test, c
});
Blaze.render(Template.node2, commonDiv);
-});
\ No newline at end of file
+});
+
+// also tests: addToParent, addChild
+Tinytest.addAsync('famous-views - Wrappers - Node - template create', function(test, complete) {
+ Template.node3.rendered = function() {
+ var scene = FView.byId('node3_scene');
+ var node = FView.byId('node3_node');
+
+ test.equal(scene.children.indexOf(node), 0);
+ test.equal(scene.node._children.indexOf(node.node), 0);
+ complete();
+ };
+ Blaze.render(Template.node3, commonDiv);
+});
+
+// also tests: dismount
+Tinytest.addAsync('famous-views - Wrappers - Node - template destroy', function(test, complete) {
+ var x = new ReactiveVar(true);
+ Template.node4.helpers({
+ x: function() { return x.get(); }
+ });
+
+ Template.node4.rendered = function() {
+ var scene = FView.byId('node4_scene');
+ var node = FView.byId('node4_node');
+
+ Tracker.afterFlush(function() {
+ test.equal(scene.children.indexOf(node), -1);
+ test.equal(scene.node._children.indexOf(node.node), -1);
+ complete();
+ });
+ x.set(false);
+ };
+ Blaze.render(Template.node4, commonDiv);
+});