public
Last active

knockout template engine for handlebars.js

  • Download Gist
handlebarsTemplateEngine.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
/*
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);

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!

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

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));            
    }

};

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.