I am currently building a quizz app in Meteor and have three function within helpers
Template.quizPage.helpers({
//this helper finds the currentquiz and returns the currentquestion to the template
question: function(){
currentquiz = this.quiztitle;
currentQuestion = Session.get('currentQuestion') || 0;
return Quizzes.findOne({'quiztitle': currentquiz}).quizquestions[currentQuestion]
},
length: function (){
questionlength = $(Quizzes.findOne({'quiztitle': currentquiz}).quizquestions).length;
return questionlength;
},
answers: function(){
currentquiz = this.quiztitle;
currentQuestion = Session.get('currentQuestion') || 0;
return Quizzes.findOne({'quiztitle': currentquiz}).answers[1][currentQuestion]
}
});
As you can see some of this code is already duplicate (currentquiz = this.quiztitle). How can I share currentquiz between functions within a helper?
This is becoming a real problem as I need to define the variable currentquiz one time
currentQuestion = [0,0,0,0,0];
But current code resets the currentquestion any time I activate the helper in the template. I can;t define it above the function $(document.ready) wrap to set the variable or define it . This should be really easy right?
i know this is an old thread but I just came across the same problem. What you can do is define a reactive variable on the template level:
var abc;
Template.XY.rendered = function(){
abc = new ReactiveVar("abc");
},
Template.XY.helpers({
someFunction:function(){
return abc.get();
}
)}
I define a var abc; to bind the variable to the template instance (i guess this can also be done with this.abc = new ... )
To provide a variable available in the template helpers and events you just need to add it in the template instance.
In the onCreated function you can access the template instance with the keyword this.
Template.myTemplate.onCreated(function(){
this.mySharedVariable = new ReactiveVar("myValue");
});
In the helpers you can access to the templace instance with Template.instance().
Template.myTemplate.helpers({
myHelper1(){
const instance = Template.instance();
instance.mySharedVariable.get(); // Get the value "myValue"
// Do something
},
myHelper2(){
const instance = Template.instance();
instance.mySharedVariable.get(); // Get the value "myValue"
// Do something
}
});
In the events you can access to the template instance with the second parameter of the function.
Template.myTemplate.events({
'click .js-do-something' : function(event, instance){
instance.mySharedVariable.set("newValue"); // Set the value "newValue"
}
});
In the example above i'm using a ReactiveVar so if its value get changed the two helpers myHelper1 and myHelper2 will re-execute. But be free to use normal variable depending on your need.
PS: Because Session are global, you should not use Session if you only use this "shared" variable inside your template.
Related
Clearly, I am doing something wrong with ReactiveVar because I cannot get it to work as I expect it should.
I am trying to set the value of an ReactiveVar by calling a Meteor.call method which returns the list of usernames. But it does not update when the usernames get changed in another part of the app.
I tried both:
Template.qastatistics.created = function () {
this.trackUsernames = new ReactiveVar(false);
var instance = Template.instance();
Meteor.call('trackUsernames', function (err, data) {
instance.trackUsernames.set(data);
});
};
and:
Template.qastatistics.helpers({
users: function () {
var usernames,
instance = Template.instance();
if (instance.trackUsernames.get() === false) {
Meteor.call('trackUsernames', function (err, data) {
instance.trackUsernames.set(data);
});
}
usernames = instance.trackUsernames.get();
...
But neither updates the list of usernames when these change in the database.
Is this even possible with ReactiveVars or have I completely misunderstood them?
EDIT: The usernames I mention are not from Meteor.users collection, but rather a distinct call from another collection that has usernames in it.
Fist of all I would use the onCreated function instead of defining created. That's a little more extendable and it's the new API. created is just kept around for backwards compatibility.
About your problem. You are right, you seem to have misunderstood what ReactiveVars do. They are a reactive data source. That means that when you call myReactiveVar.get in some Tracker.autorun (aka. reactive computation), the computation will rerun whenever myReactiveVar.set is called.
You got the first part right. Spacebars helpers always run inside their own computation. What you got wrong is thinking that a method call is a reactive action. That means, that you could call trackUsernames and set the trackUsernames ReativeVar again and the value in your template would update itself. But a method is only run once. It doesn't do anything fancy with reactivity.
A method call only transfers data once. When you publish a set of documents (like all users) on the other hand, they will be updated dynamically. Whenever a change happens inside that set of published documents, it will be synced to the client. So in general, it's a better idea to use publications and subscriptions to sync data reactively. If you'd want to use a method for the same thing you'd need to do some kind of polling (so your back in the stone-age again).
The easiest way to implement what you are trying to do is to use Meteor.users.find().fetch(). As it says in the docs fetch registers dependencies for all the documents you are fetching if it's being called from within a reactive computation.
First you'll need to properly set up your publications, so that users can see other users usernames. I'll leave that to you. Then you need to reimplement your helper
Template.qastatistics.helpers({
users: function () {
var usernames = _.pluck(Meteor.users.find().fetch(), 'username');
...
Thanks to suggestions from #kyll, I managed to get what I wanted by publishing the data I need:
server:
cope.publish.usernamesID = Random.id();
Meteor.publish("itemsusernames", function () {
self = this;
var initializing = true;
var handle = Items.find().observeChanges({
added: function (id) {
!initializing && self.changed(
"itemsusernames",
cope.publish.usernamesID,
Items.distinct("p4User"));
},
changed: function (id) {
!initializing && self.changed(
"itemsusernames",
cope.publish.usernamesID,
Items.distinct("p4User"));
},
removed: function (id) {
!initializing && self.changed(
"itemsusernames",
cope.publish.usernamesID,
Items.distinct("p4User"));
}
});
initializing = false;
self.added("itemsusernames", cope.publish.usernamesID, Items.distinct("p4User"));
self.ready();
self.onStop(function () {
handle.stop();
});
});
client:
users: function () {
var usernames = [],
oUsernames = ItemsUsernames.find().fetch();
if (!oUsernames[0]) return [];
usernames = $.map(oUsernames[0], function (value, index) {
if (!isNaN(index)) {
return [value];
}
});
...
And ofcourse: ItemsUsernames = new Mongo.Collection("itemsusernames");
I have a helper function that depends on a collection document lookup, the result of which it passes to a subscription via a Session. It then needs to query the documents from that subscription.
The code explains it better than I could.
Helper:
var selection = Selections.findOne()
var itemIds = function() {
return selection && selection.itemIds
}
var itemIdsArray = itemIds()
Session.set('itemIdsArray', itemIdsArray)
console.log(Session.get('itemIdsArray'))
_.each(Items.find({_id: {$in: itemIdsArray}}).fetch(), function(element, index, list) {
//doing stuff
})
Subscription:
Meteor.subscribe('itemsById', Session.get('itemIdsArray'))
Publication:
Meteor.publish('itemsById', function(itemIdsArray) {
return Items.find({_id: {$in: itemIdsArray}})
})
My console.log returns an undefined value before it returns the array of IDs. So undefined gets passed all the way to the publication, which complains of a null value (which is weird in itself) after $in and breaks.
My solution was to set the Session to default to [],
Session.setDefault(`itemIdsArray`, [])
which I honestly had high hopes that it'd work, but alas, it did not.
I've tried putting it inside IronRouter's onBeforeAction, I've tried putting it at the top of the helper, I've tried putting it pretty much anywhere but it still logs and returns undefined once before it gets the correct value.
I've also tried to move around my subscription, from waitOn to subscriptions to onAfterAction to onRendered, but those attempts have been utterly fruitless.
What should I do?
That's fairly typical behavior in Meteor. Session variables are not always ready at the time. The usual way of dealing with this is to introduce a guard in the helper that checks the variable is defined before doing anything else with it.
In your case something like this would work: itemsIdArray = itemIds() || [];
To answer the actual question you are asking, where do you set Session defaults that they are available in your subscriptions: it's not important where you set them, but when you access them. You can wait for the subscription to be ready using iron router's waitOn() function, or you can check the subscription handle's ready() function (see https://github.com/oortcloud/unofficial-meteor-faq#user-content-how-do-i-know-when-my-subscription-is-ready-and-not-still-loading)
If you return a subscription in your waitOn option of Iron Router you should have the data in your template then:
Router.route('/yourRoutePath/:_id', {
// this template will be rendered until the subscriptions are ready
loadingTemplate: 'loading',
waitOn: function () {
// return one handle, a function, or an array
return Meteor.subscribe('itemsById', this.params._id);
},
action: function () {
this.render('myTemplate');
}
});
Your template helper:
Template.myTemplate.helpers({
items: function() {
return Items.find();
}
});
I noticed that you publish Items collection and you want to use Selections collection in your helper. If you need more than one subscription, you can return an array of subscriptions in waitOn:
waitOn: function () {
// return one handle, a function, or an array
return [
Meteor.subscribe('itemsById', this.params._id),
Meteor.subscribe('selections')
];
}
WaitOn ensures that your template will be rendered when all subscriptions are ready.
I would like to store an object that I create during Template.myTemplate.rendered = function ( ) { ... } and use that object in Template.myChildTemplate.helpers(helpers). So far I'm resorting to using a global object, but that feels very hacky. Is there a nice Template-centric way of doing this?
You could use UI._templateInstance(); and ReactiveDict (to make your data reactive too) (added with meteor add reactive-dict
Template.myTemplate.created = function() {
this.templatedata = new ReactiveDict();
}
Template.myTemplate.rendered = function() {
this.templatedata.set("myname", "value";
};
Template.myTemplate.helpers({
myvalue: function() {
var tmpl = UI._templateInstance();
return tmpl.templatedata.get('myname');
}
});
This will allow you to use this template multiple times on the page, and still have a variable scope to each template, which global variables or non instance variables wouldn't allow.
A note of warning, the current iron router (0.7.1) breaks UI._templateInstance();, which is an open bug at the moment.
There's no such method yet, unfortunately.
The common pattern is to use reactive dict either as a file-wide variable, or in a namespace related to the template if you need the access in several files. The downside of this solution is that such variable is shared among all instances of the same template, so you have to work around this if you render this template in more than one place.
var data = new ReactiveDict();
Template.myTemplate.rendered = function() {
data.set('key', 'value');
};
I'm trying to use a client side collection as a site configuration system. I insert documents representing my different pages, and the iron-router and navigation tabs all use them to determine what pages they are and what templates are represented by them. Each page uses a {{> contentTemplate}} inclusion helper to load it's relevant template.
It all works great, when the data has all loaded. When I restart the app on certain pages, the data hasn't loaded yet, and I receive the Exception from Deps recompute function: Error: Expected null or template in return value from inclusion function, found: undefined error.
Here's my javascript:
StoriesArray = [
{ category: 'teaching', contentTemplate: 'teachingHome', title: 'Teaching Home'},
...
];
Stories = new Meteor.Collection(null);
StoriesArray.forEach(function (story, index) {
story._id = index + '';
Stories.insert(story);
});
// in main.js
Template.teachingPost.contentTemplate = function() {
console.log(this);
console.log(this.contentTemplate);
return Template[this.contentTemplate];
};
// in router.js
this.route('teaching', {
layoutTemplate: 'teachingPost',
data: function() { return Stories.findOne({contentTemplate: 'teachingHome', category: 'teaching'}); }
});
The console logs in the contentTemplate helper above log twice, the first time as this:
Object {} main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:45
undefined main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:46
and the second time as this:
Object {category: "teaching", contentTemplate: "teachingHome", title: "Teaching Home"} main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:45
teachingHome main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:46
so the router is simply trying to load this data too early.
I've tried putting the StoriesArray loading process into different files all over my app, including lib, and even tried putting it into Meteor.startup, but it's always the same result.
The normal iron-router waitOn/subscription pattern doesn't really apply here, since this is a client side collection built with null, that has no server representation. I don't want this to have server representation, because this is static content that there's no need to go to my server for.
How do I ensure this information is done before continuing?
Untested, but per Iron Router's docs on waitOn:
Returning a subscription handle, or anything with a ready method from the waitOn function will add the handle to a wait list.
Also in general it's better to use find with data, rather than findOne, as find will return an empty cursor when the collection is empty as opposed to findOne returning undefined. So try this:
// in router.js
this.route('teaching', {
layoutTemplate: 'teachingPost',
data: function() {
return Stories.find({contentTemplate: 'teachingHome', category: 'teaching'});
},
waitOn: function() {
var handle = {};
handle.ready = function() {
if (Stories.find().count() !== 0)
return true;
else
return false;
}
return handle;
}
});
And adjust your Template.teachingPost.contentTemplate function to work with a cursor rather than an object.
Ok, let's say I have a client side function that returns a Session variable, eg:
Template.hello.random = function() {
return Session.get('variable');
};
Somewhere else let's say I have a button that does
Session.set('variable', random_number);
Everytime I hit that button, will my Template.hello.random function run? I find it hard to wrap my head around that..
All the Meteor "Magic" comes from Deps.Dependency and Deps.Computation (http://docs.meteor.com/#deps_dependency)
eg
var weather = "sunny";
var weatherDep = new Deps.Dependency;
var getWeather = function () {
weatherDep.depend()
return weather;
};
var setWeather = function (w) {
weather = w;
// (could add logic here to only call changed()
// if the new value is different from the old)
weatherDep.changed();
};
Session mirror's this pattern, but as a key/value store instead of just one value. (Actually Session is just an instance of the ReactiveDict class)
When you have a computation that calls getWeather() the .depend() call links it to the computation - and the .changed() call invalidates that computation.
eg, getting a computation via Deps.autorun()
computaton = Deps.autorun(function(){
var localWeather = getWeather();
console.log("local weather", localWeather);
});
computation.onInvalidate(function(){
console.log("a dependency of this computation changed");
});
console.log("setting weather");
setWeather("abc");
With this infrastructure - we find out that Template helpers are run in a computation - and when the dependencies of the computation are .changed(), it queues up the template to re-render.