Infuser – a Template Loader

{Cross-posted on Fresh Brewed Code}

In my last post I discussed a simple jQuery plugin called “Traffic Cop” – which prevents duplicate AJAX requests for the same resource while an identical request is already running. The reason I wrote Traffic Cop was to support a project called “infuser“. Infuser was born out of frustrations I’ve had with where other template engines/binding frameworks expect templates to reside (quite often SCRIPT tags). Requiring template fragments to reside in SCRIPT tags is a recipe for epic fail when it comes to maintenance & readability (not to mention the lack of IDE syntax support when writing markup inside a SCRIPT tag). I was surprised to find that no one had written a “generic-ized” utility that could interface with a given template engine and handle the fetching of templates from a remote resource. So, borrowing some ideas from Dave Ward, I initially wrote an external template engine for Knockout.js. The Knockout 1.3 beta included significant changes to templating, so as part of my effort to update the plugin for 1.3, I began to separate out utilities that could be re-used on their own or with other frameworks.

So – what does infuser do?

  • Asynchronously (or synchronously, if you must) retrieves templates and stores them locally in the client once retrieved. Default storage options are in-memory (hash) or in SCRIPT tags (since some engines prefer that). You can write a different storage provider if necessary.
  • Provides hooks for a callback to be invoked after you “get” a template, OR if you use the “infuse” method, you have more extensive control about how the template is rendered (if it’s data-driven) and attached to the DOM – including pre- and post-attach options and a render override.
  • Provides a hook for telling infuser how your preferred template engine handles binding a model to a template (making it possible for infuser to work with several major template engines).

But wait, why is this useful?

  • First, you don’t have to put your template content in your main document (or duplicate it in multiple documents, God forbid)
  • If your template engine expects your templates to be in SCRIPT tags, you don’t have to lose syntax highlighting, etc. in your IDE – you can still place them in their own files with a valid markup extension
  • Dovetailing the above point, it can aid in re-usability and maintainability via separation of concerns
  • Infuser takes advantage of not only it’s own in-memory storage (once a template has been retrieved, it’s cached), but your browser’s cache is also leveraged (assuming it’s not disabled and assuming the server is returning a 304 response)
  • It abstracts away infrastructure/ceremonial code involved in retrieving, binding and rendering templates to the DOM

Usage

Infuser provides two main ways to interact with templates. The first is the bare-bones “get” method. You provide it two arguments: a template name, and a callback to be invoked when the template is retrieved (the callback takes the retrieved template as it’s only argument). The second way is via the “infuse” method, which provides a much more sophisticated set of functionality. Let’s look at examples of both:

“get(templateId, callback)”

Let’s assume we have a static template that we want to load when a button is clicked. In the following example, we’re telling infuser to look for templates in a “templates” directory (relative to the current document) that have a prefix of “tmpl_” and a file extension of “.html”. Then, we’re binding the “#btnTemplate” button’s click event to a function that gets the “HellowWorld” template. In our callback, we’re hiding the original content and removing it, then fading in the new content:

$(function(){
    infuser.config.templateUrl= "./templates",// look for templates in a "templates" directory (relative to this page)
    infuser.config.templateSuffix= ".html"    // look for templates with an ".html" suffix (this is a default value, just showing as an example)
    infuser.config.templatePrefix = "tmpl_";  // look for templates with a "tmpl_" prefix
    // Now - wire up a click event handler that asynchronously fetches a static html file and appends it to an element
    $('#btnTemplate').click(function(){
        infuser.get("HelloWorld", function(template){
            var tgt = $("#target");
            tgt.hide().children().remove();
            tgt.append($(template)).fadeIn();
        })
    });
});

Very straightforward stuff. What about data-driven templates? In this example we’re retrieving a jQuery template, binding it and then rendering it:

// Pulling a jquery-tmpl
var model = { names: ["Ronald", "George", "William", "Richard"] };
$(function(){
    $('#btnTemplate').click(function(){
        infuser.config.templateUrl= "./templates",
        infuser.get("Example", function(template){
            var tgt = $("#target");
            tgt.hide().children().remove();
            var div = $("<div/>");
            $.tmpl(template, model).appendTo(div);
            tgt.append(div).fadeIn();
        })
    });
});

So – while it’s straightforward, it’s in danger of turning into a lot of ceremonial code – especially if you have several templates to pull in for a given document. Plus, what if you always wanted to take the approach of “hiding, then removing” the target content, and then fading in the new template? Writing that each time would be overkill. That’s where “infuse()” comes into play:

“infuse(templateId, options)”

The “infuse” method takes two arguments: the template id/name, and an options hash that has the following members:

  • preRender: a method with a signature of (target, template), where “target” is a selector used to target n-number of DOM elements where the template is to be rendered, and “template” is the retrieved template content prior to any “binding” (if a template engine is involved).
  • render: method with a signature of (target, template), where “target” is a selector used to target n-number of DOM elements where the template is to be rendered, and “template” is the completed template (i.e.- if you’re using a template engine, this is after the model has been bound to it).
  • postRender: method with a signature of (target), where “target” is a selector used to target n-number of DOM elements where the template has been rendered.
  • target: can be a function or a string. If it’s a function, it takes the template id as its only argument and should return – based on whatever transformation logic you desire – a selector (string). (The default implementation is a function that takes the template id and returns it prefixed with “#”, making it a selector that targets a DOM element with an id matching the template name.) If it’s not a function, then the value provided is treated as a selector. Typical usage is to define a default function used for most cases, and then override it with a specific value in the options hash when needed.
  • loadingTemplate: an object containing a content member (the HTML to display while loading a template), plus transitionIn and transitionOut methods that can be used to handle the appearance of the loading template (i.e.- you can fade it in and fade it out, etc.).
  • bindingInstruction: a method with a signature of (template, model) where “template” is the content retrieved from the server and model is a JavaScript object that will be bound to the template. For example, to tell infuser you are using jQuery templates, then you’d do this: infuser.defaults.bindingInstruction = function(template, model) { return $.tmpl(template, model); };. It defaults to simply return the template content.
  • useLoadingTemplate: a boolean indicating if the loadingTemplate member should be used when loading templates.

Whoa! That’s a lot of options, you’re probably thinking. The good news is that basic defaults are provided for each, so you only have to provide what you’re overriding. Let’s take the jQuery template example from above and re-write it using “infuse”:

$(function(){
    infuser.config.templateUrl= "./templates";
    infuser.defaults = $.extend({}, infuser.defaults, {
        bindingInstruction: function(template, model) {
            return $.tmpl(template, model);
        },
        preRender: function(target, template) {
            $(target).hide().children().remove();
        },
        render: function(target, template) {
            $(target).append(template).fadeIn();
        }
    });

    $('#btnTemplate').click(function(){
        infuser.infuse("Example",{ target: "#target", model: model });
    });
});

We could also easily make the click event handler even shorter if we renamed our target element’s id to “Example” instead of “target”:

$('#btnTemplate').click(function(){
    infuser.infuse("Example",{ model: model });
});

It’s important to note that the defaults we set on the “infuser.defaults” object will now apply to any template rendering on the page, so we’ve reduced the “noise” significantly. You can override any of the defaults at any point by providing a different implementation of it in the options hash (second arg to “infuse”). For example, if your heart was really set on sliding a template down (as opposed to the default fadeIn() in the above example) then you could do the following:

$('#btnTemplate').click(function(){
    infuser.infuse("Example",{
        model: model,
        render: function(target, template) {
            $(target).append(template).slideDown('slow');
        }
    });
});

Wrapping Things Up

Infuser provides a getSync call for synchronous retrieval, but the “infuse” call is always async (I have no intention of creating a synchronous version, and may remove the getSync call at some point, since synchronous AJAX is a bad idea all around). “Traffic Cop” is used internally by infuser to prevent multiple simultaneous requests for the same template. So far I’ve used infuser in conjunction with jQuery templates, Underscore templates & static content. In theory, infuser should work with any template engine that can be abstracted behind the “bindingInstruction(template, model)” call. If you want to see more usage demonstration, please check out the examples included in the github repository. Your feedback (and pull requests) are welcome!

 

Tags: , , , , , ,

  • Nope

    i want to do some work on the template during preRender() [make some mods to it in code]… but preRender doesn’t let me actually do that. would be a nice add

    • http://ifandelse.com Jim Cowart

      Actually, the template is passed in as an argument to both preRender and render. In the preRender call, the template is the un-bound template source that was just pulled in from the server. In render, the template is the bound-to-model final product.

  • Steffen

    Jim,
    I’m having issues with an outdated cache when I use the ko external template engine. It looks like that I somehow need to flush the cache if a new version of the template is available, bug fix or newer version. Is there an easy way to do that? Can i tell infuser to clear the cache if I detect that the templates are old?