I'm retrieving a Collection document based on a Session variable, and then passing this as a variable called store through an iron:router data context. The problem is that it sometimes returns undefined, as if it had not prepared itself in time for the helper to execute. How do I ensure the variable is always defined before the helper/template runs?
Here's my route, you can see that the data context includes retrieving a doc from a collection based on an _id stored in a Session variable:
Router.route('/sales/pos', {
name: 'sales.pos',
template: 'pos',
layoutTemplate:'defaultLayout',
loadingTemplate: 'loading',
waitOn: function() {
return [
Meteor.subscribe('products'),
Meteor.subscribe('stores'),
Meteor.subscribe('locations'),
Meteor.subscribe('inventory')
];
},
data: function() {
data = {
currentTemplate: 'pos',
store: Stores.findOne(Session.get('current-store-id'))
}
return data;
}
});
And here is the helper which relies on the store variable being passed to the template:
Template.posProducts.helpers({
'products': function() {
var self = this;
var storeLocationId = self.data.store.locationId;
... removed for brevity ...
return products;
}
});
This is a common problem in Meteor. While you wait on your subscriptions to be ready, this does not mean your find function had time to return something. You can solve it with some defensive coding:
Template.posProducts.helpers({
'products': function() {
var self = this;
var storeLocationId = self.data.store && self.data.store.locationId;
if (storeLocationId) {
... removed for brevity ...
return products;
}
}
});
Related
I'm trying to publish a collection with 2 different names.
freeCourses contains courses without paid_url field.
premiumCourses contains all courses which id exist in userCourses collection.
userCourses collection :
{ user_id: "1", course_id: "1" }
Meteor.publish('freeCourses', function () {
this.added('freeCourses', Courses.find({}, {fields: {'Seasons.Episodes.paid_url': 0}}));
this.ready();
});
Meteor.publish('premiumCourses', function () {
//userPremiumCourses is array of course_ids
var userPremiumCourses = userCourses.find({'user_id': this.userId}, {fields: {course_id: 1, _id: 0}}).map(
function (doc) {
return doc.course_id;
}
);
this.added('premiumCourses', Courses.find({_id: {$in: userPremiumCourses}}));
this.ready();
});
if(Meteor.isClient){
Meteor.subscribe('freeCourses');
Meteor.subscribe('premiumCourses');
}
I want to get freeCourses and premiumCourses as two different collections on the client.
I've never seen this done before but if it was possible I believe you would need to define two collections that referred to the same underlying mongo collection:
freeCourses = new Mongo.collection('userCourses');
premiumCourses = new Mongo.collection('userCourses');
I just tested that and that fails.
A collection can have multiple publications each with its own query parameters and fields but it appears you want something more like a SQL view. That doesn't exist in Meteor afaik.
so I used publishVirtual function. thanks to #michel floyd
function publishVirtual(sub, name, cursor) {
var observer = cursor.observeChanges({
added : function(id, fields) { sub.added(name, id, fields) },
changed: function(id, fields) { sub.changed(name, id, fields) },
removed: function(id) { sub.remove(name, id) }
})
sub.onStop(function() {
observer.stop() // important. Otherwise, it keeps running forever
})
}
and added this into publish :
Meteor.publish('freeCourses', function () {
var cursor = Courses.find({}, {fields: {'Seasons.Episodes.paid_url': 0}});
publishVirtual(this, 'freeCourses', cursor);
this.ready();
});
Meteor.publish('premiumCourses', function () {
//userPremiumCourses contains array of course_ids
var userPremiumCourses = userCourses.find({'user_id': this.userId}, {fields: {course_id: 1, _id: 0}}).map(
function (doc) {
return doc.course_id;
}
);
var cursor = Courses.find({_id: {$in: userPremiumCourses}});
publishVirtual(this, 'premiumCourses', cursor);
this.ready();
});
and made two client-side collections for subscribe :
if (Meteor.isClient) {
freeCourses = new Mongo.Collection("freeCourses");
premiumCourses= new Mongo.Collection("premiumCourses");
Meteor.subscribe('freeCourses');
Meteor.subscribe('premiumCourses');
}
I wish to use Meteor to subscribe a few remote publication via DDP. Then show the documents in one template. Here is what I did:
Posts = {};
var lists = [
{server: "localhost:4000"},
{server: "localhost:5000"}
];
var startup = function () {
_.each(lists, function (list) {
var connection = DDP.connect(`http://${list.server}`);
Posts[`${list.server}`] = new Mongo.Collection('posts', {connection: connection});
connection.subscribe("allPosts");
});
}
startup();
This file is at client folder. Every startup, in this example, at browser I have two client collections Posts["localhost:4000"] and Posts["localhost:5000"], both are same schema. I know this format (Collection[server]) is ugly, please tell me if there is a better way.
Is there a way to show these client collections in the same template with reactive. Like this:
Template.registerHelper("posts", function () {
return Posts.find({}, {sort: {createdAt: -1}});
});
I think Connected Client is a big part of the Meteor. There should be a best practice to solve this problem, right?
Solved.
Connect to multiple servers via DDP, then observe their collections reactive via cursor.observeChanges.
Posts = {};
PostsHandle = {};
// LocalPosts is a local collection lived at browser.
LocalPosts = new Mongo.Collection(null); // null means local
// userId is generated by another Meteor app.
var lists = [
{server: "localhost:4000", userId: [
"hocm8Cd3SjztwtiBr",
"492WZqeqCxrDqfG5u"
]},
{server: "localhost:5000", userId: [
"X3oicwXho45xzmyc6",
"iZY4CdELFN9eQv5sa"
]}
];
var connect = function () {
_.each(lists, function (list) {
console.log("connect:", list.server, list.userId);
var connection = DDP.connect(`http://${list.server}`);
Posts[`${list.server}`] = new Mongo.Collection('posts', {connection: connection}); // 'posts' should be same with remote collection name.
PostsHandle[`${list.server}`] = connection.subscribe("posts", list.userId);
});
};
var observe = function () {
_.each(PostsHandle, function (handle, server) {
Tracker.autorun(function () {
if (handle.ready()) {
console.log(server, handle.ready());
// learn from http://docs.meteor.com/#/full/observe_changes
// thank you cursor.observeChanges
var cursor = Posts[server].find();
var cursorHandle = cursor.observeChanges({
added: function (id, post) {
console.log("added:", id, post);
piece._id = id; // sync post's _id
LocalPosts.insert(post);
},
removed: function (id) {
console.log("removed:", id);
LocalPosts.remove(id);
}
});
}
})
});
}
Template.posts.onCreated(function () {
connect(); // template level subscriptions
});
Template.posts.helpers({
posts: function () {
observe();
return LocalPosts.find({}, {sort: {createdAt: -1}}); // sort reactive
}
});
Session.set('coursesReady', false); on startup.
UPDATE:
I made it into a simpler problem. Consider the following code.
Inside router.js
Router.route('/', function () {
Meteor.subscribe("courses", function() {
console.log("data ready")
Session.set("coursesReady", true);
});
}
and inside main template Main.js
Template.Main.rendered = function() {
if (Session.get('coursesReady')) {
console.log("inject success");
Meteor.typeahead.inject();
}
The message "inject success" is not printed after "data ready" is printed. How come reactivity does not work here?
Reactivity "didn't work" because rendered only executes once (it isn't reactive). You'd need to wrap your session checks inside of a template autorun in order for them to get reevaluated:
Template.Main.rendered = function() {
this.autorun(function() {
if (Session.get('coursesReady')) {
console.log("inject success");
Meteor.typeahead.inject();
}
});
};
Probably a better solution is to wait on the subscription if you want to ensure your data is loaded prior to rendering the template.
Router.route('/', {
// 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('courses');
},
action: function () {
this.render('Main');
}
});
And now your rendered can just do this:
Template.Main.rendered = function() {
Meteor.typeahead.inject();
};
Don't forget to add a loading template.
To Solve Your Problem
Template.registerHelper("course_data", function() {
console.log("course_data helper is called");
if (Session.get('coursesReady')) {
var courses = Courses.find().fetch();
var result = [ { **Changed**
name: 'course-info1',
valueKey: 'titleLong',
local: function() {
return Courses.find().fetch();
},
template: 'Course'
}];
Session.set('courseResult', result); **New line**
return Session.get('courseResult'); **New line**
,
Explanation
The answer is at the return of the helper function needs to have be associated with reactivity in order for Blaze, template renderer, to know when to rerender.
Non-reactive (Doesn't change in the DOM as values changes)
Template.Main.helpers({
course_data: UI._globalHelpers.course_data ** Not reactive
});
Essentially: UI._globalHelpers.course_data returns an array of objects which is not reactive:
return [
{
name: 'course-info1',
valueKey: 'titleLong',
local: function() {
return Courses.find().fetch();
},
template: 'Course'
},
Reactive
From Meteor Documentation:
http://docs.meteor.com/#/full/template_helpers
Template.myTemplate.helpers({
foo: function () {
return Session.get("foo"); ** Reactive
}
});
Returning Session.get function to Blaze is reactive; thus, the template will change as the values changes.
I have a route I call many times. I have to subscribe two collections for having all datas, here's a snapshot:
var one = new Blaze.ReactiveVar(false);
var two = new Blaze.ReactiveVar(false);
this.route('stopIndex', {
path: '/stop/:section/:stop_id',
waitOn: function() {
Meteor.call('getTripIdsForStop', {
stop_id: this.params.stop_id,
from: fromNow(),
to: toMax(),
db: prefix
}, function(err, ids) {
DEBUG && console.log('TRIP_IDS:', ids);
Meteor.subscribe(prefix + '_trips', {
trip_id: {$in: ids}
}, function() {
one.set(true);
});
Meteor.subscribe(prefix + '_stop_times', {
trip_id: {$in: ids}
}, function() {
two.set(true);
});
});
return [
function () { return one.get(); },
function () { return two.get(); }
];
},
The first time I call the route, all goes fine. The second time, the one and two vars are already setted to true so the waitOn doesn't wait and I get a no data message on my template for some seconds, until collections responds. I've tried putting on the first lines of waitOk method:
one.set(false);
two.set(false);
but this makes the waitOn to wait forever. Am I doing something wrong or missing something? Thanks for the help.
I've solved this way:
Router.onStop(function() {
one.set(false);
two.set(false);
});
that invalidates ReactiveVars and will wait. I've also moved all code from waitOn to data. Now the waitOn is like this:
return [
function () { return one.get(); },
function () { return two.get(); }
];
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.