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 @@ + 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}} + + + + 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); +});