Meteor "counts-by-room" example causing template not to load - javascript

I used that "counts-by-room" example from Meteor's docs to count the number of "reviews" for an "entry" on my project. Now, when I click through to an entry, the template isn't loading. It requires a refresh. Did I mess the example up somewhere? Here's my code. I don't really understand the "initializing" part of this, and don't know what to play with to get it to work correctly.
Server:
Meteor.publish("counts-by-entry", function (entryId) {
var self = this;
check(entryId, String);
var count = 0;
var initializing = true;
var handle = Reviews.find({entry: entryId}).observeChanges({
added: function (id) {
count++;
if (!initializing)
self.changed("counts", entryId, {count: count});
},
removed: function (id) {
count--;
self.changed("counts", entryId, {count: count});
}
});
initializing = false;
self.added("counts", entryId, {count: count});
self.ready();
self.onStop(function () {
handle.stop();
});
});
Client (iron-router):
this.route('entry', {
path: '/entry/:_id',
layoutTemplate: 'layout',
yieldTemplates: {
'navigation': {to: 'navigation'},
'pt_entry': {to: 'content'}
},
waitOn: function () {
Meteor.subscribe('singleEntry', this.params._id),
Meteor.subscribe('entryReviews', this.params._id),
Meteor.subscribe('counts-by-entry', this.params._id);
},
data: {
singleEntry: function () {return Entries.findOne()},
reviews: function () {return Reviews.find({entry: Session.get('entryId')}, {sort: {date: -1}})},
count: function () {return Counts.findOne(Session.get("entryId")).count + " reviews."}
},
before: function() {
Session.set("activeNav", Router.current().route.name),
Session.set("entryId", this.params._id);
},
notFoundTemplate: 'notFoundTemplate'
});
and also:
Counts = new Meteor.Collection("counts");

I just tried recreating the minimum amount required of your example to get it to work and I don't have the same issue as you.
Do you have the javascript console open in your browser? I would look for an error there, usually this kind of stuff happens when a helper is called upon when the collection data isn't available to the client. This is what iron-router's waitOn fixes which you have made use of.
In my reproduction I only have the one subscription (counts-by-entry) so maybe there is a issue with the other ones.
As for what the initializing part does:
The publish block is a piece of code that will be run for each client subscription. It does 2 things, it provides the client with the initial payload of data which in a traditional publication would be all the documents from a collection query then it reacts to changes that affects the result of the query and sends just those changes to the client.
Here is the most common publication you will see:
Meteor.publish("all-reviews", function() {
return Reviews.find({});
});
Meteor hides the complexities of what is really going on in this publication. This is closer to what is really going on:
Meteor.publish("all-reviews", function() {
var self = this;
//this is the query we want realtime updates for
//when relevant additions, removals or changes happen the correct callbacks will fire...
var handle = Reviews.find({}).observeChanges({
added: function(id, fields) {
//when a document is added it gets sent to the client.
//Note: the initial payload of data happens here, lets say you had 5 reviews
//this would get called 5 times as soon as a user subscribes.
self.added("reviews", id, fields);
},
removed: function(id) {
//when a document is removed the client is told which one
self.removed("reviews", id);
},
changed: function(id, fields) {
//when a document has a change made to its fields the changes get sent
self.changed("reviews", id, fields);
}
});
//letting the client know that the initial payload of data has been sent.
//Stuff like iron-routers waitOn would rely on this message before loading a template
//that relies on this publication
self.ready();
//stops the publication running forever. This will fire when a client no longer needs a
//subscription or when they close the page.
self.onStop(function() {
handle.stop();
});
});
As for what it going on in the docs example with the initializing flag. The initializing flag is used as a way of simply counting all the initial payload of existing reviews in your case then after the observeChanges call telling the client how many there are. This is an optimisation on the other way of doing it which would be to send the client several self.changed messages during the initial payload.
Maybe it will make more sense if I show how it would be done without the flag:
Meteor.publish("counts-by-entry", function (entryId) {
var self = this;
check(entryId, String);
var count = 0;
//we need to initially add the document we are going to increment
self.added("counts", entryId, {count: 0});
var handle = Reviews.find({entry: entryId}).observeChanges({
added: function (id) {
count++;
//So for example if there were 100 reviews for this entry when the user
//subscribes this self.changed would get called 100 times:
//self.changed("counts", entryId, {count: 1})
//self.changed("counts", entryId, {count: 2})
//self.changed("counts", entryId, {count: 3})
//self.changed("counts", entryId, {count: 4}) etc...
//which is a waste when we can just initially tell the client how
//many reviews there are at the point of them subscribing
self.changed("counts", entryId, {count: count});
},
removed: function (id) {
count--;
self.changed("counts", entryId, {count: count});
}
});
//remove the self.added(...) which handles the optimiation as explained above
self.ready();
self.onStop(function () {
handle.stop();
});
});
Regardless it doesn't look like that particular publish is the problem. I would expect the console to make it clear what the issue is

waitOn should return an array with the subscription handles.

Related

Necessary to pass limit to data when subscription already limits the dataset?

In a example about pagination, different number is passed to the route in terms of how many results should be displayed.
// lib/router.js
Router.route('/:postsLimit?', {
name: 'postsList',
waitOn: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
},
data: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return {
posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
};
}
});
I am wondering if it's necessary to still give the Posts.find() arguments in the data attribute. It seems kinda redundant here because the subscribe in waitOn already limit the dataset that i am getting back from the server. I know that data is used to provide data context for different parts of my templates. However, providing the same arguments here to data Posts.find() just seems redundant. I have tried just using Posts.find() without the argument and it worked.
Also, I am wondering what is a way to access all the data in router? I am accessing the Posts collections outside the individual route and I thought that I would be able to access all the data in Posts collections. However, it returned with a count 0.
// lib/router.js
var totalPostsCount = Posts.find().count()
console.log('total count?')
console.log(totalPostsCount) // output 0
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
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() {
// console.log("is find limited to subscriptions too??");
// console.log(Posts.find().count())
return Posts.find({}, this.findOptions());
},
data: function() {
// console.log('is data limited to this?')
// console.log(Posts.find().count())
var hasMore = this.posts().count() === this.postsLimit();
var adjustOrNot = this.posts()
if (!hasMore){
var nextPath = this.route.path({postsLimit: this.posts().count()});
}else{
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'
});
Edit 1: code for getting all posts:
// server
Meteor.publish('allPosts',function(){
return Posts.find({});
})
// lib/router.js
if (Meteor.isClient){
var handle = Meteor.subscribe('allPosts');
if (handle.ready()) {
// This logs the actual count of posts
console.log(Posts.find().count());
} else {
console.log('not ready yet');
}
}
This only outputs 'not ready yet' in the console and it's not changing even when the page finish loading.
Thanks a lot for the help.
Edit 2: possible solutions
I tried wrapping the reactive source ready() inside a computation such as Tracker.autorun() and now it worked.
if (Meteor.isClient){
var handle = Meteor.subscribe('allPosts');
Tracker.autorun(function(){
var status = handle.ready();
if (status){
console.log(Posts.find().count())
}else{
console.log('not ready yet')
}
})
}
Yes, you should pass limit and other query options to the front-end query too, regardless of your subscription. This is because if you were to have multiple subscriptions to the same collection active at the same time (as is often done in larger apps) the subscribed records end up all in the same Minimongo collection in the front-end. In this case, if you had omitted the query parameters, you would end up with unpredictable results. See more in this excellent explanation. Note also that while iron:router supports doing subscriptions in the route handlers, it's encouraged to handle them in the template lifecycle methods.
For your second question, the reason the Posts collection appears empty is that the code in the top level of the file is run immediately as the file is loaded. At this point, the collection subscription isn't loaded yet, so the front-end collection is empty. You need to check for subscription readiness in some reactive context, such as a template helper or Tracker.autorun, to ensure that the data is loaded:
var handle = Meteor.subscribe('posts');
if (handle.ready()) {
// This logs the actual count of posts
console.log(Posts.find().count());
} else {
// This logs 0 (unless there are other subscriptions to this collection)
console.log(Posts.find().count());
}

How to show the total count of items and a limit list of items on a same template

I have two sections on my page.
The first section has a limited list of items. The Second section has a total count of items (recordsCount).
When a server adds a new item I see the list of items is updated but the total count has an old value.
Tracks = new Mongo.Collection('tracks')
Client:
Meteor.subscribe('tracks')
Meteor.call('records', function(err, data) {
Session.set('_records', data)
})
Template.tracks.helpers({
tracks: function() {
return Tracks.find()
},
recordsCount: function() {
return Session.get('_records')
}
})
Server:
Meteor.publish('tracks', function() {
return Tracks.find({}, {limit: 100})
})
Meteor.methods({
records: function() {
return Tracks.find({}).count()
}
})
var CronManager = new Cron(10000)
CronManager.addJob(1, function() {
Tracks.insert({filed1: 'test'})
})
If you just want to efficiently bring counts into your app, check out the publish-counts package.
Meteor.publish('posts', function(author) {
var cursor = Tracks.find(...);
Counts.publish(this, 'tracks-count', cursor, {nonReactive: true});
});
The nonReactive options sends the count only on demand. This can be useful if you don't really need real-time counts, since most apps can do fine with updates every few seconds. That will save a lot of CPU.
Template.tracks.helpers({
tracks: function() {
return Counts.get('posts-count')
}
});
Hattip to Arunoda's excellent chapter on this from Bulletproof Meteor, Counting Documents.
I've investigated the solution with the publish-counts package. In my case it is the very heavy package. It was loading CPU all the time. I've replace my server side code to use a collection with count field.
Common:
Counter = new Mongo.Collection('count')
Server:
Meteor.Publish('count', function() {
return Counter.find()
})
if(Counter.find().count() === 0) Counter.insert({count: Tracks.find().count()})
var CronManager = new Cron(10000)
CronManager.addJob(1, function() {
Counter.update({}, {$inc: {count: 1}})
Tracks.insert({filed1: 'test'})
})

Meteor - how do I make this "reactive" using Deps?

On my client side, I display a list of users and a small chart for each user's points stored in the DB (using jQuery plugin called sparklines).
Drawing the chart is done on Template.rendered method
// client/main.js
Template.listItem.rendered = function() {
var arr = this.data.userPoints // user points is an array of integers
$(this.find(".chart")).sparkline(arr);
}
Now I have a Meteor method on the server side, that is called on a regular basis to update the the user points.
Meteor.methods({
"getUserPoints" : function getUserPoints(id) {
// access some API and fetch the latest user points
}
});
Now I would like the chart to be automatically updated whenever Meteor method is called. I have a method on the template that goes and calls this Meteor method.
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id);
}
});
How do I turn this code into a "reactive" one?
You need to use reactive data source ( Session, ReactiveVar ) together with Tracker.
Using ReactiveVar:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
var instance = Template.instance();
Meteor.call("getUserPoints", this._id, function(error, result) {
instance.userPoints.set(result)
});
}
});
Template.listItem.created = function() {
this.userPoints = new ReactiveVar([]);
};
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = self.userPoints.get();
$(self.find(".chart")).sparkline(arr);
})
}
}
Using Session:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id, function(error, result) {
Session.set("userPoints", result);
});
}
});
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = Session.get("userPoints");
$(self.find(".chart")).sparkline(arr);
})
}
}
Difference between those implementation :
A ReactiveVar is similar to a Session variable, with a few
differences:
ReactiveVars don't have global names, like the "foo" in
Session.get("foo"). Instead, they may be created and used locally, for
example attached to a template instance, as in: this.foo.get().
ReactiveVars are not automatically migrated across hot code pushes,
whereas Session state is.
ReactiveVars can hold any value, while Session variables are limited
to JSON or EJSON.
Source
Deps is deprecated, but still can be used.
The most easily scalable solution is to store the data in a local collection - by passing a null name, the collection will be both local and sessional and so you can put what you want in it and still achieve all the benefits of reactivity. If you upsert the results of getUserPoints into this collection, you can just write a helper to get the appropriate value for each user and it will update automatically.
userData = new Meteor.Collection(null);
// whenever you need to call "getUserPoints" use:
Meteor.call("getUserPoints", this._id, function(err, res) {
userData.upsert({userId: this._id}, {$set: {userId: this._id, points: res}});
});
Template.listItem.helpers({
userPoints: function() {
var pointsDoc = userData.findOne({userId: this._id});
return pointsDoc && pointsDoc.points;
}
});
There is an alternative way using the Tracker package (formerly Deps), which would be quick to implement here, but fiddly to scale. Essentially, you could set up a new Tracker.Dependency to track changes in user points:
var pointsDep = new Tracker.Dependency();
// whenever you call "getUserPoints":
Meteor.call("getUserPoints", this._id, function(err, res) {
...
pointsDep.changed();
});
Then just add a dummy helper to your listItem template (i.e. a helper that doesn't return anything by design):
<template name="listItem">
...
{{pointsCheck}}
</template>
Template.listItem.helpers({
pointsCheck: function() {
pointsDep.depend();
}
});
Whilst that won't return anything, it will force the template to rerender when pointsDep.changed() is called (which will be when new user points data is received).

Fetching mongodb one by one

I have a collection named Questions.
I want to fetch first question, then when user clicks a button, give him the next..
So i'm fetching the first item like this:
Template.home.user_questions = function () {
return Questions.find({}, {sort: {answer1:{'$ne': ''}}, limit: 1});
}
And I'm getting the user's click like this:
'click input' : function () {
Questions.update(this._id, {$inc: {value1: 1}})
// now show him next item
}
But I can't seem to figure out how to show the next item since I hasNext() and next() methods are not supported by meteor
You could use skip.
Template.home.user_questions = function () {
var skip = Session.get("skip") || 0;
return Questions.find({}, {sort: {answer1:{'$ne': ''}}, limit: 1, skip: skip});
}
Then when you want to move to the next question, augment the session value of skip by 1. e.g
Session.set("skip", (Session.get("skip") || 0) ++);
This should reactively move to the next question, up to the last.

Meteor observe changes added callback on server fires on all item

Tracker.autorun(function() {
DATA.find().observeChanges({
added: function(id, doc) {
console.log(doc);
}
});
});
This code is being called on the server. Every time the meteor server starts, the added function fires for every single item in the database. Is there a way to have the added callback fire only when new items are added?
added will be called for every document in the result set when observeChanges is first run. The trick is to ignore the callback during this initialization period. I have an expanded example in my answer to this question, but this code should work for you:
(function() {
var initializing = true;
DATA.find().observeChanges({
added: function(id, doc) {
if (!initializing) {
console.log(doc);
}
}
});
initializing = false;
})();
Note that Tracker.autorun is a client-only function. On the server I think it only ever executes once.
I struggled with this for a long time. For some reason, David's answer did not work for me - it was firing after the initializing variable was set to false.
This pattern from Avi was successful for me:
var usersLoaded = false;
Meteor.subscribe("profiles", function () {
// at this point all new users sent down are legitimately new ones
usersLoaded = true;
});
Meteor.users.find().observe({
added: function(user) {
if (usersLoaded) {
console.log("New user created: ", user);
}
}
});
Since it is initialization issue, you can do this.
var observerOfMessages = Messages.find({}).observe({
added: function(doc){
if(!observerOfMessages) return;
console.log(doc)
}
});
This is more elegant actually.
Provide a selector for the query which does not match old items. If using mongo ObjectID as _id you could query for items that have _id greater than the latest item's:
const latest = DATA.findOne({}, {sort: {_id: -1}})
DATA.find({_id: {$gt: latest._id}}).observeChanges({
added: function() { ... }
})
Or with createdAt timestamp:
const currentTime = new Date()
DATA.find({createdAt: {$gt: currentTime}}).observeChanges({
added: function() { ... }
})
Here's another way to solve this:
Meteor.subscribe('messages', function() {
var messages = Messages.find();
var msgCount = messages.count();
messages.observe({
addedAt: function(doc, atIndex) {
if(atIndex > (msgCount - 1)) console.log('added');
}
});
});
Should only fire for docs added after the existing amount is delivered. It's important that this goes in an onReady callback for Meteor.subscribe so that the msgCount changes as your subscription does... if for example, you're paginating your subscriptions.
cursor.observe() documentation

Categories