... as I went through others, there is always this voice in head, which says: "Oh, this is a lot of code and functionality. Is that really necessary or would it slow down the performance?"
This is a reasonable consideration, but premature optimisation is evil for a reason. It might be a good idea to try to roll out your own solution, but it would be beneficial to learn more about the problem and other existing solutions before implementing it yourself.
As for minimalistic templating library, take a look at Underscore _.template
.
It's literally 45 lines (add one or two helper functions) and does its job well.
As for your solution, I have a few points:
Don't use AJAX to load templates
If you're worried about performance, loading templates via AJAX is a bad idea. This will be a terrible bottleneck, not some hundred more lines of code in a different templating library.
Moreover, your code assumes you need to make an HTTP request each time you render something. If you have a list view with 20 items, that would be 20 requests.
This is absolutely unsuitable for production code.
Don't use synchronous AJAX calls, like, ever
The very point of AJAX is processing things asynchronously, without blocking other scripts from executing. There is rarely a reason to do things synchronously.
Again, please don't do this in production code.
Instead, put templates in HTML or (better) compile them to JS
Most, if not all, templating libraries assume the template is already available in the client code, and this is the correct way to go. Now, there are two ways how you can accomplish this:
1. You can embed templates in DOM with tricks like <script text="text/template">
The upside is that it is simple (no additional build steps), but the downside is that all your templates will have to be repeatedly passed in HTML with every page.
2. But really, you should precompile templates to a JS file
In this case, your build workflow will include an additional step when a certain command-line script goes over your templates/*.html
files and compiles each HTML template into a JavaScript function that “takes” data arguments and returns the “rendered” HTML string.
For example, a template like
<div class="editor-Gallery-media">
<div class="editor-Gallery-mediaItem"
style="background-image: url({{ thumbnail_url }});"
data-orientation="{{ orientation }}">
<div class="editor-Gallery-deleteMediaItem"><i class="icon-cross"></i></div>
<div class="editor-GrabboxMedia-orientation"></div>
<div class="editor-Gallery-mediaItemCaption">{{ caption }}</div>
</div>
</div>
would be compiled in a function like
this["st"]["Template"]["templates/editor/items/gallery_media.html"] = function(obj) {
obj || (obj = {});
var __t, __p = '', __e = _.escape;
with (obj) {
__p += '<div class="editor-Gallery-media">\n <div class="editor-Gallery-mediaItem"\n style="background-image: url(' +
((__t = ( thumbnail_url )) == null ? '' : __t) +
');"\n data-orientation="' +
((__t = ( orientation )) == null ? '' : __t) +
'">\n\n <div class="editor-Gallery-deleteMediaItem"><i class="icon-cross"></i></div>\n\n <div class="editor-GrabboxMedia-orientation"></div>\n\n <div class="editor-Gallery-mediaItemCaption">' +
((__t = ( caption )) == null ? '' : __t) +
'</div>\n </div>\n</div>';
}
return __p
};
which would be placed in a generated templates.js
file. This would be blazing fast.
Note that there is no overhead of parsing in this case, because parsing happens on your machine, during the build. The client uses pregenerated functions.
Such functions can be generated by most templating engines.
Don't parse the same template twice
Even if we bundle templates with the HTML (script type="text/template"
approach), your code still suffers from the fact it parses the same template every time it is being rendered. That means, for 20 identical items, the same template is parsed 20 times. Of course, parsing is nothing compared to fetching HTML, but it's something that is relatively straightforward to optimize away.
Instead, you should parse the template once, somehow cache the “parsed” version and figure out how to “apply” it to different models. As I advocated before, the natural way to it is to make template
build a function that corresponds to your template.
This is exactly what Underscore's _.template
does, so you want to look at its implementation (see also an annotated version):
_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
return template;
};
It parses your template, “converting” it to a JavaScript function, and returns this function. When you call this function with different models, no parsing occurs: it was only done once, during initialization.
Minor considerations
Your templating library should probably be agnostic of jQuery. After all, what you want to do is to transform a string + data
into another string
. That you insert this string into DOM is another matter, and doesn't seem to belong here.
On a minor note,
var string = new String();
is not idiomatic JavaScript; you never need to call String
, Array
or Object
constructors explicitly. If you want to set string
to an empty string, write var string = '';
. Still, this is unnecessary because you don't plan to use the empty value, so why bother assigning? Write var string;
and leave it undefined
until you assign it later. But string
is a really bad name for a variable. It is a template, right? So you should probably write
var template;
Having an undefined
represent a value that is not yet initialized/ready is okay.
If you're there for performance, doT claims to be fastest (although I'm not convinced their weird interpolation syntax justifies shaving off a few milliseconds).
To conclude, I advise you learn from Underscore, doT, mustache.js. It's fun and good for learning to reinvent something, but it's unwise to consider your implementation more performant before taking the time to test it.