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

Anatomy of HTMLBars Templates

Anatomy of HTMLBars Templates

Avatar for Martin Muñoz

Martin Muñoz

June 05, 2014
Tweet

More Decks by Martin Muñoz

Other Decks in Programming

Transcript

  1. 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.
  2. 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>!
  3. 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>!
  4. 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.
  5. 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.
  6. Let’s take a look at our template again, but through

    the HTMLBars compiler. Hello <em>{{name}}</em>!
  7. 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>!
  8. 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>!
  9. 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>!
  10. 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.
  11. 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>!
  12. 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>!
  13. 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:
  14. 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.
  15. export function simple(path, context, options) { return Ember.get(context, path); }

    simple A hook that resolves a path against a context.
  16. 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); }); } }
  17. 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);
  18. 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; }; }();
  19. 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; }; }(); }
  20. 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) {
  21. 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
  22. 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.
  23. <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.
  24. 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.
  25. 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}}
  26. What about (web components / subexpressions / mixed attribute values

    / SVG / other edge cases and optimizations in the compiler)?
  27. If you want to learn more, check out the JSBin

    I used to generate the templates: http://jsbin.com/raxon/