Skip to content

Instantly share code, notes, and snippets.

@mjtko
Created May 12, 2011 22:39
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mjtko/969621 to your computer and use it in GitHub Desktop.
Save mjtko/969621 to your computer and use it in GitHub Desktop.
knockout template engine for handlebars.js
/*
Handlebars Template Engine for Knockout JavaScript library
*//*!
Copyright (c) 2011 Mark J. Titorenko
License: MIT (http://www.opensource.org/licenses/mit-license.php)
*/
ko.handlebarsTemplateEngine = function () {
// adapted from MooTools.Element
//
// This is necessary to allow us to easily deal with table
// fragment templates.
var setHtml = (function(){
var tableTest = (function() {
try {
var table = document.createElement('table');
table.innerHTML = '<tr><td></td></tr>';
return false;
} catch (e) {
return false;
}
})();
var wrapper = document.createElement('div');
var translations = {
table: [1, '<table>', '</table>'],
select: [1, '<select>', '</select>'],
tbody: [2, '<table><tbody>', '</tbody></table>'],
tr: [3, '<table><tbody><tr>', '</tr></tbody></table>']
};
translations.thead = translations.tfoot = translations.tbody;
var empty = function(target) {
var node;
while ( node = target.firstChild ) {
node.parentNode.removeChild(node);
}
return target;
}
return function(target,html){
var wrap = (!tableTest && translations[target.tagName.toLowerCase()]);
if (wrap){
var first = wrapper;
first.innerHTML = wrap[1] + html + wrap[2];
for (var i = wrap[0]; i--;) first = first.firstChild;
empty(target);
var node;
while (node = first.firstChild) {
target.appendChild(node);
}
} else {
target.innerHTML = html;
}
};
})();
var templates = {};
var parseMemoCommentText = function (memoCommentText) {
var match = memoCommentText.match(/^<!--(\[ko_memo\:.*?\])-->$/);
return match ? match[1] : null;
};
var render = function(data,node,target) {
while (node) {
var nodeOut;
if ( node.tagName === 'SCRIPT' &&
node.getAttribute('data-generator') === 'ko.handlebarsTemplateEngine' ) {
// this needs to be evaluated within the context of
// 'data' in order to get data bindings to work
// correctly when not prefixed with 'data.'
with(data)
nodeOut = document.createComment(parseMemoCommentText(eval(node.innerHTML)));
} else {
// recurse
nodeOut = node.cloneNode(false);
render(data,node.firstChild,nodeOut);
}
target.appendChild(nodeOut);
node = node.nextSibling;
}
return target;
};
this['getTemplateNode'] = function (template) {
var templateNode = document.getElementById(template);
if (templateNode == null)
throw new Error("Cannot find template with ID=" + template);
return templateNode;
};
this['renderTemplate'] = function (templateId, data, options) {
var result = templates[templateId](data);
// we have to deal with anything that contains <tr>
// separately, as we can't append <tr> elements to a <div>.
//
// references:
// http://stackoverflow.com/questions/5090031/regex-get-tr-tags/5091399#5091399
var tabular = result.match(/<tr[\s\S]*?<\/tr>/g);
var container = document.createElement( ( tabular ? 'tbody' : 'div' ) );
// Use our custom setHtml (adapted from MooTools) in order to
// work around readonly tbody under IE.
//
// references:
// http://stackoverflow.com/questions/4729644/cant-innerhtml-on-tbody-in-ie/4729743#4729743
setHtml(container,result);
// deal with late binding for KO
var src = render(data,container.firstChild,document.createElement("div")).childNodes
// clone the nodes so they can be cleanly inserted into the DOM
var target = [];
for ( var i = 0, l = src.length; i < l; i++ ) {
target.push(src[i].cloneNode(true));
}
return target;
};
this['isTemplateRewritten'] = function (templateId) {
return templates[templateId] !== undefined;
};
this['rewriteTemplate'] = function (templateId, rewriterCallback) {
var templateNode = this['getTemplateNode'](templateId);
// elide templateNode from the DOM - no longer needed
templateNode.parentNode.removeChild(templateNode);
templates[templateId] = Handlebars.compile(rewriterCallback(templateNode.innerHTML));
};
this['createJavaScriptEvaluatorBlock'] = function (script) {
return '<script data-generator="ko.handlebarsTemplateEngine" type="text/javascript">// <![CDATA[\n' + script + '\n// ]]></script>';
};
};
Handlebars.registerHelper('dyn', function(observable) {
return observable();
});
ko.handlebarsTemplateEngine.prototype = new ko.templateEngine();
// Use this one by default
ko.setTemplateEngine(new ko.handlebarsTemplateEngine());
ko.exportSymbol('ko.handlebarsTemplateEngine', ko.handlebarsTemplateEngine);
@mjtko
Copy link
Author

mjtko commented May 31, 2011

In Knockout Issue #86 sha1dy commented:

thanks! one note - why are you wrapping template output in div or tbody? is it required by KO?

I imagine that you're referring to this code on line 102:

var container = document.createElement( ( tabular ? 'tbody' : 'div' ) );

The container here is a temporary element used to create the DOM nodes to be returned from the renderTemplate function. If the content is tabular data (ie. <tr> elements) then it has to be written inside a <tbody> element, otherwise it is written inside a <div> element.

The nodes are created using setHtml (adapted from MooTools) which sets about the task of creating the DOM nodes using the innerHTML property - it does some feature detection to work out if there's going to be a problem in IE, which has a readonly innerHTML property on <tbody> elements and thus requires a workaround.

Once the handlebars rendered DOM nodes have been created, they are then passed to Knockout's renderer which performs the necessary data binding where specified - this excludes the temporary container node (hence container.firstChild).

Finally the DOM nodes returned from the KO data binding renderer (again, excluding the container, hence .childNodes) are looped over, cloned and pushed into a target array for return to KO.

There shouldn't be any additional or unexpected <div> or <tbody> elements - they're just used internally in order to deal with the DOM correctly.

Hope this clears things up!

@maxfridbe
Copy link

Any chance you could post some sample usages that show nesting observables, or compiled views.

@jelling
Copy link

jelling commented Aug 19, 2013

Replacing rewriteTemplate() with the following adds support for pre-compiled templates:

this['rewriteTemplate'] = function (templateId, rewriterCallback) {

    // first see if we have a pre-compiled template
    if (typeof(Handlebars.templates[templateId]) !== "undefined") {
        templates[templateId] = Handlebars.templates[templateId];
    } else {
        // try loading the template from a DOM node 
        var templateNode = this['getTemplateNode'](templateId);
        // elide templateNode from the DOM - no longer needed
        templateNode.parentNode.removeChild(templateNode);
        templates[templateId] = Handlebars.compile(rewriterCallback(templateNode.innerHTML));            
    }

};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment