Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I've just started writing my own little templating library in JavaScript, because 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?"

If you are able to follow conventions, you would probably run into less issues.

So I ended up with this little script with the upper quote in my head. The convention, here is that data needs an object and its keys must be the same like in template, mentioned between those delimiter-brackets "{{key}}"

function Template(url, data) {

    this.url = url;
    this.data = data;

    this.delimiter = ['{{','}}'];


    this.load = function() {

        var string = new String();

        var http = null;
        if ( window.XMLHttpRequest ) {
            http = new XMLHttpRequest();
        } else {
            http = new ActiveXObject("Microsoft.XMLHTTP");
        }

        http.open('GET', url, false);
        http.onreadystatechange = function() {
            if ( http.readyState === 4 ) {
                string = http.response;
            }
        };
        http.send(null);

        this.template = string;
        return this;

    };

    this.exchange = function() {

        for ( var key in this.data ) {

            var bracketedKey = this.delimiter[0] + key + this.delimiter[1];
            var indexOfKey = this.template.indexOf(bracketedKey);
            var lengthOfKey = bracketedKey.length;

            var exchange = this.template.substring(indexOfKey, indexOfKey+lengthOfKey);

            this.template = this.template.replace(exchange, this.data[key]);

        }

        return this;

    };

    this.build = function(selector) {

        this.load();
        this.exchange();

        var div = document.createElement('div');
        div.innerHTML = this.template;
        div = div.firstChild;

        if ( typeof selector === 'string' ) {
            document.querySelector(selector).appendChild(div);
        } else {
            return div;
        }

    };

}

But now I've started thinking about the fact, that other libraries include more code than mine and there must be a reason for it, mustn't it?

There also a few more things that are coming to my head, like:

  1. Less code = better performance?

  2. Why do so many people relate their projects with this templating library? (Matter of performance => that would refute my first point)

It'd be cool if someone could explain me a few things about this topic.

  1. Am I right with my mindset?
  2. When I am right, is there something that I could improve, or even when I am wrong?
  3. If you would recommend a library to me, what is your reason?
share|improve this question

5 Answers 5

up vote 20 down vote accepted

... 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.

share|improve this answer
    
wow, I'd never expected this huge amount of helpful information! thank you, I appreciate it! –  Jon Lamer Jan 3 at 3:10

From a once over:

  • delimiter should be delimiters
  • var string = new String(); could be var string = '';, it raises less questions
  • On a general note, I think it is bad design to AJAX each template..
  • Worse, you do not cache AJAX results, using a template n times will trigger n AJAX calls
  • The exchange function could be more elegant by using the split join trick:
this.exchange = function() {
  for ( var key in this.data ) {
    var token = this.delimiter[0] + key + this.delimiter[1];
    this.template = this.template.split( token ).join( this.data[key] );
  }
};
  • Note that the way template gets changed stops re-use of the template, which really goes against what a template is meant for.
  • You keep returning this, but you do not chain your function calls
  • I am not sure how your code works? Update : now I know how it works, a synchronous call per template function call is a bad idea.

On a side note, I use the following for my personal templating needs:

function RNG( array )
{ //Return a random entry from the provided array, all hail the RNG
  return array[Math.floor( Math.random() * array.length )];
}  

function fillTemplate( s )
{ //Replace ~ with further provided arguments, those might be arrays
  for( var i = 1 ; i < arguments.length ; i++ )
    s = s.replace( "~" , arguments[i].pop?RNG( arguments[i] ):arguments[i] );
  return s;
}   

var template  = "~ hits you in the ~ for ~ damage points";
var bodyParts = ['head','chest','groins'];
var output = fillTemplate( template , 'The coding troll' , bodyParts , 3.14 );
share|improve this answer
    
Thanks for your review, you've mentioned many useful things :) –  Jon Lamer Jan 2 at 18:47
    
So when you say, it's bad "to ajax each template", there must be a better way to deal with it, mustn't it? But if I use AJAX, I should call the exchange function, when the ready state change, so within the in the http.onreadystatechange function, whichs allows me to make the ajax call asynchronous again or am I totally wrong now? –  Jon Lamer Jan 2 at 18:57
1  
I meant that this will slow down performance, I would personally get the template with the initial JS script. –  konijn Jan 2 at 19:11
    
okay, I'm just a beginner, so any opinion is welcome to me :) –  Jon Lamer Jan 2 at 20:08
    
this.template = string; seems pointless, since string will not be set yet -- Jon's using synchronous XMLHTTPRequest. So it kinda works. (Not a good idea though.) –  Dan Jan 2 at 21:27

Different templating libraries offer different functions: including helper functions/logic (eg foreach of an object/array and if statements), precompilation, and DOM binding.

In terms of raw performance, libraries that precompile the templates will often be fastest, as you're serving a special javascript file with contents along the lines of function(context) { return "name: " + context.name} from template "name: {{name}}" and it won't need to be compiled on the clientside.

Anyway your template engine seems more of a simple formatter and doesn't have much functionality baked in. Your exchange function also has a rather obvious bug in that it will miss multiple keys in keys. For example try running it on "{{name}} {{name}}" - it will only replace the first occurance of {{name}}.

Update here's a common way of doing a regex format (I stole most of this regexp from mootools)

this.exchange = function() {
    var data = this.data;
    this.template = this.template.replace(/\\?\{\{([^{}]+)\}\}/g, function(match, name){//reasonably complicated regex replace matches a string surrounded by {{key}}
        return data[name] || match;//if the object has property in {{key}} replace it otherwise don't change
    });

    return this;
};
share|improve this answer
    
okay, this is helpful, and the multiple occurance of an string like you mentioned, is a nice feature, which I haven't thought of, but probably because I'm in no need of that functionality. so your advise to me would probably be, that I should send the data to file (for example a php script) directly? –  Jon Lamer Jan 2 at 18:09
    
Your approach is absolutely fine for formatting a string however you may want to consider a regexp replace for your exchange function. I'll update my answer a bit later today with an example of how to achieve that –  megawac Jan 2 at 18:30
    
Thanks, appreciate it! :) –  Jon Lamer Jan 2 at 18:44
    
Besides {{name}} {{name}} not working, you can also place substitution markers inside the arguments to the template, which is usually undesirable. Your solution solves this too. –  icktoofay Jan 3 at 6:41

In a good implementation you pay for a lot of functionality only if you are actually using that functionality - so a lot of functionality doesn't necessarily mean slow. In fact, more often than not, implementations with small amount of functionality are very sloppy and not fast. Consider underscore using a with-statement. This slows down performance unspeakably but is done because it takes no effort from the implementer. Implementing variable referencing properly without using with would take a lot more code.

Overall an optimized library will always have a lot of more code than an unoptimized one. It's a simple physical law.

share|improve this answer

I wanted to have templates in JavaScript too, and managed to create something even simpler than your approach:

String.prototype.replaceAll = function(find, replace) {
    return this.split(find).join(replace);
};

String.prototype.insertVariables = function (nameValuePairs) {
    var string = this.valueOf();

    for (var key in nameValuePairs) {
        if (nameValuePairs.hasOwnProperty(key)) {
            string = string.replaceAll('{{' + key + '}}', nameValuePairs[key]);
        }
    }

    return string;
};

And you use it like this:

'some {{i}} variable'.insertVariables({i: 'inserted'}); // some inserted variable
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.