A Guide to Meteor Templates & Data Contexts
I recently had the chance to speak at Rebuild, a design & development conference in Indianapolis. My talk was a quick introduction to Meteor, in the form of a step-by-step walk-through that takes you through building a simple Meteor app from start to finish.
But as I was preparing for the talk and going over the slides, I kept stumbling over one section. I just couldn’t find a straightforward way to explain data contexts.
So I thought it would be worth it to revisit the topic for the blog, and take a better look at one of the key building blocks of Meteor apps.
A Visual Explanation
So what is a data context exactly? To understand this, let’s begin with a piece of plain old boring HTML enclosed inside a Meteor template
tag:
Now in most situations, you’ll probably want this data to be dynamic. So we’ll replace these static strings by dynamic placeholder tags:
Another way to look at it is imagining those tags are “holes” in the template, waiting to be filled in with something:
As you may have guessed, that “something” is the data context. When we add in that missing part, we end up with our rendered template displayed in the user’s browser:
Moreover, since our template is dynamic, we can easily apply it to a different data context:
And finally, there’s nothing preventing us from applying a different template to the same data context:
#with
Now that we have the basics straight, we need to talk about the main difficulty when dealing with data contexts: most of the time, we set them and use them implicitly. While that can be a great time-saver, it can also be the source of a lot of confusion and ambiguity.
The easiest way to set a data context is with the {{#with}}
template tag. The tag takes an argument, and that argument will become the data context of the block enclosed within the tag. For example, in the following example the template will use the profile
object as its data context:
<template name="profile">
{{#with profile}}
<img src="{{avatar}}"/>
<p>{{name}}</p>
{{/with}}
</template>
{{#with}}
is fairly flexible. You can apply it to just a fragment of a template, and even nest it. For example, assuming our profile
object looks something like this:
var profile = {
avatar: 'avatar.jpg',
name: {
first: 'John',
last: 'Smith'
}
}
We could very well write:
<template name="profile">
<h2>Profile</h2>
{{#with profile}}
<img src="{{avatar}}"/>
{{#with name}}
<p>{{first}} {{last}}</p>
{{/with}}
{{/with}}
</template>
So far so good. But {{#with}}
is far from the only way of setting data contexts.
#each
The {{#each}}
helper tag is another common way of setting a data context. Like {{#with}}
, {{#each}}
takes an argument (usually a cursor, i.e. the result of a Collection.find()
call).
The tag will then loop over that argument, and set the data context for each iteration of the loop.
<template name="profiles">
<ul>
{{#each profiles}}
<li>
<img src="{{avatar}}"/>
<p>{{name}}</p>
</li>
{{/each}}
</ul>
</template>
So in this case, the data context is set implicitly not to profiles
, but to each elements contained within profiles
successively.
Template Includes
We’ve just seen the two most common ways of setting data contexts. But did you know you could also set data contexts using template include tags?
Let’s imagine we have a post
template, and that we want to display the post’s author’s profile. We’ll set the profile
template’s data context to author
as we include it from within that post
template:
<template name="post">
{{#with post}}
<h1>{{title}}</h1>
<p>{{content}}</p>
<div>{{> profile author}}</div>
{{/with}}
</template>
Note that this won’t work if you’re already setting a data context inside the profile
template (such as with a {{#with}}
tag, for example).
Iron Router
Iron Router is the main routing package for Meteor, and it includes powerful features for setting and manipulating data contexts through its data
function:
this.route('profile', {
path: '/profile/:_id',
template: 'profile',
data: function() { return Users.findOne(this.params._id); }
});
Here, we’re defining a route that will bring up the profile
template while setting its data context to Users.findOne(this.params._id)
.
Setting the data context in the router means you don’t need to do it in the template. Which in turns makes it much easier to reuse your template for different data contexts.
Template Helpers
So far we’ve only talked about data contexts in the, um, context of HTML templates. But data contexts are also very important when dealing with their JavaScript counterpart, template helpers.
In a template helper, the data context is accessible through the this
keyword. It’s also important to note that a template helper will inherit its data context from the template itself.
In other words, depending on where you call a template helper, it might end up with a completely different data context!
Let’s go back to our profile
example:
<template name="profile">
<h2>Profile</h2>
{{#with profile}}
<img src="{{avatarPath}}"/>
{{#with name}}
<p>{{fullName}}</p>
{{/with}}
{{/with}}
</template>
Here’s the corresponding JavaScript code:
Template.profile.helpers({
profile: function () {
return Users.findOne(Session.get('someUserId'));
},
avatarPath: function () { // data context set to profile
return "images/" + this.avatar;
},
fullName: function () { // data context set to profile.name
return this.first + " " + this.last;
}
});
Because the template’s {{#with}}
tag sets the data context to the name
property of the profile
object, the avatarPath
and fullName
helpers will end up having different data contexts, even though they belong to the same template.
Debugging Data Contexts
When it comes to debugging data context problems, your best friend is still the humble console.log
. Just add a console.log(this)
to your helper to know what context it’s using:
Template.profile.helpers({
fullName: function () { // data context set to profile.name
console.log(this);
return this.first + " " + this.last;
}
});
Alternatively, if you need to find out more about the data context of a specific template fragment, you can also write a dedicated {{log}}
helper:
Template.profile.helpers({
log: function () {
console.log(this);
}
});
And use it directly in your templates:
<template name="profile">
{{#with profile}}
{{log}}
<img src="{{avatar}}"/>
<p>{{name}}</p>
{{/with}}
</template>
Update: Parent Data Context
In the comments, Rune Jeppesen asked if there was any way to access the parent data context. I wasn’t sure, so I asked Percolate’s Zoltan Olah, and he gave me this clever way to do it.
It turns out that since the 0.8 Blaze update, you can access the parent context with the ..
keyword from within a template. So we’ll just pass this keyword as an argument to a template helper:
<template name="profile">
<h2>Profile</h2>
{{#with profile}}
<img src="{{avatarPath}}"/>
{{#with name}}
{{parentHelper ..}}
<p>{{fullName}}</p>
{{/with}}
{{/with}}
</template>
And access it from within our JavaScript code:
Template.profile.helpers({
parentHelper: function (parentContext) {
console.log(this); // profile.name data context
console.log(parentContext); // profile data context
}
});
And by the way, you can access the grandparent data context the same way using the ../..
keyword.
Wrapping Up
Meteor is a fairly free-form framework, and it’s ultimately up to you to decide how to architecture your app. Still, I think it’s generally good practice to try and make your templates as “data-context-free” as possible for maximum flexibility. In other words, set the data context outside of the template, either through Iron Router or include tags, rather than inside the template with the {{#with}}
tag.
Data contexts are a simple concept once you get the hang of them, but the fact that they’re so often declared implicitly can make for a tough learning curve. Hopefully, this article will help you get a better understanding of their subtleties.
Comments
comments powered by Disqus