Simple External Templates With jQuery

If you've ever tried building a jQuery object from scratch then you've probably found that it is a little time consuming and probably a little ugly by the time you're finished. You could put additional HTML templates as hidden elements on the page but maybe you'd rather keep that extra mark up out of your page.

One option would be load a template from a website address and then make your changes once it arrives. The code would probably look something like this.

//create the container
var element = null;

//make the request for the HTML
$.ajax({
    url:"template.html",
    success:function(html) {
        element = $(html);
        //make changes here
    }
});

There isn't anything wrong with this code but it does put a bit of separation of your declaration of an element and the actual usage of the element. The problem is that you can't do the whole process at once since the AJAX call takes a little time and you have to rely on a call back before you have access to the object.

It would be nice if we could have immediate access to our jQuery object on an AJAX call, but not block anything else on the page.

Time For Some Javascript Magic ™

I'm not really sure what you would call the example below, but the general idea is to capture (and queue up) any actions that would be invoked on our jQuery object and then release them once the AJAX call has been finished.

Let's look at some code and see what it would look like (extra comments to try and explain the process)

At the time of writing this post I did not understand how to use arguments and the .apply to handle unknown argument counts. If you have an unknown argument count you should use those Javascript features instead.

/*
 * Arguments
 * $.template(url) - Pass in a string to a url to load 
 * $.template(params) - An object with parameters found below
 *   url: Path to the template resource (required)
 *   data: Same as the 'data' argument in the $.ajax call
 *   error: Same as the 'error' argument in the $.ajax call
 *   complete: Same as the 'complete' argument in the $.ajax call
 *   beforeUpdate: delegate(html) called just before the actual object is created
 *   afterUpdate: delegate(html, actual) called just after the actual object is created
 */

//creates a function to access a web template
jQuery.template = function(params) {

  //format the passed in parameters
  //check if this was only the resource url
  if (typeof(params) === "string") {
    params = { url:params };
  }

  //prepare the arguments passed into the class
  if (!$.isFunction(params.beforeUpdate)) { params.beforeUpdate = function() {}; }
  if (!$.isFunction(params.afterUpdate)) { params.afterUpdate = function() {}; }


  //create the object that handles the work
  var self = {

  //Properties
  //---------------------------------------------------------

    //handles forwarding methods onto the actual object
    container:null,

    //the actual jQuery object being created
    actual:null,

    //method calls that are waiting to be called
    queue:[],

  //Methods
  //---------------------------------------------------------

    //prepares the container for use
    setup:function() {

      //apply each of the methods
      self.container = $("<div/>");
      for(var item in self.container) {
        if (!$.isFunction(self.container[item])) { continue; }
        self.register(item);
      }

    },

    //handles creating method forwarding on the returned object
    register:function(method) {

      //create a method that handles routing the original method calls
      self.container[method] = 
        function(p0, p1, p2, p3, p4, p5, p6, p7, 
          p8, p9, p10, p11, p12, p13, p14) {

        //if the actual object has been called, just invoke
        if (self.container.actual) {
          return self.container.actual[method](
            p0, p1, p2, p3, p4, p5, p6, p7, 
            p8, p9, p10, p11, p12, p13, p14
            );
        }
        //otherwise, queue the request
        else {
          self.queue.push({
            action:method,
            params:[p0, p1, p2, p3, p4, p5, p6, p7, 
              p8, p9, p10, p11, p12, p13, p14]
          });

          //then return the temporary object
          return self.container;
        }

      };

    },

    //executes any queued commands and updates the jQuery object
    update:function(html) {

      //create the jQuery object
      self.container.actual = $(html);

      //then execute all of the waiting commands
      $.each(self.queue, function(i, arg) {
        self.container.actual[arg.action](
          arg.params[0], arg.params[1], arg.params[2], 
          arg.params[3], arg.params[4], arg.params[5], 
          arg.params[6], arg.params[7], arg.params[8], 
          arg.params[9], arg.params[10], arg.params[11], 
          arg.params[12], arg.params[13], arg.params[14]
          );
      });

    },

    //starts the ajax request to download the template HTML
    download:function() {

      //starts downloading content
      $.ajax({

        //parameters for the template request
        url:params.url,
        data:params.data,
        dataType:"html",

        //performs the catch up work for the ajax
        success:function(html) {
          //** Optional: Uncomment 'setTimeout' to simulate a delay 
          //setTimeout(function() {
          params.beforeUpdate(html);
          self.update(html);
          params.afterUpdate(html, self.container.actual);
          //}, 2000);
        },

        //additional handling of the request
        error:params.error,
        complete:params.complete
      });

    },

  //Initalization
  //---------------------------------------------------------

    //setup the object to work
    init:function() {

      //prepare the temporary container
      self.setup();

      //start downloading the content
      self.download();
    }

  };

  //prepare the the template code
  self.init();

  //return the custom container to forward method calls
  return self.container;

};

This code allows you to have direct access to a jQuery object even before it has finished loading the content from the server. So, instead of using a callback we can write our jQuery like we normally would.

//note: the first command is all one long chain with a comments
//between functions to explain a bit more

//create the object and immediately apply changes
var template = $.template("template.txt")
  .css({"color":"#f00", "width":"200", "background":"#333"})
  .animate({width:800, height:900}, 3000)

  //you can even append and appendTo the object in advance
  .appendTo($("#container"))

  //or search for and update child elements
  .find(".title").text("howdy")

  //and parent elements
  .parent().css({"background":"#00f"});

//you can also still access the template from the 
//assigned variable
template.find("div").click(function() {
  alert('clicked');
});

//even if if you call it much later, it still works
setTimeout(function() {
  template.css({"color":"#0f0"});
}, 10000);

What Is Going On Here?

As I mentioned before the real problem is that the jQuery object we create isn't ready as soon as we want to assign to it. Because of that we have to use a callback to resume the work. However, the cool thing about dynamic languages is that we can override anything we want.

In this example we start by creating a container that has all the same functions as a typical jQuery object but has one slight modification -- all the methods are overridden and placed into a queue. Once our AJAX call has completed we run an update command that executes everything we have saved against our new jQuery object instead of the one that received the calls to begin with -- madness!

After we've caught up and our actual object is created we can stop saving actions to the queue and instead just invoke them immediately. I nicknamed this method forwarding for the sake of having a cool, buzzword sounding sort of name but if anyone knows what this is really called, please tell me.

It is worth noting that this is only going to work with functions that return the jQuery object (at least until the 'real' object is created). The reason is that since we're just capturing methods and saving them for later then we don't know what the actual return type is. Needless to say, using a property probably won't be accurate either (since we can't intercept the request for the property like we would with a method)

In any case, this might simplify the next time you need to download content in jQuery but you don't want to break up your functionality.

January 14, 2010

Simple External Templates With jQuery

Example of avoiding callbacks when using external HTML templates.