A future version of Ember will come with a new templating engine known as HTMLBars.
The original motivation for HTMLBars was to allow helpers and properties to have better contextual information about what they were bound to.
So for example, consider a template like this:
In the current version of Ember's Handlebars, this can't work, because we need to emit a string placeholder into the DOM in order to update a bound value later. The above template would compile down to something like this:
var output = "";
output.push("<a href=\"");
output.push("<script type='text/x-placeholder' id='start-1'></script>");
// insert the value of url if known
output.push("<script type='text/x-placeholder' id='end-1'></script>");
output.push("\">");
output.push("<script type='text/x-placeholder' id='start-2'></script>");
// insert the value of link if known
output.push("<script type='text/x-placeholder' id='end-2'></script>");
output.push("</a>");
Inside of content, we can do document.getElementById('start-1')
to update the value when it changes, but that of course won't work inside of an attribute. Because the compiler has no HTML context, it can't do anything smarter.
HTMLBars solves that problem by using a mixed HTML/Handlebars parser and compiler. Because the compiler understands HTML, it can do more contextually-aware tricks for data binding.
Instead of the above output, the HTMLBars compiler emits something like this (dom
below is just an abstraction that can be used for server-side rendering and other uses).:
var output = dom.createDocumentFragment();
var a = dom.createElement('a');
dom.RESOLVE_ATTR(context, a, 'href', 'url');
var text = dom.createTextNode();
dom.RESOLVE(context, text, 'textContent', 'link');
a.appendChild(text);
output.appendChild(a);
A couple of things:
- The above code is only roughly equivalent to what the actual compiled code will look like, for expository purposes
- The
RESOLVE
andRESOLVE_ATTR
helpers are actually low-level APIs that are used to build higher-level APIs more like NodeBind. - I didn't show it above, but if necessary, we can use empty text nodes to wrap groups of DOM (like those produced by
{{{unescaped}}}
). In most cases, though, we can just bind directly to a single text node.
When working on building up the higher level abstraction, I spent some time thinking about the philosophy of Handlebars and data bindings a bit more.
For some background, the reason I built Handlebars in the first place wasn't an attempt to create a "logicless" template engine. Even Mustache, the consummate "logicless" template engine, supports conditionals, iterations, and will execute lambdas for you.
I build Handlebars so that I could start from zero and build up syntactic structures that would support data binding from the get-go. Instead of having to figure out how foo in bar
is data-bound, I could define an extremely limited syntax and then define how data bindings worked for each piece of the syntax.
For the uninitiated, Handlebars 1.1 has only the following syntax:
- Numbers (
1
) - Strings (
"hello"
) - Booleans (
true
,false
) - Paths (
foo.bar.baz
) - Mustache call expressions, which have a:
a. helper name (
identifier
) b. params, made up of 1-4 above c. hash params, whose keys areidentifier
and whose values are 1-4 above
With this restricted syntax, we were able to build simple abstractions in Ember that worked pretty well.
Unfortunately, with the first version of Ember's data binding support (the ones that shipped with Ember 1.0), every helper that comes with Ember needs to manually manage data binding. Because of the restricted syntax, it works out, but I've been wanting a nicer set of abstractions at the lower level for a while.
Disclaimer: The inspiration for much of the API design I have fallen in love with comes from .Net Reactive Extensions.
So let's say you have a simple template that looks like this:
Currently, Ember provides an abstraction that allows you do define that join
helper:
Ember.Handlebars.helper('join', function(first, second, options) {
return first + options.hash.with + second;
});
As a high-level abstraction, this is pretty nice, and Ember handles keeping the result up to date whenever foo
or bar
changes. Under the hood, however, Ember is doing some pretty low-level stuff to manage the data binding.
When working on HTMLBars, I had what turned out to be a very powerful idea:
- Every
{{...}}
consumes a stream of values. Whenever the stream produces a new value, HTMLBars (not a higher level abstraction like Ember) is responsible for updating the contents of the{{...}}
. - Recall that the Handlebars syntax is very simple. In HTMLBars, make every parameter a stream:
- String, numbers and booleans are streams that produce a single value and immediately complete
- Higher level abstractions like Ember implement a hook that converts a path into a stream of values that updates as the value changes.
For the purpose of HTMLBars, a stream's API is very simple:
stream.subscribe(function next(value) {
// update the attribute, text node or range represented by this value
}, function error(err) {
// error handling in data binding TBD
}, function complete() {
// tear down no-longer-necessary bookkeeping
});
The really cool thing about streams is that it's really easy to build higher level operations that work on any stream (like map
, filter
, zip
, firstOnly
and many many more).
So let's revisit that join
helper, using only streams:
// Just a regular helper here
HTMLBars.helper('join', function(first, second, options) {
// first and second are streams
return first.zip(second).map(function([ a, b ]) {
return a + options.hash.with + b;
});
});
In this example zip
is an operation on a stream that combines it with another stream and produces an output stream. The output stream will get a new value any time either the first or second stream gets a new value.
With this in mind, we can see that {{name}}
and {{join firstName lastName with=" "}}
work the same way. When HTMLBars sees {{name}}
, it generates a path stream and subscribes directly. Because join
is just a call made up of the same small number of primitives, and we now have a runtime primitive that represents the syntax, we can build up abstractions pretty easily.
For a long time, people have been asking Handlebars for arbitrary expressions inside of #if
, etc. For example:
Remember that I never set out to build a "logicless" template engine, so I'm not opposed to arbitrary expressions for that reason. However, I do care very much about keeping the syntax of Handlebars small so that it can be easily understood and host data binding implementations.
I've resisted arbitrary expressions because they open up a huge pandora's box of questions about how to bind to things like foo in bar
, and because I didn't have a nice way of modelling an alternate expression syntax that could easily be used to build up data-bindable abstractions.
But now, with HTMLBars and streams, I finally have it! (or so I think).
Let's take a look at the simple equality test above, slightly changed to use an alternate expression syntax.
It's easy enough to build equal
as a helper in normal Handlebars:
Handlebars.helper('equal', function(a, b) {
return a === b;
});
But what about in the data binding case? Again, I've resisted adding a syntax like this unless there was a nice, elegant solution for data binding as well. As it turns out, we now have the elegant solution:
HTMLBars.helper('equal', function(a, b) {
return a.zip(b).map(function(a, b) {
return a === b;
});
});
And the data binding semantics actually works equally well for more complex expressions:
The stream primitive makes all of this take care of itself:
HTMLBars.helper('exclaim', function(value) {
return value.map(function(value) { return value + "!"; });
});
HTMLBars.helper('join', function(first, second, options) {
first.zip(second).zip(options.with).map(function([ a, b, with ])
});
HTMLBars.helper('even-chars', function(value) {
return value.map(function(value) {
return value.length % 2 === 0;
});
});
The cool thing about this is that we haven't actually expanded the primitive syntax at all here. All we've done is provided a way to take the existing primitive values and compose them into higher level values. And by making each primitive representable as streams, we can build up libraries of abstractions that are data bound without worrying about the pitfalls of binding to arbitrary expressions.
And no, the similarity to Lisp and FRP is not lost on me.
It looks weirdly like Cloju....
you already noticed.