In this post I am proposing a better way to write your Meteor templates. The goal is to have more explicit variables and a scope similar to what you can see in a regular JavaScript code. We are going to talk about data contexts, dynamic variables and lexical scope in your templating language.
A short history lesson
Handlebars - a popular templating language. At the time a web developer would pick Handlebars to render their html pages. That was before the world of web development moved to cilent-heavy templating and most application servers became a dumb API layer for the database.
For a person like me, who came to web dev a couple of years ago, little is known
about the dark time of 2010, when the project was born. Various evidence shows
that people were debating about "logicless templates" and good
"presentation/logic" separation. At the time, Handlebars have borrowed the
syntax from Mustache templates and introduced more syntax: constructs like #if
and #each
, template helpers, compiled templates.
Update: It appears that Mustache got its inspiration from a Google project
called CTemplate
, a C++ templating libarary from 2006-2008 that is still
available on Google Code.
The success of curly braces ({{
, }}
) propagated to the client-side
templating. Multiple projects has adapted the syntax and structure while
bringing some "life" to the braced expressions.
Ember.js, Ractive and Meteor's Blaze (and its predecessor Spark) all use the Handlebars syntax to create "Live HTML templates" - templates changing the HTML page on the fly as the underlying data model changes. That's it - if your JavaScript value changes, you can expect the presentation constructed by your templates will change accordingly.
Handlebars at Meteor
A lot of efforts has been spent by the Meteor Core team to maintain a lot of the syntax features and behaviors of Handlebars while making it more reactive and "live".
Around the time of arrival of Blaze, Meteor's latest iteration of reactive templates, the syntax started to diverge. The Meteor fork of the syntax is called "Spacebars", similar to other front-end frameworks, Spacebars enforce structured templates for fine-grained updates, some of the features of Handlebars are dropped but most of the semantics remained the same.
Data context - the root of all confusion
One feature remained the same and that's the "data context". In Handlebars this
is the only way to pass data from template to template. "Data context" can be
accessed with troublesome this
keyword. It is inherited by default on
templates inclusions, so it sneaks into everything; It is silent and the only
argument, it is also implicit.
<template name="people">
{{!-- the data context here is { people: [...] } --}}
{{#each people}}
{{!-- the data context here is changed by #each and it is { name: ... } --}}
{{> person}}
{{/each}}
</template>
<template name="person">
{{!-- the data context here is expected to be an object --}}
{{!-- with fields 'name' and 'age' --}}
<span>{{name}}: {{age}}</span>
</template>
As always, the confusion happens when the data context changes. Constructs like
#each
and #with
change the data context and often do it in such a way, that
you can't access the parent data unless you reside to a weird dotted syntax.
{{#each people}}
{{#each favoriteFruits}}
{{!-- accessing the parent data with a weird ../ syntax --}}
{{!-- also using {{this}} to refer to the current data --}}
<span>The favorite fruit of {{../name}} is {{this}}</span>
{{/each}}
{{/each}}
The reason for such syntax is the concept of "paths" in Handlebars. So every
time you see a construct like {{person.bio.homeTown}}
, it is not a property
access - it is a path. This is why {{../name}}
construct makes sense if you
think of it as a path. In fact, an expression {{.}}
would be equivalent to
{{this}}
in Handlebars. Path is another confusing syntax feature that just
adds to the data context.
Data context is a dynamic variable
If you have a template "person"
and it displays a name and an age from the
current data context. What is the current data context? How do we know if there
are other fields on it that we can use in this template? It completely depends
on the template that includes the "person"
template.
In other words, data context is just a dynamic variable, something that depends on the chain of calls that led you to here, rather than the environment that existed when your template was defined (lexical variable).
Dynamic variables and dynamic scope are the programming language features from 60s and no other modern sane programming languages uses them as the main way to refer to variables. In fact, if you search for materials online explaining dynamic variables, most likely you will find something about Emacs Lisp or Bash or Perl (the last two keep it for backwards compatibility).
JavaScript has only one sort-of dynamic variable called this
. And guess what?
People are confused as hell by it. Thankfully, in JavaScript this
is not the
main way of passing arguments to a function. Unfortunately, in Handlebars it is
the only way.
Similar to Handlebars' {{#with}}
JavaScript the with
keyword. Guess
what? Every JS developer will tell you to never ever use it. It makes the code
confusing, it cripples the scope, it is a source of bugs, it is deprecated in
the strict mode.
Handlebars without data context
How would we get rid of the data context? Recently, I have been working on a Pull Request to Meteor's Blaze to introduce a concept of lexical scope into the templating language: PR #3560 at meteor/meteor.
#let
First, let's introduce the notion of scope and tools to manipulate it. I thought
that the let
keyword would be appropriate here. It would be familiar to people
who have seen the let
form in Scheme and the let .. in ..
construct in ML.
{{#let city=person.bio.homeTown name=person.name}}
<div>
{{!-- access newly introduced variables city and name in this let block --}}
{{name}} is from {{city}}!
{{!-- still can access person from the scope above --}}
Get to know {{person.name}}.
</div>
{{/let}}
Since templates are not a full-fledged programming language and their use is
usually simpler and limited, it makes sense to make these variables immutable.
That's it. They can be overshadowed by a different {{#let}}
but cannot be
changed. Their application is limited to their block and they don't leak to
other templates included within their block.
New #each
Now let's fix the most commonly used construct that relies on the dynamic data
context: {{#each}}
. The new {{#each}}
plays the game of lexical scoping and
introduces a new variable within its body representing the iteration variable:
{{#each person in people}}
<div>{{person.name}} is from {{person.bio.homeTown}}.</div>
{{/each}}
person
as a variable makes a lot more sense to the reader than an unnamed
this
. This is very similar to what you would do in JavaScript:
people.forEach(function (person) {
// new scope, person is introduced
});
Including templates with their scope as arguments
And lastly, let's make the template inclusion more descriptive to the reader:
<template name="person" args="name age city">
<div>
Everyone, meet {{name}}.
{{name}} is {{age}} years old and is coming from {{city}}.
</div>
</template>
Now the inclusion would look like this:
{{> person name=jack.name age=jack.age city=jack.bio.homeTown}}
The syntax is a lot more verbose now but what we have here is just a set of named arguments. Included template gets its own scope set to the passed arguments, similar to a function in JavaScript that gets its own scope set to its positional arguments:
function (name, age, city) {
return "Everyone, meet " + name + ".\n" +
name + " is " + age + " years old and is coming from " + city + ".";
}
This already works in Blaze without any changes. The trick is, the same syntax is used to set a custom data context. And if this is the only place where we use data context, it can as well treated as a template-local scope.
Arguments declared in the template open tag are optional but I think it makes it very clear to anyone who is going to use this template in the future, what are the expected arguments.
Try it out!
We are done! Following this pattern, I believe, your templates can become easier to follow, less confusing to read and more explicit. The data context is a legacy from Handlebars and I wish it would remain there.
You can try using these features and this style as soon as my PR lands to a release. You can also pull it from GitHub and play with it running Meteor from a checkout.