Skip to content

New Template Engine Preview

Avital Oliver edited this page Oct 15, 2013 · 46 revisions

The first stage of "Meteor UI" is an overhaul of Meteor's rendering system -- including Spark and the Template API -- to support fine-grained DOM updates, jQuery integration and simpler APIs. Because Meteor UI is a big undertaking that makes significant changes to existing APIs, while introducing new ones as well, it will land on master in multiple stages. This email describes the major changes to expect in the first stage, landing in the next month or two.

This preview includes a brand-new template parser and replaces Spark with a new rendering system. Because these under-the-hood changes will have implications for many apps, we're rolling them out first, while keeping higher parts of the UI stack mostly the same. For example, we're holding back on some major improvements to the Template API, including events, helpers, template instances, and callbacks, in order to keep the syntax the same for Stage I (except where noted below, notably the rendered callback).

The following sections, listed here, explain the major changes in the new template engine:

For the adventurous user, you can play with the current work-in-progress version of the code on the "shark" branch, or with the "rendering-preview-0" release (run your app with meteor --release template-engine-preview-0). Expect major bugs and limitations. We usually don't solicit feedback on code in such an unfinished state, but there's been so much interest and excitement about Meteor UI that we think our hardcore users might be willing to help bang on a buggy version. We also want to know how these changes affect the patterns you use in your Meteor apps and packages. Even if some patterns are no longer possible, we believe that there will be new, more elegant patterns to take their place.

Current known limitations to be resolved prior to release

  • {{#markdown}} no longer works. There will be a replacement pattern.
  • TEXTAREA and SELECT tags with contents that call helpers don't work.

Fine-grained updates that play nicely with jQuery

The new rendering model doesn't replace a template when its data changes, it just replaces its parts, like text nodes and element attributes. This approach is not only more efficient, it means that DOM elements generally remain in place as long their parent elements and templates are not removed by an #if, an #each, or other conditional logic. Even if a template's data context changes, or its parent's data context, or an attribute on an enclosing element, the changes are reactively propagated to where they affect the DOM, rather than causing whole parts of the DOM to replaced as before. Structural changes to the DOM or template hierarchy are only made when a clear reason exists, and you can expect critical elements such as text fields and videos to be automatically preserved.

Thanks to this new model, Meteor will no longer assume it has complete control over rendered templates. This allows jQuery plugins to mostly work out of the box, even in the presence of template re-renders. For example, an element created with <div class="foo {{bar}}" interoperates perfectly with jQuery's $('.foo').addClass(...). Here's a short video demonstating using jQuery UI to create a reorderable list.

It's worth noting that Spark's match-and-patch algorithm is gone. Therefore, form fields (eg INPUT, SELECT, TEXTAREA, ...) generated via HTML strings returned from helpers will suffer from the "input preservation problem". When the helpers gets executed, input field that is currently active will no longer be selected. To work around this, use templates instead of HTML strings to generate your forms.

No more {{#constant}}, {{#isolate}}, or preserve

Because {{#constant}}, {{#isolate}}, and preserve all existed to escape various consequences of the old rendering model, these directives have all been removed.

Rendered callback only fires once

The rendered callback now only fires once per template instance, when the template is first rendered. Template instances are never re-rendered in the new rendering model. We expect this to greatly simplify dealing with the rendered callback!

New Template Parser

Meteor UI has a new template parser and compiler based on Handlebars, with the internal codename "Spacebars." This parser allows us to do several things that were previously impossible.

HTML-aware updates. The template parser now parses HTML tags in addition to stache tags, making finer-grained reactive updates possible, including attribute-level updates to DOM elements.

Component syntax. If you have a component named MyWidget, you'll be able to invoke it using {{> MyWidget}}, or as a block with content: {{#MyWidget}} ... content ... {{/MyWidget}}. See section 'New pattern for defining custom block helpers'.

Precompilation. The Spacebars compiler generates simple procedural code that calls an internal Meteor interface which in the future will perform either client-side or server-side rendering. This is more efficient than either interpreting the template or parsing its HTML output at runtime.

Syntax extensions. Handlebars syntax is extremely minimal, and we foresee adding some additional well-chosen extensions over time. (We will also implement the top features of current Handlebars that are missing from Meteor, like #each that supports objects and lets you access the current index or key.)

Future pluggable template engines. Having our own template parser marks a shift from trying to support existing template languages with only minor code changes. It's become clear over time how much value comes from a great reactive templating experience, and our top priority in that area is figuring out what that looks like even if it's not possible to do using existing string-based template engines. The good news, if you're a fan of other styles of template language, is that the long-term story actually just got better. The interface between the Spacebars and Meteor UI packages opens the door for first-class pluggable template systems by providing a compilation target for third-party template compilers.

Template.foo is not a function and does not return a string

The Spark templating model was engineered around the concept of a template as a function returning a string. In the new world, templates don't construct strings, they construct reactive DOM through a rendering API (which can generate either HTML or DOM, for eventual server-side rendering). You can write code manually against this rendering API, but in most cases it's easiest to use a template.

When components land (after the new template engine), templates will be thought of as simple component classes, which can have subclasses and instances participating in a prototype chain, which may help explain why they are now objects rather than functions.

Here are the common cases for calling Template.foo(...) to get a string, and some new ways to do the same thing.

  1. Helper calculates which template to display.

Old:

{{{post}}}

Template.foo.post = function () {
  return Template[this.postName]();
}

New:

{{> post}}

Template.foo.post = function () {
  return Template[this.postName];
}

Template inclusions now search the namespace of helpers and data for template objects, so it's easy to programmatically choose which template to use. This is a powerful feature, and will allow patterns like assigning one template as a helper of another so that it can be overridden.

  1. Helper calculates which template to display, passing a data context to the template.

Old:

{{{post}}}

Template.foo.post = function () {
  return Template[this.postName]({foo: "bar"});
}

New:

{{> post}}

Template.foo.post = function () {
  return Template[this.postName].withData({foo: "bar"});
}
  1. Helper calculates how to wrap a template. See section 'New pattern for defining custom block helpers'.

  2. Meteor.render for rendering a template and inserting it into the DOM. See next section.

Meteor.render has been removed

Meteor.render was a thin wrapper around Spark.render and used the old string-based paradigm.

The most common use of Meteor.render was for inserting some reactive Meteor UI content into an existing document. At present, this is the API used to achieve this:

UI.insert(UI.render(Template.foo), document.body)

UI.insert(UI.render(Template.bar), parentNode, beforeNode)

UI.insert(UI.render(Template.baz.withData(myData)), containerDiv)

In other words, UI.render instantiates a template and UI.insert adds it as a child of some element in the DOM.

No more {{..}}; fields and helpers walk up the template tree

Using {{..}} in template to access the patern data context no longer works. The old implementation (search the parent data context) was different than the Handlebars specification (search the parent template). Instead, any usage of {{foo}} to access fields or helpers will always search up the data context tree.

New pattern for defining custom block helpers

If you've defined any custom block helpers (those besides the built-in #if, #each, #with, and #unless), you'll have to convert them. Block helpers are now defined either as templates or as helper functions that return templates.

Here are examples of the two ways to define custom block helpers.

  1. Defining {{#ifEven}} as a template:

Definition:

<template name="ifEven">
  {{#if isEven value}}
    {{> content}}
  {{else}}
    {{> elseContent}}
  {{/if}}
</template>

Template.ifEven.isEven = function (value) {
  return (value % 2) === 0;
}

Usage:

{{#ifEven value=2}}
  2 is even
{{else}}
  2 is odd
{{/if}}

Within templates that are used as block helpers, use {{> content}} and {{> elseContent}} to insert the main or else contents passed to your helper. When defining a block helper as a template, there is no way to access positional arguments.

  1. Defining {{#maybeDiv}} as a helper returning a template:

This block helper accepts one boolean positional argument. If it evaluates to true, a div wrapper element is used. Otherwise, no wrapper element is used.

Definition:

<template name="_maybeDiv_wrapInDiv">
  <div>
    {{> content}}
  </div>
</template>

<template name="_maybeDiv_noop">
  {{> content}}
</template>

Handlebars.registerHelper('maybeDiv', function (isBlock) {
  if (isBlock)
    return Template._maybeDiv_wrapInDiv;
  else
    return Template._maybeDiv_noop;
});

Usage:

{{#maybeDiv true}}
  contents
{{/maybeDiv}}

Components defined as helper functions accept any number of positional arguments, with an additional final options argument with any keyword arguments (if they exist).

Events use jQuery

Event delegation uses jQuery's implementation rather than a custom-built global event capturing system. Attaching event handlers closer to the templates that declare them has performance benefits, and jQuery's event implementation is the best out there at papering over IE quirks, and even differences in more recent browsers. (Early Android browsers, for example, are now in the picture.) Meteor developers who already use jQuery also benefit from an increasing level of interoperability. For example, when Meteor removes elements from the DOM, it also cleans up jQuery event handlers that you bound external to Meteor.

The broader story is that there is some kernel of DOM compatibility code that every modern app needs. For apps and sites that need to support the web at large, that kernel should probably come from jQuery's codebase, while for apps that only need to work on Chrome or iOS, say, the amount of code needed to provide features like event delegation -- basically to do "jQuery without the jQuery API" -- shrinks a lot but doesn't disappear.

If you don't use jQuery and are concerned about having a dependency on it, we are defining a "DOM backend" connector interface between Meteor and jQuery that will allow other libraries such as Zepto, or something even smaller, to play the same role. As for today, there are suitable builds of jQuery starting at 17K on the wire, depending on whether you need IE 8 support and whether you want jQuery's extended selector syntax. This is small considering that we haven't done much yet to optimize the downloaded code size of Meteor apps. In the future, there's always the possibility of creating a super-slim jQuery build to bring the file size down more.

There's also the future possibility of using browser feature detection to download different code on different browsers, so that only IE 8 would ever receive IE 8 compatibility hacks.

Clone this wiki locally