I'm having trouble understanding exactly what is available as my context in a template invoked by Meteor with Iron-Router – and how these things inherit.
Here are all the potential sources for "stuff I can refer to inside double curly braces" that I can think of:
Built-in helpers
Handlebars.registerHelper(...)
Template.myTemplate.myVar/myHelper = ...
Template.myTemplate.helpers({ ... })
data: { ... } inside route (Router.map)
Something to do with #each?
Something to do with #with?
Have I forgotten anything? Are there any global variables?
I guess I'm a bit confused about what the standard way is of giving the template a context in the first place. Also about what happens inside control structures such as #each and #with.
A clarification would be great!
IronRouter renders your template with the result of RouteController.data as the current data context.
<template name="viewPost">
<div>
<h1>{{title}}</h1>
<p>{{content}}</p>
</div>
</template>
var PostsController=RouteController.extend({
template:"viewPost",
waitOn:function(){
return Meteor.subscribe("postsById",this.params._id);
},
data:function(){
return Posts.findOne(this.params._id);
}
});
this.route("viewPost",{
path:"/posts/:_id",
controller:PostsController
});
In this example, IronRouter will render the "viewPost" template with the post having this.params._id as data context.
What is the standard way of giving a template a context in the first place ?
There is 2 ways :
{{#with myContext}}
{{> myTemplate}}
{{/with}}
{{> myTemplate myContext}}
As you can see, the #with control structure sets the current data context.
The #each structure iterates over a Cursor (or an Array) and sets the current data context to the current fetched document (or the current cell).
<template name="postsList">
{{#each posts}}
<h1>{{title}}</h1>
{{/each}}
</template>
Template.postsList.helpers({
posts:function(){
// cursor
return Posts.find();
// array
return [{title:"Title 1"},{title:"Title 2"},{title:"Title 3"}];
}
});
UPDATE : Could you possibly add a note about inheritance ? For instance, if I have nested #each blocks, do variables cascade ?
I came up with this example :
<template name="parent">
<ul>
{{#each parentContexts}}
{{> child}}
{{/each}}
</ul>
</template>
<template name="child">
<li>
<ul>
{{#each childContexts}}
{{> content}}
<p>../this.parentProperty = {{../this.parentProperty}}</p>
{{/each}}
</ul>
</li>
</template>
<template name="content">
<li>
<p>this.childProperty = {{this.childProperty}}</p>
<p>this.parentProperty = {{this.parentProperty}}</p>
</li>
</template>
Template.parent.helpers({
parentContexts:function(){
return [{
parentProperty:"parent 1"
},{
parentProperty:"parent 2"
}];
}
});
Template.child.helpers({
childContexts:function(){
return [{
childProperty:"child 1"
},{
childProperty:"child 2"
}];
}
});
If you run this example, you'll notice that you can't access the parentProperty in "content" because the default #each helper OVERRIDES the parent data context with the new context provided.
You can access the parentProperty in the nested #each block using this syntax : ../this.parentProperty, which is reminiscent of the UNIX parent directory access syntax.
However you cannot use this syntax in the "content" template, because it is agnostic of the nested each structures it was called from : you can only use the ../../parent syntax in a template where the actual nesting happens.
If we want to access the parentPropery in the content template, we must augment the current data context with the parent context.
To do so, we can register a new #eachWithParent helper like this :
Handlebars.registerHelper("eachWithParent",function(context,options){
var parentContext=this;
var contents="";
_.each(context,function(item){
var augmentedContext=_.extend(item,parentContext);
contents+=options.fn(augmentedContext);
});
return contents;
});
Now if you replace the nested #each with this new helper, you will have access to parentProperty in "content".
Related
I have a parent template with several nested child templates inside it. And inside each nested template I have a string generated from an array. A simplified model of my html:
<template name="parent">
<a class="logId">Console log parent Id</a>
{{> firstChild}}
{{> secondChild}}
</template>
In addition my child templates have an each loop like this:
<template name="firstChild">
{{#each users}}
{{this}}
{{> adminSelectUser}}
{{/each}}
</template>
<template name="adminSelectUser">
<a class="selectUser">SelectUser</a>
</template>
In my js code:
Template.parent.events({
'click .logId': function () {
var eventId = this._id._str;
console.log(eventId);
});
Template.adminSelectUser.events({
'click .selectUser': function(template){
var userString = this.valueOf();
console.log(userString);
}
});
To this point I get it to work and in both of my templates I can find the id of my parent and the string of my user as I want it to do. But then I would also want to use both my variables eventId and userString in the same method call, called from the template adminSelectUser. Is there any way that I can pass the eventId found in the parent template down to the adminSelectUser template so that I can work with both variables with the same method call.
This is what I aim to do with my server methods:
Meteor.methods({
removeUser: function(){
Meets.update({_id: eventId}, {$pull : {usersApplied: userString}});
}
});
I want my server method to recognize the eventId and the userString and get them to work together when I call the method later on in the adminSelectUser template.
Hope this is clear enough!
There are several ways to go about this.
Using Template.parentData
You can traverse the template hierarchy from within template JS using Template.parentData(n) where n is the number of levels up in the hierarchy you want to go. So specifically for your example:
Template.adminSelectUser.events({
'click .selectUser': function(template){
var user = this;
var parentId = Template.parentData(2)._id;
myRemoveMethod(parentId, user);
}
});
However, this means that the inner template relies on always being rendered within the same hierarchy, which kinda has a bad smell. If the templates are tightly coupled and this is the case (which is likely) then using parentData is probably fine. However, if a template has data dependencies it does make sense to pass the appropriate data explicitly (see #2).
Pass the data into the templates explicitly.
<template name="firstChild">
{{#each users}}
{{this}}
{{> adminSelectUser user=this parent=..}}
{{/each}}
</template>
In your example, when you pass no argument to a template render, the data context is assumed to be this. Here instead we pass a user and a parent into the template (Note the use of .. here to reference this outside of the users loop). Your handler would then look more like:
Template.adminSelectUser.events({
'click .selectUser': function() {
var data = Template.currentData();
myRemoveMethod(data.parent._id, data.user);
}
});
I am trying to access the data context of my Template from an event handlers, but it is null.
Here is my Template:
<template name="calendar">
<div class="calendar">
{{#with calendar}}
<h1>{{name}}</h1>
{{#each days}}
<div class="calendar-day">
{{this.date.getDate}}
<!-- I want to access the data context when this div is pressed -->
</div>
{{/each}}
{{/with}}
</div>
</template>
Here is where I want to get the data context.
Template.calendar.events({
'click .calendar-day': function(e, template) {
console.log(Template.currentData()); // null
console.log(Template.parentData()); // null
}
});
Update: I am able to access the current data context through this, but I also want to access the data context of the parent, namely, the data context of calendar
OK. Let me make it clear;
Template.calendar.events({
'click .calendar-day': function(e, template) {
//FYI: template === Template.instance()
//will give you your data context
console.log(this);
//will give you your data context
console.log(template.data);
//will give you your data context
console.log(Template.currentData());
//will give you your data context also
console.log(Template.parentData(0));
//will give you your parent template data context. Equivalent of Template.parentData()
console.log(Template.parentData(1));
}
});
If it turns out that your parentData is null then IT IS null, double check it.
you can get the context from this.
Template.calendar.events({
'click .calendar-day': function(e, template) {
console.log(this);
}
});
It's possible there is a bug in Blaze - github has 3 or 4 open issues which relate to this, so I haven't raised a specific issue myself.
The issue is with having multiple data contexts within a single template; this fix is to split the contexts by splitting the template:
<template name="calendar">
<div class="calendar">
{{#with calendar}}
<h1>{{name}}</h1>
{{#each days}}
{{> calendarDay}}
{{/each}}
{{/with}}
</div>
</template>
<template name="calendarDay">
<div class="calendar-day">
{{this.date.getDate}}
<!-- I want to access the data context when this div is pressed -->
</div>
</template>
And then just move your event to the new template:
Template.calendarDay.events({
'click .calendar-day': function(e, template) {
console.log(Template.currentData()); // no longer null!
console.log(Template.parentData()); // no longer null!
}
});
Within this event, you can now get to the following data contexts:
console.log(this); // the {{#each days}} context
console.log(template.data); // the {{#each days}} context
console.log(Template.currentData()); // the {{#each days}} context
console.log(Template.parentData(0)); // the {{#each days}} context
console.log(Template.parentData(1)); // the {{#with calendar}} context
By using this pattern as described I've managed to solve an identical problem to the OP, but only after 2-3 hours of investigation!
I'm developping an hybrid App based on Meteor with Meteoric (http://meteoric.github.io/). But I think the problem doubt I'm having is more general.
I have a template called AppLayout (AppLayout.html/AppLayout.js) used as a layout for the whole App. Basically it has the tabbar on it:
<template name="appLayout">
{{#ionBody}}
{{> ionNavBar }}
{{#ionNavView}}
{{> yield}}
{{/ionNavView}}
{{#ionTabs style="ios"}}
{{> ionTab title="Home" path="pictures" iconOff="ios-home-outline" iconOn="ios-home"}}
{{> ionTab title="Notificaciones" path=notificationsId iconOff="android-notifications-none" iconOn="android-notifications"}}
{{> ionTab title="Locales" path="places" iconOff="ios-world-outline" iconOn="ios-world"}}
{{> ionTab title="Mi Actividad" path="activity" iconOff="ios-list-outline" iconOn="ios-list"}}
{{> ionTab title="Perfil" path="profile" iconOff="ios-person-outline" iconOn="ios-person"}}
{{/ionTabs}}
{{/ionBody}}
</template>
My problem is that I need one of the ionTab icons to have a path that it's not constant.
{{> ionTab title="Notificaciones" path=notificationsId iconOff="android-notifications-none" iconOn="android-notifications"}}
To do so, I have the variable "notificationsId" declared in the helpers section of the Template:
Template.appLayout.helpers({
notificationsId : function() {
var id = "0";
if(Meteor.user()){
id = Meteor.user()._id;
}
return "notifications/"+id;
}
});
But it doesn't work and I can't find a way to pass a variable to the ionTab helper.
This example seems to match the pattern you're using, namely a helper that returns a variable which is then used as a named parameter for a template.
Meteor newbie here.
I have a template for a home page. The home pages has several "day"s in it, which each have "task"s. I want to display the appropriate task in the day that it belongs to, but I don't know how to do this.
I also only want to retrieve the relevant tasks with one database query if possible (ie all tasks within two weeks).
I found a couple other questions possibly related to this including this and this but I can't discern any useful related info.
I have a collection for tasks, and as part of the home page I retrieve a two week span of tasks. Then I sort them into day buckets.
buckets = [[...], [...], [...], ... ] # array of arrays of task objects
Now I don't know what to do. In the home template, I think I can do
Template.home.helpers(
tasks: ()->
#return buckets, defined above
)
(home.coffee)
<template name="home">
{{#each tasks}}
{{> day}}
{{/each}}
</template>
(home.html)
to iterate over the day buckets, but how do I access the task objects from each day template?
<template name="day">
{{#each ???}}
{{> task}}
{{/each}}
</template>
<template name="task">
{{name}}
</template>
How can I access the data from the current iteration of the each loop from the parent template? Am I structuring this incorrectly? Should I just make separate db calls for every day?
This should do the trick:
<template name="day">
{{#each this}}
{{> task}}
{{/each}}
</template>
Edit: this is the incorrect original answer.
Let task have fields called name, important and dueToday. Then you may write:
<template name="day">
{{name}}
</template>
Or, if you insist:
<template name="day">
{{this.name}}
</template>
Also:
Template.day.isItUrgent = function() {
return this.data.important && this.data.dueToday;
};
I have a situation in a template where I want to use an if block on a value in the parent context while inside an each block.
The code:
App = Ember.Application.create({});
App.view = Ember.View.extend({
foo: [1, 2, 3],
bar: true
});
The template:
<script type="text/x-handlebars">
{{#view App.view}}
{{#each foo}}
{{#if bar}}
{{this}}
{{/if}}
{{/each}}
{{/view}}
</script>
This does not work because names referenced inside an each loop are scoped to the element of iteration. How do you refer to things in the parent context?
Demo: http://jsfiddle.net/hekevintran/sMeyC/1/
I found a better solution.
From the Ember.js View Layer guide (http://emberjs.com/guides/understanding-ember/the-view-layer/):
Handlebars helpers in Ember may also specify variables. For example, the {{#with controller.person as tom}} form specifies a tom variable that descendent scopes can access. Even if a child context has a tom property, the tom variable will supersede it.
This form has one major benefit: it allows you to shorten long paths without losing access to the parent scope.
It is especially important in the {{#each}} helper, which provides a {{#each person in people}} form. In this form, descendent context have access to the person variable, but remain in the same scope as where the template invoked the each.
The template:
<script type="text/x-handlebars" >
{{#view App.view}}
{{#each number in view.foo}}
{{#if view.bar}}
{{number}}
{{/if}}
{{/each}}
{{/view}}
</script>
Demo: http://jsfiddle.net/hekevintran/hpcJv/1/
What hekevintran's answer means is that you can rename any variable using #with. We have a similar problem in JavaScript with this. In JavaScript, sometimes you'll see code like this to work around it.
var self = this;
doSomething(function() {
// Here, `this` has changed.
if (self.bar) {
console.log(this);
}
});
In Ember flavored Handlebars, something similar is happening with view. Say you have App.MyOuterView and another view inside it. You can work around it like this.
{{#with view as myOuterView}}
{{#each foo}}
{{#if myOuterView.bar}}
{{this}}
{{/if}}
{{/each}}
{{/with}}
Similar to the JavaScript, you can essentially rename view to something else so it doesn't get shadowed by the inner view. {{#each person in people}} is just a special case of that. But renaming using {{#with view as myView}} is the more general solution/workaround to this problem that also works with nested calls to the view helper.
I was also stumped on this. This thread and this other thread (Using a container view in ember.js - how to access parent variables from child view) helped me with the solution. I used Jonathan's suggestion to do {#with} and also figured out that I should access my variable by calling the controller. Mine worked like this:
// I use the #which command to preserve access to the outer context once inside the #each
{{#with view as myOuterView}}
{{#each myInnerArray}}
//here, i get my 'name' property from the *controller* of myOuterView
{{myOuterView.controller.name}}
// stuff i did in inner array
{{/each}
{{/with}
No need to place the if inside each in the first place:
<script type="text/x-handlebars">
{{#view App.view}}
{{#if view.bar}}
{{#each view.foo}}
{{this}}
{{/each}}
{{/if}}
{{/view}}
</script>
Demo: http://jsfiddle.net/ppanagi/NQKvy/35/