Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Anatomy of HTMLBars Templates

Anatomy of HTMLBars Templates

2597afdfd094c35a8d496c337c49ce9e?s=128

Martin Muñoz

June 05, 2014
Tweet

Transcript

  1. Anatomy of HTMLBars Templates

  2. I’m Martin Muñoz @mmun on GitHub & IRC @_mmun on

    Twitter :(
  3. Background

  4. What’s Handlebars?

  5. Handlebars is a templating language that compiles a string template

    into a JavaScript function. This function takes in a context argument and returns a string of HTML.
  6. Hello <em>{{name}}</em>!

  7. function template(Handlebars, depth0, helpers, partials, data) { this.compilerInfo = [4,'>=

    1.0.0']; helpers = this.merge(helpers, Handlebars.helpers); data = data || {}; var buffer = '', stack1, helper, functionType = ‘function’, escapeExpression = this.escapeExpression; buffer += 'Hello <em>'; if (helper = helpers.name) { stack1 = helper.call(depth0, { hash: {}, data: data }); } else { helper = depth0 && depth0.name; stack1 = typeof helper === functionType ? helper.call(depth0, { hash: {}, data: data }) : helper; } buffer += escapeExpression(stack1) + '</em>!'; return buffer; } Hello <em>{{name}}</em>!
  8. function template(Handlebars, depth0, helpers, partials, data) { this.compilerInfo = [4,'>=

    1.0.0']; helpers = this.merge(helpers, Handlebars.helpers); data = data || {}; var buffer = '', stack1, helper, functionType = ‘function’, escapeExpression = this.escapeExpression; buffer += 'Hello <em>'; if (helper = helpers.name) { stack1 = helper.call(depth0, { hash: {}, data: data }); } else { helper = depth0 && depth0.name; stack1 = typeof helper === functionType ? helper.call(depth0, { hash: {}, data: data }) : helper; } buffer += escapeExpression(stack1) + '</em>!'; return buffer; } Hello <em>{{name}}</em>!
  9. Handlebars just concatenates strings together. It doesn’t see the HTML.

  10. Consequences • Mustaches don’t know where they appear in the

    HTML document. For example, in order to bind src to url in the template <img src=“{{url}}”> the mustache needs to know that it’s in an attribute value. • Every time a template is rendered, it’s parsed from scratch via innerHTML. Can we do better? • String manipulations increase the pressure on the garbage collector*. * I haven’t checked this for myself.
  11. If only there was a way to parse the HTML

    at compile time, programatically create DOM nodes using the DOM APIs, and then subsequently deep clone them to make rerenders really fast.
  12. HTMLBars

  13. [Some of this stuff will change.]

  14. Let’s take a look at our template again, but through

    the HTMLBars compiler. Hello <em>{{name}}</em>!
  15. function anonymous(dom, Morph) { return function () { function build(dom)

    { var el0 = dom.createDocumentFragment(); dom.appendText(el0, 'Hello '); var el1 = dom.createElement('em'); dom.appendChild(el0, el1); dom.appendText(el0, '!\n'); return el0; } var cachedFragment = null; return function template(context, options) { if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragment); var hooks = options && options.hooks; var helpers = options && options.helpers || {}; var morph0 = Morph.create(fragment.childNodes[1], -1, -1); hooks.content(morph0, 'name', context, [], { escaped: true }, helpers); return fragment; }; }(); } Hello <em>{{name}}</em>!
  16. function anonymous(dom, Morph) { return function () { function build(dom)

    { var el0 = dom.createDocumentFragment(); dom.appendText(el0, 'Hello '); var el1 = dom.createElement('em'); dom.appendChild(el0, el1); dom.appendText(el0, '!\n'); return el0; } var cachedFragment = null; return function template(context, options) { if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragment); var hooks = options && options.hooks; var helpers = options && options.helpers || {}; var morph0 = Morph.create(fragment.childNodes[1], -1, -1); hooks.content(morph0, 'name', context, [], { escaped: true }, helpers); return fragment; }; }(); } fragment! program hydration! program template! program Hello <em>{{name}}</em>!
  17. function anonymous(dom, Morph) { return function () { function build(dom)

    { var el0 = dom.createDocumentFragment(); dom.appendText(el0, 'Hello '); var el1 = dom.createElement('em'); dom.appendChild(el0, el1); dom.appendText(el0, '!\n'); return el0; } var cachedFragment = null; return function template(context, options) { • Creates a document fragment via a dom abstraction. • Doesn’t see the {{mustaches}}. Fragment program Hello <em>{{name}}</em>!
  18. var base = { appendText: function(element, text) { element.appendChild(document.createTextNode(te },

    ! appendChild: function(element, childElement) { element.appendChild(childElement); }, ! setAttribute: function(element, name, value) { element.setAttribute(name, value); }, ! createElement: function(tagName) { return document.createElement(tagName); }, ! createDocumentFragment: function() { return document.createDocumentFragment(); }, ! createTextNode: function(text) { return document.createTextNode(text); }, ! cloneNode: function(element) { return element.cloneNode(true); } This is the default dom. Nothing fancy.
  19. var cachedFragment = null; return function template(context, options) { if

    (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragment); var hooks = options && options.hooks; var helpers = options && options.helpers || {}; var morph0 = Morph.create(fragment.childNodes[1], -1, -1); hooks.content(morph0, 'name', context, [], { escaped: true }, he return fragment; }; • Calls the fragment program to build the fragment on the first run through. • Caches it and deep clones it on the next runs. Template program Hello <em>{{name}}</em>!
  20. var hooks = options && options.hooks; var helpers = options

    && options.helpers || {}; var morph0 = Morph.create(fragment.childNodes[1], -1, -1); hooks.content(morph0, 'name', context, [], { escaped: true }, helpers); return fragment; }; • Uses Morphs to keep references to the boundary DOM nodes surrounding a mustache in the fragment that was just cloned. • Calls the content hook to fill in morph0 and possibly setup any streams. Hydration program Hello <em>{{name}}</em>!
  21. What does the content hook do?

  22. It’s up to the framework to implement it.

  23. export function content(morph, path, context, params, options) { var value;

    var helper = this.lookupHelper(path, context, options); ! if (helper) { value = helper(context, params, options); } else { value = this.simple(context, path, options); } ! if (!options.escaped) { value = new SafeString(value); } ! morph.update(value); } Here’s the default content hook:
  24. export function lookupHelper(name, context, options) { return options.helpers[name]; } lookupHelper

    A hook that resolves a string to a classical Handlebars helper, e.g. each, with, if.
  25. export function simple(path, context, options) { return Ember.get(context, path); }

    simple A hook that resolves a path against a context.
  26. What about bindings?

  27. export function content(morph, path, context, params, options) { var value;

    var helper = this.lookupHelper(path, context, options); ! if (helper) { value = helper(context, params, options); } else { value = this.simple(context, path, options); } ! if (!options.escaped) { value = new SafeString(value); } ! morph.update(value); ! if (value.isSubscribable) { value.subscribe(function(updatedValue) { morph.update(updatedValue); }); } }
  28. What about blocks? {{#if isSelected}} It’s selected! {{else}} It’s not

    selected! {{/each}}
  29. They also use the content hook, but they pass in

    a reference to the child templates via the render and inverse options: hooks.content(morph0, 'if', context, ['isSelected'], { types: ['id'], hashTypes: {}, hash: {}, render: function() { /* main template program */ }, inverse: function() { /* inverse template program */ }, escaped: true, data: typeof options !== 'undefined' && options.data }, helpers);
  30. Let’s take a closer look at this template… {{#each people}}

    Hello <em>{{name}}</em>! {{/each}}
  31. function anonymous(dom, Morph) { return function () { var child0

    = function () { function build(dom) { var el0 = dom.createDocumentFragment( dom.appendText(el0, '\n Hello '); var el1 = dom.createElement('em'); dom.appendChild(el0, el1); dom.appendText(el0, '!\n'); return el0; } var cachedFragment = null; return function template(context, optio if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFr var hooks = options && options.hooks; var helpers = options && options.help var morph0 = Morph.create(fragment.ch hooks.content(morph0, 'name', context return fragment; }; }(); function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, ''); dom.appendText(el0, ''); return el0; } var cachedFragment = null; return function template(context, options) if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragme var hooks = options && options.hooks; var helpers = options && options.helpers var morph0 = Morph.create(fragment, 0, 1) hooks.content(morph0, 'each', context, [' types: ['id'], hashTypes: {}, hash: {}, render: child0, escaped: true, data: typeof options !== 'undefined' && }, helpers); return fragment; }; }(); } function anonymous(dom, Morph) { return function () { var child0 = function () { function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, '\n Hello '); var el1 = dom.createElement('em'); dom.appendChild(el0, el1); dom.appendText(el0, '!\n'); return el0; } var cachedFragment = null; return function template(context, options) { if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragment); var hooks = options && options.hooks; var helpers = options && options.helpers || {}; var morph0 = Morph.create(fragment.childNodes[1], -1, -1); hooks.content(morph0, 'name', context, [], { escaped: true }, helpers); return fragment; }; }();
  32. function anonymous(dom, Morph) { return function () { var child0

    = function () { function build(dom) { var el0 = dom.createDocumentFragment( dom.appendText(el0, '\n Hello '); var el1 = dom.createElement('em'); dom.appendChild(el0, el1); dom.appendText(el0, '!\n'); return el0; } var cachedFragment = null; return function template(context, optio if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFr var hooks = options && options.hooks; var helpers = options && options.help var morph0 = Morph.create(fragment.ch hooks.content(morph0, 'name', context return fragment; }; }(); function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, ''); dom.appendText(el0, ''); return el0; } var cachedFragment = null; return function template(context, options) if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragme var hooks = options && options.hooks; var helpers = options && options.helpers var morph0 = Morph.create(fragment, 0, 1) hooks.content(morph0, 'each', context, [' types: ['id'], hashTypes: {}, hash: {}, render: child0, escaped: true, data: typeof options !== 'undefined' && }, helpers); return fragment; }; }(); } function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, ''); dom.appendText(el0, ''); return el0; } var cachedFragment = null; return function template(context, options) { if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragment); var hooks = options && options.hooks; var helpers = options && options.helpers || {}; var morph0 = Morph.create(fragment, 0, 1); hooks.content(morph0, 'each', context, ['people'], { types: ['id'], hashTypes: {}, hash: {}, render: child0, escaped: true, data: typeof options !== 'undefined' && options.data }, helpers); return fragment; }; }(); }
  33. function anonymous(dom, Morph) { return function () { var child0

    = function () { function build(dom) { var el0 = dom.createDocumentFragment( dom.appendText(el0, '\n Hello '); var el1 = dom.createElement('em'); dom.appendChild(el0, el1); dom.appendText(el0, '!\n'); return el0; } var cachedFragment = null; return function template(context, optio if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFr var hooks = options && options.hooks; var helpers = options && options.help var morph0 = Morph.create(fragment.ch hooks.content(morph0, 'name', context return fragment; }; }(); function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, ''); dom.appendText(el0, ''); return el0; } var cachedFragment = null; return function template(context, options) if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragme var hooks = options && options.hooks; var helpers = options && options.helpers var morph0 = Morph.create(fragment, 0, 1) hooks.content(morph0, 'each', context, [' types: ['id'], hashTypes: {}, hash: {}, render: child0, escaped: true, data: typeof options !== 'undefined' && }, helpers); return fragment; }; }(); } function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, ''); dom.appendText(el0, ''); return el0; } var cachedFragment = null; return function template(context, options) { if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragment); var hooks = options && options.hooks; var helpers = options && options.helpers || {}; var morph0 = Morph.create(fragment, 0, 1); hooks.content(morph0, 'each', context, ['people'], { types: ['id'], hashTypes: {}, hash: {}, render: child0, escaped: true, data: typeof options !== 'undefined' && options.data }, helpers); return fragment; }; }(); } dom.appendChild(el0, el1); dom.appendText(el0, '!\n'); return el0; } var cachedFragment = null; return function template(context, options) { if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragment); var hooks = options && options.hooks; var helpers = options && options.helpers || {}; var morph0 = Morph.create(fragment.childNodes[1], -1, hooks.content(morph0, 'name', context, [], { escaped: helpers); return fragment; }; }(); function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, ''); dom.appendText(el0, ''); return el0; } var cachedFragment = null; return function template(context, options) { if (cachedFragment === null) {
  34. function anonymous(dom, Morph) { return function () { var child0

    = function () { function build(dom) { var el0 = dom.createDocumentFragment( dom.appendText(el0, '\n Hello '); var el1 = dom.createElement('em'); dom.appendChild(el0, el1); dom.appendText(el0, '!\n'); return el0; } var cachedFragment = null; return function template(context, optio if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFr var hooks = options && options.hooks; var helpers = options && options.help var morph0 = Morph.create(fragment.ch hooks.content(morph0, 'name', context return fragment; }; }(); function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, ''); dom.appendText(el0, ''); return el0; } var cachedFragment = null; return function template(context, options) if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragme var hooks = options && options.hooks; var helpers = options && options.helpers var morph0 = Morph.create(fragment, 0, 1) hooks.content(morph0, 'each', context, [' types: ['id'], hashTypes: {}, hash: {}, render: child0, escaped: true, data: typeof options !== 'undefined' && }, helpers); return fragment; }; }(); } function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, ''); dom.appendText(el0, ''); return el0; } var cachedFragment = null; return function template(context, options) { if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragment); var hooks = options && options.hooks; var helpers = options && options.helpers || {}; var morph0 = Morph.create(fragment, 0, 1); hooks.content(morph0, 'each', context, ['people'], { types: ['id'], hashTypes: {}, hash: {}, render: child0, escaped: true, data: typeof options !== 'undefined' && options.data }, helpers); return fragment; }; }(); } dom.appendChild(el0, el1); dom.appendText(el0, '!\n'); return el0; } var cachedFragment = null; return function template(context, options) { if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFragment); var hooks = options && options.hooks; var helpers = options && options.helpers || {}; var morph0 = Morph.create(fragment.childNodes[1], -1, -1); hooks.content(morph0, 'name', context, [], { escaped: true } helpers); return fragment; }; }(); function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, ''); dom.appendText(el0, ''); return el0; } var cachedFragment = null; return function template(context, options) { if (cachedFragment === null) { cachedFragment = build(dom); dom.appendChild(el0, el1); dom.appendText(el0, '!\n'); return el0; } var cachedFragment = null; return function template(context, optio if (cachedFragment === null) { cachedFragment = build(dom); } var fragment = dom.cloneNode(cachedFr var hooks = options && options.hooks; var helpers = options && options.help var morph0 = Morph.create(fragment.ch hooks.content(morph0, 'name', context helpers); return fragment; }; }(); function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, ''); dom.appendText(el0, ''); return el0; } var hooks = options && options.hooks; var helpers = options && options.helpers || {}; var morph0 = Morph.create(fragment.childNodes[1], -1, -1); hooks.content(morph0, 'name', context, [], { escaped: true }, helpers); return fragment; }; }(); function build(dom) { var el0 = dom.createDocumentFragment(); dom.appendText(el0, ''); dom.appendText(el0, ''); return el0; } var cachedFragment = null; return function template(context, options
  35. [Sorry about all the code.]

  36. Tell me more about Morph.

  37. None
  38. Morph.create(parentNode, beforeChildIndex, afterChildIndex) A Morph handles a range of DOM

    between two nodes. When the cached fragment is cloned a new set of DOM nodes are instantiated, so we need to create a new set of morphs as well.
  39. <h1>Hi {{a}}</h1> <div> <span>{{b}}</span> {{c}}<br>{{d}} </div> var fragment = dom.cloneNode(cachedFragment);

    var parent0 = fragment.childNodes[2]; ! {{a}}: Morph.create(fragment.childNodes[0], 0, -1) {{b}}: Morph.create(parent0.childNodes[1], -1, -1) {{c}}: Morph.create(parent0, 2, 3) {{d}}: Morph.create(parent0, 3, 4) This template… …creates these morphs.
  40. The morph’s child nodes can be controlled through an array-like

    interface. morph.replace(startIndex, removeCount, nodesToAdd) This is important for the implementation of CollectionView.
  41. What about mustaches inside of tags? <img {{action "hi"}} src="{{src}}">

  42. Actually, it’s treated a lot like <img {{action "hi"}} {{attribute

    "src" src}}>
  43. hooks.element(fragment, 'action', context, ['hi'], { types: ['string'], hashTypes: {}, hash:

    {} }, helpers); …which compile to the element hook. hooks.element(fragment, name, context, params, options, helpers) hooks.element(fragment, 'attribute', context, [ 'src', 'path' ], { types: [ 'string', 'id' ], hashTypes: {}, hash: {} }, helpers); {{action "hi"}} {{attribute "src" path}}
  44. What about (web components / subexpressions / mixed attribute values

    / SVG / other edge cases and optimizations in the compiler)?
  45. Ask me later!

  46. If you want to learn more, check out the JSBin

    I used to generate the templates: http://jsbin.com/raxon/
  47. Thanks for listening. Any questions?