Meteor, publish:composite. how to access joined data in the template? - meteor

So I used publishComposite to do a collection join in Meteor. I have a parent collection (Subscriptions) with a user_id foreign key. I look up the user name in the Meteor.users collection to get the actual username, but how do I actually print this in the html template. My subscription data is there but how do I actually refer to the username?
Here is the publish code:
//publish subscriptions course view
Meteor.publishComposite('adminCourseSubscriptions', function(courseId){
return {
//get the subs for the selected course
find: function(){
return Subscriptions.find(
{course_id: courseId}
);
},
children:
[
{
//get the subscriber details for the course
find: function(sub){
return Meteor.users.find({_id:sub.user_id});
}
}
]
};
});
here are the template subdcriptions:
Template.adminCourseDetail.helpers({
courseDetail: function(id){
var id = FlowRouter.getParam('id');
return Courses.findOne({ _id: id });
},
courseSubscriptions: function(){
var id = FlowRouter.getParam('id');
return Subscriptions.find({course_id:id})
},
users: function(){
return Meteor.users.find();
}
});
and the template (which is garbage) ps the course details come from a separate collection. It was easier and I think more performant to get the details separately and this works fine. It's just the username that I cannot display correctly:
<template name="adminCourseDetail">
<h1>Course Details</h1>
<p>Title: {{courseDetail.title}}</p>
<p>Description: {{courseDetail.description}}</p>
<p>Start Date: {{courseDetail.startDate}}</p>
<p>Number of sessions: {{courseDetail.sessions}}</p>
<p>Duration: {{courseDetail.duration}}</p>
<p>Price: {{courseDetail.price}}</p>
<p>{{userTest}}</p>
edit
delete
<h2>Course Subscriptions</h2>
{{#each courseSubscriptions}}
<div class="row">
<div class="col-md-3">{{username}}</div>
<div class="col-md-3">{{sub_date}}</div>
</div>
{{/each}}
</template>
Thanks in advance for any suggestions!

As far as I understand your question, documents of the Subscriptions collection contain only the attribute user_id, referencing the corresponding user document in the Meteor.users collection. If this is the case, then you need to add an additional template helper which returns the username:
Template.adminCourseDetail.helpers({
// ...
getUsername: function() {
if (this.user_id) {
let user = Meteor.users.find({
_id: this.user_id
});
return user && user.username;
}
return "Anonymous";
}
// ...
});
After that, just replace {{username}} with {{getUsername}}:
<template name="adminCourseDetail">
<!-- ... -->
<h2>Course Subscriptions</h2>
{{#each courseSubscriptions}}
<div class="row">
<div class="col-md-3">{{getUsername}}</div>
<div class="col-md-3">{{sub_date}}</div>
</div>
{{/each}}
<!-- ... -->
</template>
Probably you misunderstood the concept of the reywood:publish-composite package. Using Meteor.publishComposite(...) will just publish a reactive join, but it will not return a new set of joined data.

For anyone else having a similar issue and looking at my specfic example. In my case the following code worked. Based on Matthias' answer:
In the template helper:
getUsername: function() {
let user = Meteor.users.findOne({
_id: this.user_id
});
return user;
}
and then in the template:
{{getUsername.username}}
My each block is looping through the cursor returned from the subscriptions collection rather than the course collection which is why it is simpler than the code Matthias provided.

Related

How to pass parameter data between templates in meteor flowlayout flowrouter

I have a template called : Orders which shows my orders collection of images :
{{#each images}}
<div class="images">
<img class="image" src="{{this.url }}" />
</div>
{{/each}}
No I want another tempate called order to show me only one item from collection that I click on: I try doing this way: 1. orders.js events for click on image:
"click .image": function() {
Images.find({_id:this._id});
and orders.html:
<img class="image" src="{{this.url }}" />
I also have routes.js :
FlowRouter.route("/orders", { **this part works fine**
action: function(){
FlowLayout.render("layout",{top:"orders", main:"test"});
}
FlowRouter.route('/order/', { **How do I do this part ????????**
action: function(){
FlowLayout.render("layout",{top:"order",data:image});
}
I used a dynamic layout template to show orders which shows fine.
How do I set the single order html , route and render ????
To answer your question:
Beginning by your route, you should pass the id parameter in the path like that :
FlowRouter.route('/order/:imageId', {
action: function() {
FlowLayout.render("layout",{ top: "order", main: "test" });
}
});
Looking at the rendering on order.html file which contains the template order, something like:
<template name="order">
{{#with image}}
<!-- whatever you want to do -->
<img src="{{url}}" />
{{/with}}
</template>
This template uses order.js with a subscription to your collection for one image only and a helper called image which look for the parameter imageId you transmitted in your route via the function FlowRouter.getParam:
Template.order.onCreated(function() {
var imageId = FlowRouter.getParam('imagedId');
if ( imageId !== undefined ) {
Meteor.subscribe('oneImage', imageId);
}
}
Template.order.helpers({
image: function() {
var imageId = FlowRouter.getParam('imagedId');
if ( imageId !== undefined ) {
return Images.findOne({ _id: imageId });
}
}
}
And to conclude, server side, you shall do a publication:
Meteor.publish('oneImage', function(imageId) {
return Images.findOne({ _id: imageId });
}
Following this way, you don't need anymore your event click .image and you optimized your performance! ;)
Btw on orders.html in your {{#each}} loop, you don't need to write {{this.url}} nor {{this._id}}, {{url}} and {{_id}} are fine ;)
To retrieve request parameter you can use:
FlowRouter.current().route._params.keys.id

How to deal with the situation that template is rendered but the data is not ready?

In client startup I subscribe to something:
Meteor.publish("Roles", function(){
return Roles.find();
});
Meteor.startup(function() {
if(Meteor.isClient) {
Meteor.subscribe('Roles');
}
});
And roles template:
Template.roles.helper(function() {
allRoles: function() {
return Roles.find().fetch();
}
})
<template name="roles">
<div>
{{#with allRoles}}
<label>{{> role }}</label>
</div>
</template>
The problem is sometime roles template is rendered before the Roles is ready.
How to deal with this situation?
You can do the subscribe on the template and then use the Template.subscriptionReady helper to create a conditional to show a loading panel whilst your subscription is being loaded as follows:
Template.roles.onCreated(function () {
this.subscribe("Roles");
});
Template.roles.helper(function() {
allRoles: function() {
return Roles.find().fetch();
}
})
<template name="roles">
<div>
{{#if Template.subscriptionsReady}}
{{#with allRoles}}
<label>{{> role }}</label>
{{else}}
Loading...
{{/if}}
</div>
</template>
This replaces your other subscription and these subscriptions can be added to each onCreated method for each template to have subscriptions per template.
There are some common ways of dealing with it. You can use a guard or make use of iron router's waitOn function. With a guard you only return data from the helper if you're getting any results:
allRoles: function() {
var roles = Roles.find();
//explicit version
if (roles.count()) return roles
//implicitly works as well here because you're returning null when there are no results
return roles
}
You don't need the fetch() in this case, because #with works with a cursor. If you run into a situation where you need to fetch first because you're returning partial data, check that there are results first and only then return them.
You can also use iron router's waitOn Option if you're using this as part of a route.

Iron Router / User details issue in Meteor

I am relatively new to Meteor and have been stuck on an issue for awhile. I have a /users/:_id route that is supposed to display details specific to that user id. However, whenever I hit that route, it displays information for the currently logged in user, NOT of the user whose details I want to view.
Here's my route:
Router.route('/users/:_id', {name: 'Users', controller: 'usersDetailController'});
Here's my usersDetailController:
usersDetailController = RouteController.extend({
waitOn: function () {
Meteor.subscribe('userProfileExtended', this.params._id);
},
onBeforeAction: function () {
var currUserId = Meteor.userId();
var currUser = Meteor.users.findOne({_id: currUserId});
console.log('admin? ' + currUser.isAdmin);
if (!currUser.isAdmin) {
this.render('accessDenied');
} else {
this.next();
}
},
action: function() {
this.render('Users');
}
});
And here's my server/publish:
Meteor.publish('userProfileExtended', function() {
return Meteor.users.find({_id: this.userId});
});
User Details template:
<template name="Users">
<form>
{{#with user}}
<div class="panel panel-default">
<div class="panel-heading">{{profile.companyName}} Details</div>
<div class="row">
<div class="col-md-4">
<div class="panel-body">
<p><label>Company: </label><input id="Company" type="text" value={{profile.companyName}}></p>
<p><label>Email: </label><input id="Email" type="text" value={{emails.address}}></p>
<p><label>Phone: </label><input id="Phone" type="text" value={{profile.phoneNum}}></p>
<p><label>Tire Markup: </label><input id = "tireMarkup" type="text" value={{profile.tireMarkup}}></p>
<p><button class="saveUserDetails">Save</button></p>
<p><button class="deleteUser">Delete User</button></p>
</div>
</div>
</div>
</div>
{{/with}}
Here's my Template Helper:
Template.Users.helpers({
user: function() {
return Meteor.users.findOne();
}
});
Can someone help? I think the issue is the way i reference "this.userId"...
Thank you!!
You need to change your publish function to use the userId parameter you specify when subscribing :
Meteor.publish('userProfileExtended', function(userId) {
return Meteor.users.find(userId,{
fields:{
'username':1,
'profile.firstName':1,
'profile.lastName'
}
});
});
In the publish function, userId will equal whatever value you call Meteor.subscribe with, in this case it will hold this.params._id.
Beware of using the proper syntax for route parameters, if you declare a path of /users/:_id, you need to reference the param using this.params._id.
Also note that it's insecure to publish the whole user document to the client if you only need to show specific fields in the interface, that's why you want to use the fields option of Collection.find to only publish a subset of user documents.
EDIT :
I would recommend using the route data function to specify the data context you want to apply when rendering your template, something like this :
data: function(){
return {
user: Meteor.users.findOne(this.params._id)
};
}

How do I get templates inserted from custom block helpers to be individually rerendered in Meteor?

When I use the built-in block helper #each, book templates are rerendered individually when changed:
users =
_id: 'foo'
books: [
{name: 'book1'}
{name: 'book2'}
]
<template name="user">
{{#each books}}
{{> book}}
{{/each}}
</template>
<template name="book">
<div>{{name}}</div>
</template>
When the data is changed - the first book name is set to 'bookone' instead of 'book1' - only the book template (the div containing 'book1') is rerendered. This is the desired behavior. When I use a custom block helper, the behavior is different:
<template name="user">
{{#each_with_id}}
{{> book}}
{{/each}}
</template>
<template name="book">
<div data-id="{{_id}}">{{name}}</div>
</template>
Templates.user.each_with_id = (options) ->
html = "
for book, i in this.books
this.name = book.name
html += Spark.labelBranch i.toString(), ->
options.fn this
html
Now when the name of the first book changes, the whole user template is rerendered.
It does not work as you expect, because the implementation of built-in each is based on the cursor.observeChanges feature. You will not be able to achieve the same exact result without using an auxiliary collection of some sort. The idea is quite simple. It seems that you don't have a "books" collection but you can create a client-side-only cache:
Books = new Meteor.Collection(null);
where you will need to put some data dynamically like this:
Users.find({/* probably some filtering here */}).observeCanges({
added: function (id, fields) {
_.each(fields.books, function (book, index) {
Books.insert(_.extend(book, {
owner: id,
index: index,
}));
}
},
changed: function (id, fields) {
Books.remove({
owner:id, name:{
$nin:_.pluck(fields.books, 'name')
},
});
_.each(fields.books, function (book, index) {
Books.update({
owner : id,
name : book.name,
}, {$set:_.extend(book, {
owner : id,
index : index,
})}, {upsert:true});
}
},
removed: function (id) {
Books.remove({owner:id});
},
});
Then instead of each_with_id you will be able to the built-in each with appropriate cursor, e.g.
Books.find({owner:Session.get('currentUserId')}, {sort:{index:1}});
You may also look at this other topic which basically covers the same problem you're asking about.

Accessing parent context in Meteor templates and template helpers

I'm running into a template context situation that I'm having a hard time finding a way around.
Here's the template in question:
{{#each votes}}
<h3>{{question}}</h3>
<ul>
{{#each participants}}
<li>
<p>{{email}}</p>
<select name="option-select">
{{#each ../options}}
<option value="{{option}}" class="{{is_selected_option}}">{{option}}</option>
{{/each}}
</select>
</li>
{{/each}}
</ul>
</div>
{{/each}}
And here's an example of a vote document:
{
_id: '32093ufsdj90j234',
question: 'What is the best food of all time?'
options: [
'Pizza',
'Tacos',
'Salad',
'Thai'
],
participants: [
{
id: '2f537a74-3ce0-47b3-80fc-97a4189b2c15'
vote: 0
},
{
id: '8bffafa7-8736-4c4b-968e-82900b82c266'
vote: 1
}
]
}
And here's the issue...
When the template drops into the #each for participants, it no longer has access to the vote context, and therefore doesn't have access to the available options for each vote.
I can somewhat get around this by using the ../options handlebars path to jump back into the parent context, but this doesn't affect the context of the template helper, so this in Template.vote.is_selected_option refers to the current participant, not to the current vote or option, and has no way of knowing which option we are currently iterating through.
Any suggestions on how to get around this, without resorting to DOM manipulation and jQuery shenanigans?
This is a templating issue that has come up multiple times for me. We need a formal way of reaching up the template context hierarchy, in templates, template helpers, and template events.
It seems since Spacebars (Meteor's new template engine), you have access to the parent context within {{#each}} blocks using ../.
In Meteor 0.9.1, you can also write a helper and use Template.parentData() in its implementation.
It's not particularly pretty, but I've done something like this:
<template name='forLoop'>
{{#each augmentedParticipants}}
{{> participant }}
{{/each}}
</template>
<template name='participant'>
...
Question: {{this.parent.question}}
...
</template>
// and in the js:
Template.forLoop.helpers({
augmentedParticipants: function() {
var self = this;
return _.map(self.participants,function(p) {
p.parent = self;
return p;
});
}
});
It's similar to the approach that AVGP suggested, but augments the data at the helper level instead of the db level, which I think is a little lighter-weight.
If you get fancy, you could try to write a Handlebars block helper eachWithParent that would abstract this functionality. Meteor's extensions to handlebars are documented here: https://github.com/meteor/meteor/wiki/Handlebars
I don't know the formal way (if there is one), but to solve your issue, I would link the participants with the parent ID like this:
{
_id: "1234",
question: "Whats up?",
...
participants: [
{
_id: "abcd",
parent_id: "1234",
vote: 0
}
]
}
and use this parent_id in helpers, events, etc. to jump back to the parent using findOne.
That is obviously a sub optimal thing to do, but it's the easiest way that comes to my mind as long as there is no way of referencing the parent context.
Maybe there is a way but it is very well hidden in the inner workings of Meteor without mention in the docs, if so: Please update this question if you find one.
It's a long shot, but maybe this could work:
{{#with ../}}
{{#each options}}
{{this}}
{{/each}}
{{/with}}
This should make life easier.
// use #eachWithParent instead of #each and the parent._id will be passed into the context as parent.
Handlebars.registerHelper('eachWithParent', function(context, options) {
var self = this;
var contextWithParent = _.map(context,function(p) {
p.parent = self._id;
return p;
});
var ret = "";
for(var i=0, j=contextWithParent.length; i<j; i++) {
ret = ret + options.fn( contextWithParent[i] );
}
return ret;
});
Go ahead and change
p.parent = self._id;
to whatever you want to access in the parent context.
Fixed it:
// https://github.com/meteor/handlebars.js/blob/master/lib/handlebars/base.js
// use #eachWithParent instead of #each and the parent._id will be passed into the context as parent.
Handlebars.registerHelper('eachWithParent', function(context, options) {
var self = this;
var contextWithParent = _.map(context,function(p) {
p.parent = self._id;
return p;
});
return Handlebars._default_helpers.each(contextWithParent, options);
});
This works :) with no error
Simply register a global template helper:
Template.registerHelper('parentData',
function () {
return Template.parentData(1);
}
);
and use it in your HTML templates as:
{{#each someRecords}}
{{parentData.someValue}}
{{/each}}
======= EDIT
For Meteor 1.2+, you shold use:
UI.registerHelper('parentData', function() {
return Template.parentData(1);
});
I was stuck in a similar way and found that the Template.parentData() approach suggested in other answers currently doesn't work within event handlers (see https://github.com/meteor/meteor/issues/5491). User Lirbank posted this simple workaround:
Pass the data from the outer context to an html element in the inner context, in the same template:
{{#each companies}}
{{#each employees}}
Do something
{{/each}}
{{/each}}
Now the company ID can be accessed from the event handler with something like
$(event.currentTarget).attr('companyId')
"click .selected":function(e){
var parent_id = $(e.currentTarget).parent().attr("uid");
return parent_id
},
<td id="" class="staff_docs" uid="{{_id}}">
{{#each all_req_doc}}
<div class="icheckbox selected "></div>
{{/each}}
</td>
{{#each parent}}
{{#each child}}
<input type="hidden" name="child_id" value="{{_id}}" />
<input type="hidden" name="parent_id" value="{{../_id}}" />
{{/each}}
{{/each}}
The _id is NOT the _did of the thing, it's the id of the parent!

Resources