I managed to achieve some promising preliminary results by approaching the problem with one publication / subscription to the collection and using $or in the find query.
The idea is to provide a wrapper around Meteor.Collection , which allows you to add “views”, which are mostly called cursors. But what actually happens is that these cursors do not start individually ... their selectors are retrieved, $ or'd together, and run as one request and one pub-sub.
This is not ideal, since the offset / limit will not work with this technique, but minimongo does not currently support it in any way.
But in the end, what it allows you to do is to declare that it looks like different subsets of the same collection, but under the hood they are one and the same subset. There's just a bit of abstraction to make them feel cleanly separated.
Example:
// Place this code in a file read by both client and server: var Users = new Collection("users"); Users.view("enabledUsers", function (collection) { return collection.find({ enabled: true }, { sort: { name: 1 } }); });
Or if you want to pass parameters:
Users.view("filteredUsers", function (collection) { return collection.find({ enabled: true, name: this.search, { sort: { name: 1 } }); }, function () { return { search: Session.get("searchterms"); }; });
The parameters are set as objects, because they are a single publication / subscription $ or'd together, I need a way to get the correct parameters, since they are mixed.
And actually use it in the template:
Template.main.enabledUsers = function () { return Users.get("enabledUsers"); }; Template.main.filteredUsers = function () { return Users.get("filteredUsers"); };
In short, I use the same code as on the server and on the client, and if the server does nothing, the client will be or vice versa.
And most importantly, only those records that interest you are sent to the client. This can be achieved without a layer of abstraction, simply by making $ or by yourself, but this $ or will turn out pretty ugly as more subsets are added. It just helps manage it with minimal code.
I quickly wrote this to check it, apologize for the length and lack of documentation:
test.js
// Shared (client and server) var Collection = function () { var SimulatedCollection = function () { var collections = {}; return function (name) { var captured = { find: [], findOne: [] }; collections[name] = { find: function () { captured.find.push(([]).slice.call(arguments)); return collections[name]; }, findOne: function () { captured.findOne.push(([]).slice.call(arguments)); return collections[name]; }, captured: function () { return captured; } }; return collections[name]; }; }(); return function (collectionName) { var collection = new Meteor.Collection(collectionName); var views = {}; Meteor.startup(function () { var viewName, view, pubName, viewNames = []; for (viewName in views) { view = views[viewName]; viewNames.push(viewName); } pubName = viewNames.join("__"); if (Meteor.publish) { Meteor.publish(pubName, function (params) { var viewName, view, selectors = [], simulated, captured; for (viewName in views) { view = views[viewName]; // Run the query callback but provide a SimulatedCollection // to capture what is attempted on the collection. Also provide // the parameters we would be passing as the context: if (_.isFunction(view.query)) { simulated = view.query.call(params, SimulatedCollection(collectionName)); } if (simulated) { captured = simulated.captured(); if (captured.find) { selectors.push(captured.find[0][0]); } } } if (selectors.length > 0) { return collection.find({ $or: selectors }); } }); } if (Meteor.subscribe) { Meteor.autosubscribe(function () { var viewName, view, params = {}; for (viewName in views) { view = views[viewName]; params = _.extend(params, view.params.call(this, viewName)); } Meteor.subscribe.call(this, pubName, params); }); } }); collection.view = function (viewName, query, params) { // Store in views object -- we will iterate over it on startup views[viewName] = { collectionName: collectionName, query: query, params: params }; return views[viewName]; }; collection.get = function (viewName, optQuery) { var query = views[viewName].query; var params = views[viewName].params.call(this, viewName); if (_.isFunction(optQuery)) { // Optional alternate query provided, use it instead return optQuery.call(params, collection); } else { if (_.isFunction(query)) { // In most cases, run default query return query.call(params, collection); } } }; return collection; }; }(); var Items = new Collection("items"); if (Meteor.isServer) { // Bootstrap data -- server only Meteor.startup(function () { if (Items.find().count() === 0) { Items.insert({title: "item #01", enabled: true, processed: true}); Items.insert({title: "item #02", enabled: false, processed: false}); Items.insert({title: "item #03", enabled: false, processed: false}); Items.insert({title: "item #04", enabled: false, processed: false}); Items.insert({title: "item #05", enabled: false, processed: true}); Items.insert({title: "item #06", enabled: true, processed: true}); Items.insert({title: "item #07", enabled: false, processed: true}); Items.insert({title: "item #08", enabled: true, processed: false}); Items.insert({title: "item #09", enabled: false, processed: true}); Items.insert({title: "item #10", enabled: true, processed: true}); Items.insert({title: "item #11", enabled: true, processed: true}); Items.insert({title: "item #12", enabled: true, processed: false}); Items.insert({title: "item #13", enabled: false, processed: true}); Items.insert({title: "item #14", enabled: true, processed: true}); Items.insert({title: "item #15", enabled: false, processed: false}); } }); } Items.view("enabledItems", function (collection) { return collection.find({ enabled: true, title: new RegExp(RegExp.escape(this.search1 || ""), "i") }, { sort: { title: 1 } }); }, function () { return { search1: Session.get("search1") }; }); Items.view("processedItems", function (collection) { return collection.find({ processed: true, title: new RegExp(RegExp.escape(this.search2 || ""), "i") }, { sort: { title: 1 } }); }, function () { return { search2: Session.get("search2") }; }); if (Meteor.isClient) { // Client-only templating code Template.main.enabledItems = function () { return Items.get("enabledItems"); }; Template.main.processedItems = function () { return Items.get("processedItems"); }; // Basic search filtering Session.get("search1", ""); Session.get("search2", ""); Template.main.search1 = function () { return Session.get("search1"); }; Template.main.search2 = function () { return Session.get("search2"); }; Template.main.events({ "keyup [name='search1']": function (event, template) { Session.set("search1", $(template.find("[name='search1']")).val()); }, "keyup [name='search2']": function (event, template) { Session.set("search2", $(template.find("[name='search2']")).val()); } }); Template.main.preserve([ "[name='search1']", "[name='search2']" ]); } // Utility, shared across client/server, used for search if (!RegExp.escape) { RegExp.escape = function (text) { return text.replace(/[-[\]{}()*+?.,\\^$|
test.html
<head> <title>Collection View Test</title> </head> <body> {{> main}} </body> <template name="main"> <h1>Collection View Test</h1> <div style="float: left; border-right: 3px double #000; margin-right: 10px; padding-right: 10px;"> <h2>Enabled Items</h2> <input type="text" name="search1" value="{{search1}}" placeholder="search this column" /> <ul> {{#each enabledItems}} <li>{{title}}</li> {{/each}} </ul> </div> <div style="float: left;"> <h2>Processed Items</h2> <input type="text" name="search2" value="{{search2}}" placeholder="search this column" /> <ul> {{#each processedItems}} <li>{{title}}</li> {{/each}} </ul> </div> </template>