Simple, maintainable templating with JavaScript

One of my principles of Maintainable JavaScript is to keep HTML out of JavaScript. The idea behind this principle is that all markup should be located in one place. It’s much easier to debug markup issues when you have only one place to check. I always cringe when I see code such as this:

function addItem(list, id, text){
    var item = document.createElement("li");
    item.innerHTML = "<a href=\"/view/" + id + "\">" + text + "</a>";  //ick
    item.id = "item" + id;
    list.appendChild(item);
}

Whenever I see HTML embedded inside of JavaScript like this, I foresee a time when there’s a markup issue and it takes far longer than it should to track down because you’re checking the templates when the real problem is in the JavaScript.

These days, there are some really excellent templating systems that work both in the browser and on the server, such as Mustache and Handlebars. Such templating systems allow all markup to live in the same template files while enabling rendering either on the client or on the server or both. There is a little bit of overhead to this in setup and preparation time, but ultimately the end result is a more maintainable system.

However, sometimes it’s just not possible or worthwhile to change to a completely new templating system. In these situations, I like to embed the template into the actual HTML itself. How do I do that without adding junk markup to the page that may or may not be used? I use a familiar but under-appreciated part of HTML: comments.

A lot of developers are unaware that comments are actually part of the DOM. Each comment is represented as a node in the DOM and can be manipulated just like any other node. You can get the text of any comment by using the nodeValue property. For example, consider a simple page:

<!DOCTYPE html>
<html>
    <body><!--Hello world!--></body>
</html>

You can grab the text inside of the comment via:

var commentText = document.body.firstChild.nodeValue;

The value of commentText is simply, “Hello world!”. So the DOM is kind enough to remove the opening and closing comment indicators. This, plus the fact that comments are completely innocuous within markup, make them the ideal place to put simple template strings.

Consider a dynamic list, one where you can add new items and the UI is instantly updated. In this case, I like to put the template comment as the first child of the <ul> or <ol> so its location isn’t affected by other changes:

<ul id="mylist"><!--<li id="item%s"><a href="/item/%s">%s</a></li>-->
    <li id="item1"><a href="/item/1">First item</a></li>
    <li id="item2"><a href="/item/2">Second item</a></li>
    <li id="item3"><a href="/item/3">Third item</a></li>
</ul>

When I need to add another item to the list, I just grab the template out of the comment and format it using a very simple sprintf() implementation:

/*
 * This function does not attempt to implement all of sprintf, just %s,
 * which is the only one that I ever use.
 */
function sprintf(text){
    var i=1, args=arguments;
    return text.replace(/%s/g, function(pattern){
        return (i < args.length) ? args[i++] : "";
    });
}</code>

This is a very minimal sprintf() implementation that only supports the use of %s for replacement. In practice, this is the only one I ever use, so I don’t bother with more complex handling. You may want to use a different format or function for doing the replacing – this is really just a matter of preference.

With this out of the way, I am left with a fairly simple way of adding a new item:

function addItem(list, id, text){
    var template = list.firstChild.nodeValue,
        result = sprintf(template, id, id, text),
        div = document.createElement("div");

    div.innerHTML = result;
    list.appendChild(div.firstChild);
}

This function retrieves the template text and formats it into result. Then, a new <div> is created as a container for the new DOM elements. The result is injected into the <div>, which creates the DOM elements, and then the result is added to the list.

Using this technique, your markup still lives in the exact same place, whether that be a PHP file or a Java servlet. The most important thing is that the HTML is not embedded inside of the JavaScript.

There are also very simple ways to augment this solution if it’s not quite right for you:

  • If you’re using YUI, you may want to use Y.substitute() instead of sprintf() function.
  • You may want to put the template into a <script> tag with a custom value for type (similar to Handlebars). You can retrieve the template text by using the text property.

This is, of course, a very simplistic example. If you need more complex functionality such as conditions and loops, you’ll probably want to go with a full templating solution.

Comments

  1. Andrey

    Interesting article. Thanks!
    Are there any benefits using comment node over script tag?
    ...

  2. Chris Nielsen

    A template is something that is necessary to the proper functioning of a piece of code.

    I'm not sure I'm convinced that it is a good idea to squirrel away necessary information inside a comment. In most scenarios, comments serve two purposes: to explain the code, and sometimes to disable code temporarily. Here you are suggesting a comment that adds a third purpose, one that will be unexpected and unfamiliar to fresh eyes looking at your code.

    What will your successor do, when they come to maintain this code, and see large hunks of HTML that are commented out? It would not be unreasonable for them to start deleting what they perceive as stale code. Why should they expect that other code would depend on a COMMENT to function correctly?

    Your initial problem is valid, I think: embedding template bits as strings in JavaScript is ugly and needs a proper solution. However, I don't think this is the right way to go either.

  3. Nicholas C. Zakas

    @Andrey - I don't think there are any measurable benefits. It's just a personal preference of mine. Your mileage may vary.

  4. Nicholas C. Zakas

    @Chris - Maintainability is about leaving repeatable and familiar patterns in the code. If the product uses comments to aid templating, then it becomes team knowledge. All patterns are unfamiliar when first encountered, so it's important that there's documentation and socialization of the pattern to get over that hump. Case in point: if you ever see this in a site, you'll now know exactly what it is. If you're worried about confusion with regular comments, you can always prefix the template, maybe just with the word TEMPLATE and then strip it out.

  5. Ronny Orbach

    Nicholas, creative idea and a well-writen article as always, but I'm afraid using comments for this matter provides no pros over using script type="something" like most js templating libraries do nowadays.
    So although your prefix suggestion might help mitigate Chris's valid points, why bother? What do I miss?

  6. Nicholas C. Zakas

    @fpiat - I used to do that until a colleague pointed out that having potentially unused markup in the document breaks progressive enhancement. Without display:none, you end up with all kinds of confusing content on the page. With comments, that content is never displayed regardless of CSS.

  7. twobee

    I like the concept! I agree though that using comments in a way they weren't intended will probably cause more issues. No reason you can't use things for new purposes of course! My one thought, was if a system is set up to remove comments from pages it serves up, it would break this solution. Not sure how common that might be, but just a thought.

    Always looking for new ideas and new uses for things though!

  8. Luke Smith

    I'm sure I've become biased by my history with Y.substitute, Y.Lang.sub, and similar object=>{placeholder} systems, but it seems to be more maintainable to use a substitution method based on values in an object hash. Markup changes could result in the order of %s tokens changing, resulting in a requirement to change both the markup template and code that uses the template. %s couples the template to the code that consumes it.

  9. Andrew Petersen

    What about using script tags with a custom type attribute? This gets around at least having visible blocks if CSS is disabled or not available.

    I really enjoy the idea of placing the template near the target, at least in one-off usages. I wonder how browsers deal with a script tag inside of a ?

  10. Nicholas C. Zakas

    @Luke - You got me. I was just looking for an excuse to put my sprintf() function into a blog post. I also prefer Y.substitute(), which is why I mentioned it towards the bottom.

    @Ronny,Ronny - Yes, that's another approach, which is why I mentioned it towards the bottom of the post. I still prefer comments for smaller chunks of HTML, but there's nothing preventing you from using script with a custom type.

  11. Daniel Stockman

    We've been using the commentText pattern for awhile at Zillow, it's pretty handy. One big potential pitfall is unescaped double-dashes inside the template, which ends the SGML comment too early and results in fragmentary non-commented output (usually an error). Make sure your wrapper component (if you use one, as we do) escapes the double-dashes of the content passed in.

    For example, <div><!-- I don't like em-dashes -- seriously! --></div> would be <div><!-- I don't like em-dashes \-\- seriously! --></div> when properly escaped. (Otherwise, the div would contain the text "seriously!" when rendered)

  12. jaseem abid

    Supplants provide a much better solution.
    Source : http://javascript.crockford...

    Define a teplates object and fill teplates into it.

    var templates = {
    box : "{title} {fullname}{viewcount}"
    };

    var box = templates.box.supplant(myobj); // my obj could be an ajax loaded json with data

    load box into dom

  13. Nicholas C. Zakas

    @Jaseem - I've actually found that to be horrible to use. The big problem is in needing to escape the JavaScript string, which means you don't get to write HTML as you would normally. Otherwise, you need to handcode it in your JavaScript, which leads back to the original maintainability problem. In fact, this pattern is exactly what led me to use the comment pattern I describe in this post.

  14. Louis

    This is interesting because I was planning to write something on accessing comments with JS... But this does remind me of James Padolsey's "JSHTML", which might be of interest here:

    http://james.padolsey.com/j...

    Yeah, James is like 20 years younger than me and can kick my ass any day of the week in anything related to JavaScript. :)

  15. Vishal

    The article is helpful, thanks for sharing. After looking at your 3 line sprintf implementation, I am loving JavaScript even more!

  16. ChrisB

    One should keep in mind that XML parsers are allowed to strip all comments from a document when building the DOM - so while this approach may work fine as long as you're using a content-type text/html, it might break when you switch to a text/xml content-type (in the future).
    It might even break today for example when you use AJAX/XMLHttpRequest to load document parts in the background and use text/xml for that.

  17. ChrisB
    Markup changes could result in the order of %s tokens changing, resulting in a requirement to change both the markup template and code that uses the template. %s couples the template to the code that consumes it.


    You could quite easily work around that, if you modify the sprintf function a little, so that it supports "argument swapping", similar to for example PHP's version of sprintf:
    function sprintf(text){
    var args=arguments;
    return text.replace(/%([0-9]+)s/g, function(pattern, num){
    return (num >= 1 && num < args.length) ? args[num] : pattern;
    });
    }
    alert(sprintf("The %1s contains %2s monkeys.\nThat&#039s a nice %1s full of %2s monkeys.\nAnd this: %3s is not replaced, because there&#039s no value for that.", "tree", 3));

    Added benefit, analog to PHP's version, is that you don't have to repeat arguments that you want to use more than once in your replacement.

Understanding JavaScript Promises E-book Cover

Demystify JavaScript promises with the e-book that explains not just concepts, but also real-world uses of promises.

Download the Free E-book!

The community edition of Understanding JavaScript Promises is a free download that arrives in minutes.