Meteor : subscribe to collection subset & collection total count - meteor

Here is my issue :
I'm subscribing to a subset of a Collection for "infinite pagination" in iron-router (like the Discover Meteor example) :
ApplicationsListController = RouteController.extend({
template: 'applicationsList',
increment: 10,
limit: function(){
return parseInt(this.params.applicationsLimit) || this.increment;
},
findOptions: function(){
return {sort: {name: 1}, limit: this.limit()};
},
subscriptions: function(){
this.applicationsSub = Meteor.subscribe('applications', this.findOptions()) ;
},
applications: function(){
return Applications.find({}, this.findOptions());
},
data: function(){
var hasMore = this.applications().fetch().length === this.limit();
var nextPath = this.route.path({applicationsLimit: this.limit() + this.increment});
return {
applications: this.applications(),
ready: this.applicationsSub.ready,
nextPath: hasMore ? nextPath : null
};
}
});
//(...)
this.route('applicationsList', {
path: '/applications/:applicationsLimit?',
controller: ApplicationsListController
});
I'm publishing it well, no problem there. But, on the same page, I also need the total count of the entire collection (not only the subset). I publish it like that :
Meteor.publish('applications', function(options){
return Applications.find({}, options);
});
Meteor.publish('applicationsCount', function(){
return Applications.find().count();
});
But there is something I guess I did not understand. I need to use the total count in my template, but I just can't see how to subscribe to "just a number", without creating a new collection (which I don't want to do).
I've seen the 'counts-for-room' example on Meteor Doc, but it seems that it is far from what I need (I don't have room with message in it, I just need to count my applications without getting them all on client).
Thanks a lot, I hope I was clean enough.
Have a great day.

If you want the Count of the collection.
Try with the publish-counts package.
$ meteor add tmeasday:publish-counts
So this how your code should looks alike.
//server.js
Meteor.publish('applicationsCount', function() {
Counts.publish(this, 'applicationsCount', Applications.find());
});
on the lib folder.
if(Meteor.isClient){
Meteor.subscribe('applicationsCount')
Counts.get('applicationsCount');
}
Now look that Counts.get works like a helper, so you can use it on the template like this.
<span> There is a Total of {{getPublishedCount 'applicationsCount'}} Applications</span>

Thanks to Ethann I made it work.
First I installed the publish-counts package
$ meteor add tmeasday:publish-counts
As Ethann said, I published the count on my server\publications.js
Meteor.publish('applicationsCount', function() {
Counts.publish(this, 'applicationsCount', Applications.find());
});
And I updated my iron-router Controller like that :
ApplicationsListController = RouteController.extend({
template: 'applicationsList',
increment: 10,
(...)
subscriptions: function(){
this.applicationsSub = Meteor.subscribe('applications', this.findOptions()) ;
this.applicationsCount = Meteor.subscribe('applicationsCount');
},
(...)
data: function(){
var hasMore = this.applications().fetch().length === this.limit();
var nextPath = this.route.path({applicationsLimit: this.limit() + this.increment});
Counts.get('applicationsCount');
return {
applications: this.applications(),
ready: this.applicationsSub.ready,
nextPath: hasMore ? nextPath : null
};
}
});
To finally call the count in my template:
<span> There is a Total of {{getPublishedCount 'applicationsCount'}} Applications</span>
Thanks a lot. Hope it will help some people around here.

Related

Client data not being updated in Meteor application

I have a Meteor application with a publish of:
Meteor.publish('my_items', function() {
var selector = {owner_id: this.userId};
var items = ItemOwnership.find(selector, {fields: {item_id: 1}}).fetch();
var itemIds = _.pluck(items, 'item_id');
return Items.find({
_id: {$in: itemIds},
item_archived_ts: { $exists: false }
});
});
and a subscription of this:
Meteor.subscribe('my_items');
The application allows for the user to add items to the 'Items' collection and this is done by calling a server method. The Items collection on the server is updated with the new record, but the client-side equivalent collection is not showing the new record. Is there anything obviously wrong with what I am doing, or some way to debug this?
p.s. there are no client/server-side errors occurring?
I found a way to accomplish this using the reywood:publish-composite Meteor package. Here is the publish that achieves this:
Meteor.publishComposite('my_items', {
find: function () {
var selector = {owner_id: this.userId};
return ItemOwnership.find(selector, {fields: {item_id: 1}});
},
children: [
{
find: function(IOrecord){
return Items.find({
_id: IOrecord.item_id,
item_archived_ts: { $exists: false }
});
}
}
]
});

Meteor routing: how to extend RouterController?

I'm reading the book 'Discover meteor' and have a question about pagination(pagination chapter).
I have a code in my router.js:
//router.js
...
PostsListController = RouteController.extend({
template: 'postsList',
increment: 4,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.postsLimit()};
},
subscriptions: function() {
this.postsSub = Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
return Posts.find({}, this.findOptions());
},
data: function() {
var hasMore = this.posts().count() === this.postsLimit();
var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
return {
posts: this.posts(),
ready: this.postsSub.ready,
nextPath: hasMore ? nextPath : null
};
}
});
...
Router.route('/:postsLimit?', {
name: 'postsList'
});
And this working fine. My problem description:
I have another route ('/news') and whant to make pagination for this route too. How i should properly extend PostsListController to make it?
Every my post have a tag option, in this case it is a 'news', so i want to see only posts with 'news' tag.
I'm tryed to just copy-paste this controller(PostsListController) and:
renamed it;
set another template;
changed:
posts: function() {
return Posts.find({}, this.findOptions());
}
to:
posts: function() {
return Posts.find({postType: 'news'}, this.findOptions());
}
It not working, on my /page news i can see only all my news articles and spinner. I'm added:
Router.route('/news/:postsLimit?', {
name: 'newsTemplate',
controller: NewsTemplateController
});
But when i'm goind to /news/1 i'm see all my posts(not only one) and button 'show more'.
I think this copy-paste approach so bad but i have not ideas how to make it working proper way.
Your first issue where /news shows all the posts is because your first Route specification is too generic.
Router.route('/:postsLimit?', {
name: 'postsList'
});
This route specification will send all requests with one parameter to the PostsListController
ie. all of these paths will route to the PostsListsController:
/asdf
/test
/news
To fix this, you might want to make the first route more specific:
Router.route('/posts/:postsLimit?', {
name: 'postsList'
});
I am not sure why you are getting more than one item when going to /news/1.
Can you post your code for that controller?

Why is data set with Meteor Iron Router not available in the template rendered callback?

This is a bit puzzling to me. I set data in the router (which I'm using very simply intentionally at this stage of my project), as follows :
Router.route('/groups/:_id',function() {
this.render('groupPage', {
data : function() {
return Groups.findOne({_id : this.params._id});
}
}, { sort : {time: -1} } );
});
The data you would expect, is now available in the template helpers, but if I have a look at 'this' in the rendered function its null
Template.groupPage.rendered = function() {
console.log(this);
};
I'd love to understand why (presuming its an expected result), or If its something I'm doing / not doing that causes this?
From my experience, this isn't uncommon. Below is how I handle it in my routes.
From what I understand, the template gets rendered client-side while the client is subscribing, so the null is actually what data is available.
Once the client recieves data from the subscription (server), it is added to the collection which causes the template to re-render.
Below is the pattern I use for routes. Notice the if(!this.ready()) return;
which handles the no data situation.
Router.route('landing', {
path: '/b/:b/:brandId/:template',
onAfterAction: function() {
if (this.title) document.title = this.title;
},
data: function() {
if(!this.ready()) return;
var brand = Brands.findOne(this.params.brandId);
if (!brand) return false;
this.title = brand.title;
return brand;
},
waitOn: function() {
return [
Meteor.subscribe('landingPageByBrandId', this.params.brandId),
Meteor.subscribe('myProfile'), // For verification
];
},
});
Issue
I was experiencing this myself today. I believe that there is a race condition between the Template.rendered callback and the iron router data function. I have since raised a question as an IronRouter issue on github to deal with the core issue.
In the meantime, workarounds:
Option 1: Wrap your code in a window.setTimeout()
Template.groupPage.rendered = function() {
var data_context = this.data;
window.setTimeout(function() {
console.log(data_context);
}, 100);
};
Option 2: Wrap your code in a this.autorun()
Template.groupPage.rendered = function() {
var data_context = this.data;
this.autorun(function() {
console.log(data_context);
});
};
Note: in this option, the function will run every time that the template's data context changes! The autorun will be destroyed along with the template though, unlike Tracker.autorun calls.

Meteor collection lookup on router level

I'm building an app that has versions of pages. I'd like users to be able to visit the page without having to specify the version they wish to start working with. In case no version is specified in the URL, I'd like to look up the latest version of that page and redirect to it.
The problem I'm having is that I can't do any collection lookups on the router level to figure out what version I'm looking for because the collections simply aren't there.
Here's my code so far:
Router.route('page', {
path: '/page/:slug/:version?',
onBeforeAction: function() {
var versionId = this.params.version;
if (!versionId) {
console.log('no version! oh noes!');
var p = Pages.find({slug: this.params.slug});
var newestVersion = Versions.findOne({page: p._id}, {sort: {createdAt: -1}});
versionId = newestVersion._id;
Router.redirect('page', this.params.slug, versionId);
}
this.next();
},
waitOn: function() {
return Meteor.subscribe('page', this.params.slug, this.params.state);
},
data: function() {
return {
page: Pages.findOne({slug: this.params.slug}),
version: Versions.findOne(this.params.version)
}
}
});
Any help or insights are very much appreciated!
Here's the solution I found. The problem is that the onBeforeAction didn't actually wait for the waitOn unless you tell it to.
onBeforeAction: function() {
if (this.ready()) {
if (!this.params.version) {
var p = Pages.findOne({slug: this.params.slug}),
sortedVersions = _.sortBy(Versions.find({page: p._id}).fetch(), function(s) {
return v.createdAt;
});
var newest_version_id = sortedVersions[0]._id;
this.redirect('page', {slug: this.params.slug, state: newest_version_id});
}
this.next();
}
}
I think the key is in the this.ready() check which is only true when all subscriptions in the waitOn are true. I assumed all this happened automagically by simply using the waitOn.

How do I make a collection reactive based on the contents of another?

In short, I want to do:
Meteor.publish('items', function(){
return Item.find({categoryId: Categories.find({active: true} });
});
The flag 'active' as part of 'Categories' changes regularly.
I also tried unsub/resub to the Items collection by leveraging reactivity on the Categories collections, and it works, unfortunately it re-triggers on ANY modification to the Categories collection, regardless if it affected the 'active' flag or not.
What are my options?
Nothing solved the issue of the items not being 'deleted' locally when the category is flagged as inactive on the server. Solution (ish) is to:
Client:
Categories.find({active: true}).observeChanges({
added: function(){
itemsHandle && itemsHandle.stop();
itemsHandle = Meteor.subscribe("items");
}
});
Server:
Meteor.publish('items', function(){
var category = Categories.findOne({active: true});
return category && Items.find({categoryId: Categories.findOne({active: true}._id);
});
I realize this isn't perfect (still uses client side code), but it works and its the cleanest I could think of. I hope it helps someone!
A possible solution is to create a dependency object, watch for all categories change, and trigger the dep change if the active flag was toggled. Something along these lines:
var activeCount = Categories.find({active: true}).count();
var activeDep = new Deps.Dependency();
Deps.autorun(function() {
var activeCountNow = Categories.find({active: true}).count();
if(activeCountNow !== activeCount) {
activeCount = activeCountNow;
activeDep.changed();
}
});
Meteor.publish('items', function(){
activeDep.depend();
return Item.find({categoryId: Categories.find({active: true} });
});
Note: I'm only verifying whether the number of active categories have changes so that I don't have to keep the active list in the memory. This may or may not be appropriate depending on how your app works.
Edit: Two-sided flavor mentioned in the comments:
Client:
var activeCount = Categories.find({active: true}).count();
var activeDep = new Deps.Dependency();
Deps.autorun(function() {
var activeCountNow = Categories.find({active: true}).count();
if(activeCountNow !== activeCount) {
activeCount = activeCountNow;
activeDep.changed();
}
});
Deps.autorun(function(){
activeDep.depend();
Meteor.subscribe('items', new Date().getTime());
});
Server:
Meteor.publish('items', function(timestamp) {
var t = timestamp;
return Item.find({categoryId: Categories.find({active: true} });
});
Meteor.startup(function() {
Categories.find().observe({
addedAt: function(doc) {
trigger();
},
changedAt: function(doc, oldDoc) {
if(doc.active != oldDoc.active) {
trigger();
}
},
removedAt: function(oldDoc) {
trigger();
}
});
});
Now, the trigger function should cause the publish to rerun. This time it's easy when it's on the client (change subscription param). I'm not sure how to do this on the server - perhaps run publish again.
I use the following publish to solve a similar issue. I think it is only the one line nesting of queries that limits the reactivity. Breaking one query out inside the publish function seems to avoid the issue.
//on server
Meteor.publish( "articles", function(){
var self= this;
var subscriptions = [];
var observer = Feeds.find({ subscribers: self.userId }, {_id: 1}).observeChanges({
added: function (id){
subscriptions.push(id);
},
removed: function (id){
subscriptions.splice( subscriptions.indexOf(id)) , 1);
}
});
self.onStop( function() {
observer.stop();
});
var visibleFields = {_id: 1, title: 1, source: 1, date: 1, summary: 1, link: 1};
return Articles.find({ feed_id: {$in: subscriptions} }, { sort: {date: -1}, limit: articlePubLimit, fields: visibleFields } );
});
//on client anywhere
Meteor.subscribe( "articles" );
Here is another SO example which gets the search criteria from the client through subscribe if you decide that is acceptable.
Update: Since the OP struggled to get this going I made a gist and launched a working version on meteor.com. If you just need the publish function it is as above.

Resources