Skip to content

Latest commit

 

History

History
164 lines (129 loc) · 6.89 KB

ARCHITECTURE.md

File metadata and controls

164 lines (129 loc) · 6.89 KB

The process of converting a string template into a fully compiled HTMLbars template function that emits DOM nodes is somewhat involved. The purpose of this document is to shed some light on the process and describe where in the HTMLbars codebase these steps take place.

Overview

HTMLbars, like Handlebars, takes a string template of HTML and generates a compiled JavaScript template function, but whereas compiled Handlebars template functions emit strings that can be later parsed into DOM, HTMLBars template functions directly emit DOM.

A compiled HTMLbars template function contains the following:

  1. A build function that builds up and returns a document fragment.
  2. A hydrate function that returns information about where dynamic mustache content needs to be inserted into a clone of the document fragment.
  3. Code that is invoked every time the compiled template is rendered. This code calls build and caches the resulting fragment (so that build only needs to ever be called once), clones the fragment, then calls hydrate and loops through each mustache, passing each to runtime helpers resolve or attribute which can be overridden to perform data-binding, invoke user-defined helpers, etc.

In order to construct the above compiled template function, HTMLbars must:

  1. Parse a string template into a Handlebars AST.
  2. Convert vanilla Handlebars AST into HTML-aware HTMLbars AST.
  3. Build up document fragment generator function from HTMLBars AST.
  4. Build up hydrating function from the HTMLBars AST.
  5. Generate the final template code that performs the basic cloning logic, invokes build and hydrate, and loops through each mustache.

Compiling an HTMLbars template

1. Parse template with vanilla Handlebars

Handlebars has no knowledge/concept of the DOM or DOM elements, and the AST that it generates consists only of text nodes, mustache nodes, etc., and doesn't distinguish between mustaches that occur within an element's content or within the elements tag itself (or within a tag's attribute value).

Consider the following template:

<p class="user {{user.cssClass}}">
  User: {{user.name}}
  <button {{onclick "like"}}>Like</button>
</p>

The above vanilla HB AST looks something like:

Program
  Content: '<p class="user '
  Mustache: `user.cssClass`
  Content: '"> User: '
  Mustache: `user.name`
  Content: ' <button '
  Mustache: `onclick` params=[`"like"`]
  Content: '>Like</button> </p>'

See parser.js

2. Convert to HTMLBars AST

The whole point of HTMLbars is to add HTML awareness, so we have to take the AST generated by Handlebars and pass it to the HTMLbars parser, which accepts a vanilla Handlebars AST and outputs an AST with two main features that make it distinct from the vanilla HB AST:

  1. There are AST nodes for every DOM element
  2. The mustaches are distinguished between whether they occur a) within an element's content (e.g. <p>Hello {{name}}!</p>), b) within an element tag (e.g. <p {{some-helper}}>...</p>) or c) within an attribute's value (e.g. <p class="user {{user.cssClass}}">...</p>)

So the above example is restructured into an HTMLbars AST that looks something like:

Element tag='p' attributes=[Attr('class', Mustache(`concat "user " user.cssClass`))]
  "User: "
  Mustache(`user.name`)
  " "
  Element tag='button' helpers=[Mustache(`onclick "like"`)]
    "Like"

See parser.js and ast.js.

3. Build document fragment generator from HTMLbars AST

Overview: the HTMLbars approach

When an already-compiled HTMLbars template is first rendered into the DOM, a document fragment of all the HTML nodes in that template must be constructed. This process only needs to happen once; once this fragment is constructed, it is cloned (via fragment.cloneNode(true)) and inserted into the DOM every time the template is rendered. Example:

<p>Hello</p>

The first time this template is rendered, a master document fragment for this template will be constructed via something like:

if (!fragment) {
  // Fragment hasn't been generated for this HTMLbars 
  // template yet; this must be the first time we're
  // rendering this template. Let's create the fragment.
  // Note: all the code below is all generated by HTMLbars
  // when the <p>Hello</p> template is compiled.
  fragment = document.createDocumentFragment();
  var p = document.createElement('p');
  var textNode = document.createTextNode('Hello');
  p.appendChild(textNode);
  fragment.appendChild(p);
}

Once this master fragment has been constructed, all we need to do to generate the fragment that'll actually be inserted into the DOM (for this render and future renders), is call fragment.cloneNode(true), which returns a deep clone of our master fragment. And for our mustache-less example above, our work here is done; we can just append that cloned document fragment into the page (but obviously things will become more complicated once we add mustaches to our example).

Why cloneNode?

We could just build up each fragment that needs to be rendered via the same series of createDocumentFragment, createElement, createTextNode, etc., every time we needed to render the template, but it's not nearly as performant/efficient as building up the master template once and calling cloneNode(true) on it every time you need it.

Fwiw, the previous Handlebars-only approach to rendering templates involved generating a string of HTML and then turning it into DOM via innerHTML which made GC events unavoidable given all the string generation/copying/appending/etc. This is one of the many reasons that the HTMLbars approach offers so much in the way of perf improvements.

Build fragments from HTMLbars AST (cont.)

So, setting aside an example with mustaches for the moment, HTMLbars needs to take the DOM-aware HTMLBars AST and generate the code that will build up this fragment (i.e. the createDocumentFragment, createElement, createTextNode, etc., code from above). HTMLbars accomplishes this in two passes (on the compile side):

  1. In fragment-opcode-compiler.js, the HTMLbars AST is recursively walked and a flattened array of opcodes (used in step 2) is generated. The intent of this phase is to flatten the recursive structure of the AST so that no recursion need take place in our compiled template (recursion leads to deep object graphs, clunky GC, etc)
  2. This opcodes array is then passed to lib/compiler/fragment.js which loops through the array and builds up a string of JavaScript code for the fragment-generating build function that'll live inside the final HTMLBars template function.

Build Hydrate function

TODO: this.