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.
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:
- A
build
function that builds up and returns a document fragment. - A
hydrate
function that returns information about where dynamic mustache content needs to be inserted into a clone of the document fragment. - Code that is invoked every time the compiled template is rendered.
This code calls
build
and caches the resulting fragment (so thatbuild
only needs to ever be called once), clones the fragment, then callshydrate
and loops through each mustache, passing each to runtime helpersresolve
orattribute
which can be overridden to perform data-binding, invoke user-defined helpers, etc.
In order to construct the above compiled template function, HTMLbars must:
- Parse a string template into a Handlebars AST.
- Convert vanilla Handlebars AST into HTML-aware HTMLbars AST.
- Build up document fragment generator function from HTMLBars AST.
- Build up hydrating function from the HTMLBars AST.
- Generate the final template code that performs the basic cloning logic,
invokes
build
andhydrate
, and loops through each mustache.
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
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:
- There are AST nodes for every DOM element
- 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"
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).
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.
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):
- In fragment_opcode.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)
- 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-generatingbuild
function that'll live inside the final HTMLBars template function.
TODO: this.