I have this Meteor code which is meant to update a notifications' template in real time whenever an appropriate action is triggered:
~/server/app.js
Meteor.methods({
notifs: function(){
return Meteor.users.findOne({_id:this.userId}, {'profile.notifs': 1});
}
});
and:
~/client/apps.js
Template.notifs.helpers({
notifs: function(){
Meteor.call('notifs', function(err, data){
Session.set('notifs', data);
});
return Session.get('notifs');
}
});
finally:
~/public/templates/notifs.html
<template name='notifs'>
{{#each notifs}}
<p>...</p>
{{/each}}
</template>
What this code does for the moment is just listing the notifications when the user logs in, but fails to update in real time to show new notifications as actions are triggered.
New notifications show up only after a page refresh (which is, honestly, useless).
After hours of googling I give up and post this here so someone can help me, please.
Thanks in advance.
At it's core, this question is about reactivity. Because Meteor.call isn't a reactive data source (it's just a remote procedure call), the helper won't run again if the underlying data changes.
Because the necessary user document is already published (it's for the current user), the method call isn't needed. You could rewrite your helper using find (a reactive data source) like this:
Template.notifs.helpers({
notifs: function() {
return Meteor.users.findOne({_id: Meteor.userId()});
}
});
However, Meteor.user() gives you the same functionality:
Template.notifs.helpers({
notifs: function() {
return Meteor.user();
}
});
However, you don't really even need this because templates come with the {{currentUser}} helper. So you can drop the helper altogether and modify your template like this:
<template name='notifs'>
{{#each currentUser.notifs}}
<p>...</p>
{{/each}}
</template>
If you ever actually do need the result of a meteor call in a helper, you should read the answers to this question.
Related
I'm new to meteor.js. Still getting used to it.
I get how templates update reactively according to the cursor updates on the server, like this:
{{#if waitingforsomething.length}} Something Happened! {{/if}}
This is good to display elements on the page, updating lists and content. Now, my question is: what if I want to call some javascript or fire some event when something gets updated reactively? What would be the right way to do it with meteor.js?
Anything inside Tracker.autorun or template instance this.autorun runs with changes in reactive data sources inside these autoruns.
Reactive data sources are ReactiveVar instances, db queries, Session variables, etc.
Template.myTemplate.onCreated(function() {
// Let's define some reactive data source
this.reactive = new ReactiveVar(0);
// And put it inside this.autorun
this.autorun(() => console.log(this.reactive.get()));
});
Template.myTemplate.events({
// Now whenever you click we assign new value
// to our reactive var and this fires
// our console.log
'click'(event, template) {
let inc = template.reactive.get() + 1;
template.reactive.set(inc);
}
});
It is a little bit outdated, but Sacha Greif's Reactivity Basics is a very quick and concise introduction to meteor's reactivity model.
Basically, you have what's called reactive computations, code that observes special data objects (sessions, subscriptions, cursors, etc.) and gets executed whenever any of these reactive sources changes.
This is exposed via the Tracker API
Computation works pretty well for me:
Template.myTemplate.onRendered(function() {
this.computation = Deps.autorun(function () {
if (something) {
$(".reactive").html("Something Happened!");
}
});
});
Template.myTemplate.destroyed = function(){
if (this.computation){
this.computation.stop()
}
};
I Hope this helps.
I use Meteor 1.2.1. I am currently developing a Course Catalog Web App.
I'm trying to create a filtering feature just like Angular Filters. It should let the user specify his needs and then the course list refreshes automatically as he changes the filters.
This is my code:
app.html
<!-- ********** -->
<!-- courseList -->
<!-- ********** -->
<template name="courseList">
<div class="list-header">
<div>Course Name</div>
<div>Description</div>
<div>Category</div>
<div>Modality</div>
<div>Unit</div>
</div>
<div class="list-body">
{{ #each course }}
{{ > courseRow}}
<hr />
{{ /each }}
</div>
</template>
app.js
Template.courseList.helpers({
'course': function(){
Session.setDefault('course', Courses.find().fetch());
return Session.get('course');
}
});
So when I run this, I get an empty set for the course helper above, even though there's some dummy data present on the database.
It's seems to me that the issue is with Session.setDefault(). When I console.log the Session.course variable right after the find, I get an empty array [], maybe because there was no time to get the data from the server (or maybe not, because I'm developing with autopublish for now, I don't really know).
After I apply some of the filters (code not shown here), everything goes back to normal. This initialization only is the problem.
I've tried to call Session.set() inside Template.courses.rendered(), Template.courses.created(), Template.courses.onRendered(), Template.courses.onCreated() (yes, I was kinda desperate) but none of them worked.
Can someone please advise on that issue? Maybe I'm not trying the correct Meteor aproach, as I am a Meteor beginner.
Thanks!
I think you try to use template subsciption. so you can send some terms for publication. example;
Template.foo.onCreated(function() {
this.name = new reactiveVar("");
this.autorun( () => {
const terms = {
name: this.name.get()
};
this.subscribe("Posts_list", terms);
});
});
Template.foo.helpers({
lists: function() {
return Posts.find().fetch();
}
});
There are two things here. First thing is that your helper will run whenever either the result of Courses.find().fetch() or Session.get('course') changes, since both of them are reactive data sources. To avoid this, you can do
Template.foo.helpers({
lists: function() {
return Posts.find().fetch();
}
});
If you want to set the session variable so that you can use it somewhere else, then you can use Session.set instead of Session.setDefault, because Session.setDefault will only assign value only is the session variable is not already available. But I don't know why you want to do that, since you can use Posts.find().fetch() where ever required instead of session variable. Both are reactive.
Template.foo.helpers({
lists: function() {
Session.set('course', Courses.find().fetch());
return Session.get('course');
}
});
If you want to assign the course session variable only for the first time when you get non-empty data then you might want to do something like this.
Template.foo.helpers({
lists: function() {
if (Courses.find().count() > 0) {
Session.setDefault('course', Courses.find().fetch());
return Session.get('course');
} else {
return [];
}
});
If you can tell why you need the session variable, we might think of a different approach to solve the problem.
Hope it helps.
Just started using Meteor and am loving it thus far.
I'm making a variation of the tutorial "todo" app, but with my own customization.
More or less the changes I made so far have been:
Changed directory structure to client, server, public, lib/collections folders
Made the task collection through collection2 schema rather than dynamically creating with Tasks.insert(title = text,etc etc)
So to the question.
These are my client/server files -
server.js
Meteor.publish("tasks", function () {
return Tasks.find({}, {sort: {checked: 1}});
});
client.js
// This code only runs on the client
Meteor.subscribe("tasks");
Template.task.events({
"click .toggledone": function () {
// Set the checked property to the opposite of its current value
Tasks.update(this._id, {
$set: {checked: ! this.checked}
});
},
"click .delete": function () {
Tasks.remove(this._id);
}
});
And its being displayed in the standard html
<ul>
{{#each tasks}}
{{> task}}
{{/each}}
</ul>
So basically as it is now, my server is sorting the tasks on the basis of whether it is checked (completed) or not, ascending.
It works currently, when I click a task on the list, it turns red (my configuration) and registers as completed, but the list doesn't sort automatically like it did originally (before I made the directory changes).
For it to update, I need to refresh the page which is not what I want.
I'm new to this and i think it has something to do with my publish/subscribe but I can't seem to find anything to help me online.
Can anyone point me in the right direction for this?
Thanks so much.
I think you are confused between the publication/subscription named tasks and the collection tasks for the loop on the template. When you want to render the tasks on the template, it should be returned from a helper. Consider that publication/subscription making the correct part of the database available on the client and then, you retrieve the correct data from that part to display.
Normally, I make the naming convention for the publication like the SQL query so it is easier to interpret
Meteor.publish("select-from-tasks", function () {
return Tasks.find({});
});
and then,
Meteor.subscribe("select-from-tasks");
You will need to have a helper to return the correct data, like I said above
Template.task.helpers({
tasks: function() {return Tasks.find({}, {sort: {checked: 1}});}
});
I have an asynchronous method which does a Facebook request for retrieving photos from the current logged in user.
I have a template where I loop over these results. But since the call to Facebook is asynchronous, it fails.
<template name="mytemplate">
{{#if photos}}
{{> images pics=photos}}
{{/if}}
</template>
<template name="images">
{{#with pics}}
{{#each this}}
{{images}}
{{/each}}
{{/with}}
</template>
Here is my javascript:
Template.mytemplate.helpers({
photos: function () {
return Template.instance().myAsyncValue.get();
}
});
Template.mytemplate.created = function (){
var self = this;
self.myAsyncValue = new ReactiveVar("Waiting for response from server...");
Meteor.call('getPhotos', function (err, data) {
if (err)
console.log(err);
else
self.myAsyncValue.set(data.data);
});
}
I already have a variable that is being watched, but the problem is that my template is already created and the result is not an array yet.
Can someone show me the best practices on how to use an asynchronous array result in a template. I want to create image tags in the template, not with jquery or javascript.
Thanks
If I understand this right, you're getting an error when the helpers are loaded because photos is not an array. Try this:
Template.mytemplate.helpers({
photos: function () {
var val = Template.instance().myAsyncValue.get();
return val.constructor === Array && val;
}
As a side question for you, why are you using a ReactiveVar for an array? I have only found a use for using them on primitives, since reactivity doesn't do a deep scan, but maybe I'm missing out :)
Thanks for the quick response.
I've figured out after I wrote the question that my problem was not giving an actual array when the call was not yet made.
Regarding your side question. I have to use this ReactiveVar in order to keep track of the result, no? Because my variable hasn't got a value first. The template is already being setup. Then when I get a response, the value changed and my each loop reacts over the received array. That's how I understood this ReactiveVar anyway :)
I'm trying to wrap my head around Meteor's reactivity. I understand it re-renders a page when a reactive data source that's being referenced in a template changes. I also understand what constitutes a reactive source (Session, MongoDB cursors, etc.).
What I'm having trouble understanding are all these "behind my back" calls to my template helpers. There seems to be something more than reactivity that causes them.
In particular, in the following code, I have a friendRequests helper that gets recomputed sometimes two sometimes three times during a single access to the /friends page. If it gets recomputed twice, the DB queries succeed! If it gets recomputed thrice, the first DB access (for some odd reason) fails to query the DB while the latter two succeed.
This is the stack trace when the DB fails:
// NOTE: imsolonely is one of the users that should be returned in the friendRequests
imsolonely's public key: undefined
debug.js:41 Exception in template helper: TypeError: Cannot read property 'profile' of undefined
at Object.Utils.getPublicKeyByUsername (http://localhost:3000/lib/utils.js?acf4e03d4c8a70819c26f8d2fd08caf7100768fe:79:22)
at Object.Utils.getFingerprintByUsername (http://localhost:3000/lib/utils.js?acf4e03d4c8a70819c26f8d2fd08caf7100768fe:88:24)
at http://localhost:3000/client/friends.js?dbec4a7537c9d0abf56a74489824969cb7baadfe:25:35
at Array.forEach (native)
at Function._.each._.forEach (http://localhost:3000/packages/underscore.js?0a80a8623e1b40b5df5a05582f288ddd586eaa18:156:11)
at Object.Template.friendRequests.helpers.friendRequests (http://localhost:3000/client/friends.js?dbec4a7537c9d0abf56a74489824969cb7baadfe:22:7)
at http://localhost:3000/packages/blaze.js?77c0809654ee3a10dcd5a4f961fb1437e7957d33:2693:16
at http://localhost:3000/packages/blaze.js?77c0809654ee3a10dcd5a4f961fb1437e7957d33:1602:16
at Object.Spacebars.dot (http://localhost:3000/packages/spacebars.js?3c496d2950151d744a8574297b46d2763a123bdf:231:13)
at http://localhost:3000/client/template.friends.js?a2da726f6dad1aaecfdedfe216aa3378fff938b5:24:37
utils.js?acf4e03d4c8a70819c26f8d2fd08caf7100768fe:78 imsolonely's public key: {"profile":{"publicKey":"0404b4880129edc1ea2652dd1eff1c8728269874b9b0ace02cc90edcb449c3f3d716c2f8b79a5fe5695d52cd85aed228f977073538625e8e71f1cfd764766669b1"},"_id":"sXjzt7YHA8KTyAib5"}
utils.js?acf4e03d4c8a70819c26f8d2fd08caf7100768fe:78 imsolonely's public key: {"profile":{"publicKey":"0404b4880129edc1ea2652dd1eff1c8728269874b9b0ace02cc90edcb449c3f3d716c2f8b79a5fe5695d52cd85aed228f977073538625e8e71f1cfd764766669b1"},"_id":"sXjzt7YHA8KTyAib5"}
This also happens with many (or all, not sure) of my helpers. They get called when they are should not even be needed because the user is not logged in (and the template is not rendered, thus the helper should not be recomputed).
Here's some code:
client/friends.js:
Template.friendRequests.helpers({
friendRequests: function () {
// Problem 1: The template gets called right after I log in but before I am fully logged in
// so I need this call here.
if(!Meteor.user())
return Utils.notLoggedInErrorMsg;
// Problem 2: The template gets called three times. The first time fails the DB query
// as if the DB row did not exist. The next 2 times it succeeds. But it should only be
// called once.
var reqs = Friends.getFriendRequests(Utils.me());
_.each(reqs, function(element, it, list) {
check(element.initiator, String);
// Get the user's fingerprint
element.fingerprint = Utils.getFingerprintByUsername(element.initiator);
});
return reqs;
},
});
client/friends.html:
<template name="friends">
{{> friendRequests}}
{{> searchForm}}
<h2>My friends</h2>
<ul>
{{#each friends}}
<li>{{this}}</li>
{{/each}}
</ul>
</template>
<template name="friendRequests">
<h2>Friend requests</h2>
{{#if friendRequests.length}}
<p>Someone's popular today!</p>
<ul>
{{#each friendRequests}}
<li><b>{{initiator}}</b> with fingerprint <pre style="display: inline">{{fingerprint}}</pre> sent you a request on <em>{{date}}</em>. Accept <b>{{initiator}}</b> as a friend?</li>
{{/each}}
</ul>
{{else}}
<p>Sorry, nobody likes you right now.</p>
{{/if}}
</template>
lib/utils.js:
Utils = {
// ...
// other stuff
// ...
// #return a hex-encoded public key as a string
getPublicKeyByUsername: function (username) {
var user = Meteor.users.findOne({ username: username }, { fields: { 'profile.publicKey': 1 } });
console.log(username + '\'s public key: ' + EJSON.stringify(user));
var pubKey = user.profile.publicKey;
return pubKey;
},
// NOTE: not used yet, i used the CryptoUtils function directly when I needed it
//
// #return the fingerprint as a hex-encoded string
getFingerprintByUsername: function (username) {
var pubKey = Utils.getPublicKeyByUsername(username);
var fingerprint = CryptoUtils.getPublicKeyFingerprint(pubKey);
return fingerprint;
},
notLoggedInErrorMsg: 'Meteor is being silly and calling this when I\'m not logged in.',
}
If it matters, I am using the iron:router 1.0.3 package for redirecting to the /friends URL.
Any clarifications as to why the friendRequests helper is being recomputed when it's out of view, why it's being recomputed sometimes twice, sometimes thrice, instead of once when I refresh the /friends page would be greatly appreciated!
Thank you,
Alin
In general you should expect that your helpers will get called many times. If your template is being rendered while the user is logging in, and without waiting on the data to be published, it's likely that your helper will run:
When the route is requested
When the user finishes logging in
When Friends data first arrives on the client
When additional Friends data arrives or is modified
To solve the first problem, you can check for Meteor.user() || Meteor.loggingIn() as I do in the answer to this question (note the router API has changed since I answered that but it should give you an idea of what to do).
The answer to your second issue can be found in my post on guards. In short you can't assume that the data has arrived on the client, so you either need to check for its existence or you need to explicitly wait on the subscription in your router prior to rendering the template.
It turns out there was an error in my waitOn hook. I was not returning anything, I was just making Meteor.subscribe() calls instead of returning an array of them. Here's what fixed the DB exceptions:
Router.configure({
layoutTemplate: 'mainLayout',
loadingTemplate: 'loading',
waitOn: function () {
return [
Meteor.subscribe('userData'),
Meteor.subscribe('allUserData'),
Meteor.subscribe('UserProfiles'),
];
},
});
I still don't have a good solution for the Meteor.user() being undefined. Or maybe this fixed it as well?
PS: Thank you #DavidWeldon for helping me narrow this down!