Created
May 12, 2011 22:39
-
-
Save mjtko/969621 to your computer and use it in GitHub Desktop.
knockout template engine for handlebars.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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); |
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));
}
};
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In Knockout Issue #86 sha1dy commented:
I imagine that you're referring to this code on line 102:
The
container
here is a temporary element used to create the DOM nodes to be returned from therenderTemplate
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 readonlyinnerHTML
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!