jQuery Templates Proposal – dissecting plugin code

As you’ve probably heard, it was announced during the second-day MIX keynote that Microsoft is going to contribute to jQuery project. As a part of the contribution templating proposal was written and experimental plugin code is already available.

Having spent some time playing with the plugin, I will try in this post to dissect its code and look at how it works internally.

But why would you need to understand how it works in the first place?

Let me present my own motives (unfortunately I do not have any others):

1) Just from pure curiosity. It is a great opportunity to learn at least one templating solution from the ground up while it is simple and extremely small in its infancy. You have a rare ability to have your own pet templating plugin to bring up. Of course it is jQuery and Microsoft teams who will actually feed it, but the pleasure to see it growing and maturing will be all yours.

2) In my opinion, if you do not understand how some JavaScript framework or tool works - you must be very brave person to use it in any application that has a chance to be released to production some day. And this plugin looks like very promising templating solution to employ.

Convinced?

Then, just for starters let’s articulate the ultimate goal of the code which we are going to digest. According to the proposal we must be able to define HTML markup on our page interspersed with inline expressions and code blocks using following syntax:

    <script id="template" type="text/html">	    
	<div> 
	    Title: <span class='title'>{%= title %}</span>
    	    <hr />
		{%= 'Pages: ' + pages + '; Year: ' + year %}
	    <div class='price'>
		price: {%= price %}
	    </div>
	    Reviews: 
	    <ul>
		{% for (var i=0,review; review=reviews[i++];) { %}
		    <li class='review'>
			{%= review %}
		    </li>
		{% } %}
	    </ul>
	</div> 
    </script>

We should be able to bind either single data object or collection (JavaScript array instance) to the template and have it rendered whenever we like in any container we will specify.

I will use following sample data for the examples shown in this post:

    var books = [
	  { title: "JavaScript the Bad Parts", year: 2010, pages: 1760, price: 77.99,
	      reviews: ["Awesome", "Fantastic", "Always knew there were no good parts"]
	  },
          { title: "jQuery for plumbers", year: 2010, pages: 150, price: 10.75,
              reviews: ["The missing manual I looked for for years", "Many thanks", "What the ...!?"]
          },
	  { title: "jQuery Templates 4.0 Reference Guide",
	      year: 2012, pages: 1540, price: 40.99, reviews: []
	  }
        ];

With the template markup defined and data available we can use following command to render the template and  insert generated HTML into the DOM tree:

jQuery(".container").append("#template", books);

or as one of the available alternatives:

jQuery("#template").render(books).appendTo(".container");

You can click here to see the results of template rendering.

Now, we are ready to look at the plugin code. It is available at http://github.com/nje/jquery-tmpl.

Let’s define terminology that will be used throughout the post. I will use the term jQuery wrapped set or just wrapped set to refer to the set of elements selected using jQuery function and upon which we can operate using jQuery commands. Otherwise when jQuery function or jQuery object terms are used – jQuery function itself is meant, that is the object containing static utility methods like jQuery.trim(). Ok? Great.

I am going to decompose the plugin into smaller distinctive parts to digest them bit by bit. The general outline, on which we will elaborate till the end of the post, looks like the following:

(function(jQuery) {
	// 1) prepare environment

	// 2)extend wrapped element set methods
	jQuery.fn.extend({
		// new/overridden wrapped set methods
	});

	// 3) extend static utility members
	jQuery.extend({
		// additional utility members needed
		// for the templating functionality
	});
})(jQuery);

It is not at all that scary so far. Following jQuery guidelines, plugin implementation is wrapped in anonymous function. The function is executed immediately and its only parameter is the jQuery function itself.  The name of the parameter is also jQuery so we do not take advantage of the ability to define custom alias for the jQuery method – one of the purposes with which such anonymous function is usually used:

(function ($) {
    // here we can safely use $ alias in place of jQuery
})(jQuery);

But, even without the benefit of a shorter alias, having such function helps us to prevent polluting global namespace with our local variables defined within plugin body.

The code of the function can be separated into three major parts:

1) common variables’ declarations;

2) extending jQuery.fn object (i.e. prototype object for jQuery wrapped element set) with plugin-specific methods;

3) extending jQuery function itself with static utility members specific to the templating functionality

Prepare environment

The first section, which I designated as "prepare environment", is actually just a declaration of two variables: oldManip and htmlExpr: 

	// Override the DOM manipulation function
	var oldManip = jQuery.fn.domManip,
	htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$/;

oldManip variable serves as a backup for original jQuery wrapped set method named domManip(). We will talk about the method later. For now it is sufficient to say that within the plugin implementation the method will be overridden, but we still need to call its original version. So we have to store the reference to the original domManip() implementation in the variable named oldManip.

htmlExpr variable is a regular expression instance which serves as a test whether the string contains HTML markup or not:

htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$/

Maybe the expression is not entirely bullet proof, but hey, if it is ok for John Resig we will use it without a moment's hesitation.

These simple steps complete our environment initialization.

Now we can move on to the second section of the template. It is where we are going to extend jQuery wrapped element set with our own methods.

Extending jQuery wrapped set

The only new method the API introduces named render(). Here we need to clarify what our instance of jQuery wrapped set refers to when the render() method is called. Is it a set of templates or a set of containers where the template will be rendered? That is who is the subject of the render() method? It is an important point and may not be immediately clear.

The syntax defined in the proposal is:

	jQuery("#template").render(data, options);

That is, according to the API, it is the template that we are rendering and the wrapped set refers to the set of templates (or, in most cases, just a single one) that we need to render using specified data and options. Later we can add the rendered template to the container were we want the resulting HTML to come to rest.

The render() method is defined as following:

	render: function( data, options ) {
		return this.map(function(i, tmpl){
			return jQuery.render( tmpl, data, options );
		});
	},

We do not have a chance at this point to dive into internals of the template rendering. It turned out, that all the actual logic is delegated to the new static utility method which is also named render(). But I promise to get to the bottom of it in due course.

For now, just take into account, that when the set of templates is rendered, each template is passed to the static utility method jQuery.render() along with the given data and options.

There is another jQuery wrapped set method which is introduced in this section of the plugin. The one we’ve touched briefly earlier. The method is named domManip() and method with such name already exists. In order to understand why we need to override it, we have to understand what its original version does.

The domManip() is a generic helper method which is used internally by several DOM manipulation methods, namely append(), prepend(), before() and after(). Majority of the work these methods need to perform before the DOM tree can actually be modified is the same regardless of the particular DOM action:

  1. Generate document fragment node (nodeType == 11) which will contain all the nodes that should be inserted into the DOM.
  2. Calculate valid container for the fragment. For example if we use append() method to add a <tr /> element to a table, then we have to use <tbody /> element of this table as a container for the row. Also, if we have a set of containers, then we also need to clone the fragment to stamp its content into each and every container of the set.
  3. Take care of browsers’ bugs related to cloning the document fragment (ok, it is finally WebKit instead of long-suffering IE which is to blame in this case).

This chunk of common code is extracted into separate method named domManip(). It takes a callback method as its third parameter to carry out DOM modifications specific to the particular public method of a wrapped set.  As always it is easier just to look into the code. For example, the append() method definition looks like the following:

	append: function() {
		return this.domManip(arguments, true, function( elem ) {
			if ( this.nodeType === 1 ) {
				this.appendChild( elem );
			}
		});
	},

As you can see, the method internally just calls the domManip() method with anonymous function passed as its third argument. The function is a callback method which domManip() invokes to actually perform required DOM action after the document fragment was generated and proper container for the action selected. In this case the action is as simple as appendChild.

Now we know what domManip() method does. But it still is not clear why we need to override it? The answer is - for the sake of convenience that templating API provides to the end user. The authors of the jQuery templates proposal decided that it may be useful if we could revert the point of view when we want to render the template and start our query not from the template but from the container of the resulting HTML. Thus, if we have a wrapped set of container element(s) and we need to render template somewhere within, before or after the element we can write something like:

          jQuery(".myContainer").append("#template", data, options); 

Voila. The template specified by “#tempate” selector is appended to  the containers specified by “.myConainer” selector using the data and options passed as second and third arguments respectively.

The same is true for the three other methods which consume domManip() internally: prepend(), before() and after().

To make this magic happen we need to hook up to the domManip() method and following code does just that:

    // This will allow us to do: .append( "template", dataObject )
    domManip: function( args ) {
	    // This appears to be a bug in the appendTo, etc. implementation
	    // it should be doing .call() instead of .apply(). See #6227
	    if ( args.length > 1 && args[0].nodeType ) {
		    arguments[0] = [ jQuery.makeArray(args) ];
	    }

	    if ( args.length >= 2 && typeof args[0] === "string" && typeof args[1] !== "string" ) {
		    arguments[0] = [ jQuery.render( args[0], args[1], args[2] ) ];
	    }
    	
	    return oldManip.apply( this, arguments );
    }

The first three lines of code have no direct relation to the plugin functionality. It just so happened that at the time when the plugin has been written the bug which affects domManip() method was discovered and fixed.

These three lines of code in our new domManip() method is just a temporary patch for the bug which is already fixed in jQuery repository.

If you are curious, what bug the code fixes, then we need to look at the definition of the following set of jQuery commands: appendTo, prependTo, insertBefore, insertAfter. They are all defined using exactly the same code (parameterized using method name) mapped to the so called “original” methods.

Just look at the following mappings:

{
	appendTo: "append",
	prependTo: "prepend",
	insertBefore: "before",
	insertAfter: "after",
	replaceAll: "replaceWith"
}

“Oiginal” methods’ names (in terms of the code) are values of the properties. When, for example, appendTo() method is called, some intermediate code just replaces the subject of the action with the object of the action (the wrapped set which should be appended with the the wrapped set which is appended to) and then the original method (append() in this case) is invoked.

That is instead of

jQuery(".myElements").appendTo(".myContainer");

we will have

jQuery(".myContainer").append(jQuery(".myElements"));

Isn’t it clever? Without writing a single line of code we can introduce a whole bunch of new methods. Ok, it is not entirely true. There actually are several lines of code to wrap this functionality. One of them, the one that performs the call looks like the following:

jQuery.fn[ original ].apply( jQuery(insert[i]), elems );

And it is where the bug was hidden. Since the apply() method is used here, the array of elements passed as a parameter to the original method will be scattered across separate arguments . That is the append() method, for example, will be called as

append(elem1, elem2, elem3, …, elemN)

while it should be

append([elem1, elem2, elem3, …, elemN]);

To fix the issue, the first three lines of the overridden domManip() method just gather all the scattered arguments back into one array wrapped in another array:

    if ( args.length > 1 && args[0].nodeType ) {
	    arguments[0] = [ jQuery.makeArray(args) ];
    }

The outer array is needed because at the end of the new domManip() method, the array is consumed by apply() method once the original version of domManip() is called:

return oldManip.apply( this, arguments );

But, as I said, this fix does not have any relation to the code of the plugin. Just wanted to  ensure that you understand the mechanics under the hood (even if it is just a bug fix).

Now, eventually, lets concentrate on the code within the domManip() which enables templating API:

if ( args.length >= 2 && typeof args[0] === "string" && typeof args[1] !== "string" ) {
    arguments[0] = [ jQuery.render( args[0], args[1], args[2] ) ];
}

As you can see it is really straightforward. Since the domManip() receives all the arguments passed to one of the methods in question (append, prepend, before or after), then we can deduce that template rendering was intended by the number of the arguments (>=2) and their respective types. The first one should be either selector or HTML template string, and the second one must be a data object - array or object but definitely not a string.

Once again, as in the code of the instance render() method for a wrapped set, we see that the template rendering is performed using static utility method of jQuery function:

arguments[0] = [ jQuery.render( args[0], args[1], args[2] ) ];

and then, as I showed earlier, original version of oldManip() is called which now receives already rendered template:

return oldManip.apply( this, arguments );

That’s all for the second part of our journey through the plugin code.

I hope you are not tired yet, since now begins the part of the template that actually does something. And it is the joy of analyzing this part I wanted to share with you when started writing this post.

Extending jQuery function

Since this section of the code is relatively verbose (though still under 150 lines) we are going to dissect it the same way we did for the code of the entire plugin:

	jQuery.extend({
		render: function( tmpl, data, options ) {
			// instantiate template if it does not exist
			// and render it
		},
		
		// pre-built template functions' cache
		templates: {},

		// helper methods for template rendering 
		tmplFn: {
			html: function() { /* ... */ },
			text: function() { /* ... */ },
		},

		_: null, // string builder 
		
		// generate template function
		tmpl: function tmpl(str, data, i, options) {
			// ...
		}
	});

First, let’s define what “template” and “rendering” terms mean when used in the code and comments. If you still remember, our goal is to parse HTML markup that represents our template. The result of the parsing should be suitable for data binding. It would be better if we could reuse the template later without overhead of parsing initial HTML markup of the template again and again.

The template solves this task by generating (we will also use terms “compiling” or “building”) JavaScript function  from the provided HTML markup. The function takes jQuery method and context object as its parameters and returns an array of DOM elements.

The context object has the following properties: data, dataItem, index and options: data is just that – data passed to the template rendering function; dataItem –  item of the data collection which is currently being rendered; index – the index of the current data item; options – object containing custom parameters passed to the template. By default two events can be specified using the options object: rendering and rendered. We will look at the code that makes this happen later. For now it is important to note that data object can be either an array or a single object. In case single object is passed to the template rendering function, then data and dataItem properties of the context will reference the same object. index property in this case contains 0.

Plugin extends jQuery function with utility method named tmpl() for the purpose of generating reusable function (pre-built template). The code of the function is shown below:

    tmpl: function tmpl(str, data, i, options) {
	    // Generate a reusable function that will serve as a template
	    // generator (and which will be cached).

	    var fn = new Function("jQuery","$context",
		    "var $=jQuery,$data=$context.dataItem,$i=$context.index,_=$._=[];_.context=$context;" +

		    // Introduce the data as local variables using with(){}
		    "with($.tmplFn){with($data){_.push('" +

		    // Convert the template into pure JavaScript
		    str.replace(/[\r\t\n]/g, " ")
			    // protect single quotes that are within expressions
			    .replace(/'(?=[^%]*%})/g,"\t")
			    // escape other single quotes
			    .split("'").join("\\'")
			    // put back protected quotes
			    .split("\t").join("'")
			    // convert inline expressions into inline parameters
			    .replace(/{%=(.+?)%}/g, "',($1),'")
			    // convert start of code blocks into end of push()
			    .split("{%").join("');")
			    // and end of code blocks into start of push()
			    .split("%}").join("_.push('")

		    + "');}}return $(_.join('')).get();");

	    return data ? fn( jQuery, { data: null, dataItem: data, index: i, options: options } ) : fn;
    }

Are you scared? You should not be. All we need is to debug the method using the example template and data presented at the beginning of the post.

As you can see, the method creates JavaScript function by means of Function() constructor. When the tmpl() method is called, its first argument contains string with HTML markup. All the other arguments are provided for “currying”. If you are not familiar with the term then do not worry, we will talk about it later.

Now we are interested only in the first parameter – str which contains markup of the template and is used to compose the body of the compiled template (anonymous function).

To begin with, let’s see how the function would look without any markup specified:

	var fn = new Function("jQuery","$context",
		"var $=jQuery,$data=$context.dataItem,$i=$context.index,_=$._=[];_.context=$context;" +
		// Introduce the data as local variables using with(){}
		"with($.tmplFn){with($data){_.push('" +
			/// parsed markup goes here ...
		"');}}return $(_.join('')).get();");

It is not particularly intuitive, but the code actually results in the following simple function definition:

	var fn = function(jQuery, $context) {
				var $ = jQuery, 
					$data = $context.dataItem,
					$i = $context.index,
					_ = $._ =[]; 
					_.context = $context;
				
				with ($.tmplFn) { 
					with ($data) {
						_.push('');
					}
				}
				return $(_.join('')).get();
			}

Much better, and nobody is frightened. But several comments are still necessary. The function takes jQuery and context objects as its parameters. jQuery is stored in variable $. context object properties are distributed into variables $data and $i. Another important variable is the one named _ (underscore). It is initialized with an instance of array and copied to the property of jQuery object of the same name _(underscore). It is this array that will accumulate parts of HTML markup we are about to parse.

The second important part of the method is the two successive with statements. As a result of these statements objects jQuery.tmplFn and $data are added to the front of the scope chain and, since identifiers are resolved against the scope chain, we can address the members of jQuery.tmplFn and $data objects as if they were just local variables.  If, for example, our data item had property named title then we could simply write:

				with ($.tmplFn) { 
					with ($data) {
						html(title);
					}
				}

html() method is defined on jQuery.tmplFn object which is also on the scope chain.

If you are still with me, my congratulations, since we’ve approached the culmination of our research and going to decipher how it is possible to get reusable templating function from HTML string.  We’ve just learned that parts of the resulting template will be stored in the array named _ (underscore). Let’s track how it is being filled. This part of code (with comments removed for brevity) is shown below:

				str.replace(/[\r\t\n]/g, " ")
					.replace(/'(?=[^%]*%})/g, "\t")
					.split("'").join("\\'")
					.split("\t").join("'")
					.replace(/{%=(.+?)%}/g, "',($1),'")
					.split("{%").join("');")
					.split("%}").join("_.push('")

We are going to replace HTML string str with a sequence of _.push() calls. What is actually pushed in the array depends on the type of the markup we are dealing with.

a) If it is plain HTML markup, then we just take the text as is, enclosed within single quotes, and put it into the array.

b) If it is an inline expression marked with {%= %} characters, then we take the content of the expression as is – it is not a string any more, but executable code that should result in some HTML markup.

c) If it is a code block delimited with {% %} characters, then we do not push anything explicitly. We just finish previous push() call and start another. It is the code within the code block which will decide whether it needs something to be rendered in this part of the template or not. It can contain inner inline expression or call helper methods (text() or html()) explicitly to modify content of the templating array being built.

I know, it is not very clear so far, but once you see the algorithm in action you’ll get it at once (or slightly later).

Several preparatory tasks are needed though. First we are going to replace each of the following special characters  \r\t\n with spaces. They are insignificant, besides, we will use the \t character later for our own needs. This replacement  is what the first line of the code does.

	str.replace(/[\r\t\n]/g, " ")

And then, since we chose single quotes to delimit our string literals, we need to escape single quotes that may be present within our markup. There is just one subtlety. If the single quote is within inline expression or code block, it does not deserve any special treatment and should be left unescaped. To do this, we perform following sequence of steps:

1) Replace all single quotes that happen to be within inline expression or code blocks with \t character;

2) Escape all outstanding single quotes;

3) Get those single quotes within expressions back – replace \t with quote

As you can see, these preparatory steps occupy more than a half of the entire replacement sequence:

				str.replace(/[\r\t\n]/g, " ")
					.replace(/'(?=[^%]*%})/g, "\t")
					.split("'").join("\\'")
					.split("\t").join("'")

But wait, at this point you may be wondering why I am talking about replacements and escaping while there are actually just strange chain of split() and join() calls? No? Great! Because I assume that your knowledge of JavaScript is sufficient to understand that using split() + join() sequentially we’ll get an equivalent of replace() method when it used with static content (string literal) as a pattern to replace.

But it is still valid question why in the same block of code we use replace() in some cases and split()/join() in others. See below the answer provided by the author of the code (John Resig):

"... If you're searching and replacing through a string with a static search and a static replace it's faster to perform the action with .split("match").join("replace") - which seems counter-intuitive but it manages to work that way in most modern browsers. (There are changes going in place to grossly improve the performance of .replace(/match/g, "replace") in the next version of Firefox - so the previous statement won't be the case for long.)"

You can find this answer in the following post: http://ejohn.org/blog/javascript-micro-templating/. If you read through the post you can also find out where the actual roots of this “experimental plugin” are concealed (surprise, surprise).

But back to our code. As always it all becomes clearer with one simple example. Let’s use the first thee lines of the HTML template presented at the beginning of the post:

	    title: <span class='title'>{%= title %}</span>
    	    <hr />
		{%= 'pages: ' + pages + '; year:' + year %}

and execute the lines of code we already understand using this template. The resulting string is

Title: <span class=\'title\'>{%= title %}</span>           <hr />    {%= 'Pages: ' + pages + '; Year:' + year %}

No magic so far. We now have a string with some single quotes escaped and some (the ones within expressions) left as is. But we are ready for the real things and can take care of the inline expressions:

					.replace(/{%=(.+?)%}/g, "',($1),'")

As a result of this additional replacement, our sample string starts resembling JavaScript code that we can push in array:

'Title: <span class=\'title\'>',( title ),'</span>           <hr />    ',( 'Pages: ' + pages + '; Year:' + year ),'  '

and if we combine it with the rest of the code of our compiled template the result will be:

	var fn = function(jQuery, $context) {
				var $ = jQuery, 
					$data = $context.dataItem,
					$i = $context.index,
					_ = $._ =[]; 
					_.context = $context;
				
				with ($.tmplFn) { 
					with ($data) {
						_.push('Title: <span class=\'title\'>',
						( title ),
						'</span>           <hr />    ',
						( 'Pages: ' + pages + '; Year:' + year ),
						'  ');
					}
				}
				return $(_.join('')).get();
			}

It makes sense in the end. HTML markup is stored in the array as is, and expressions are calculated using properties of our data item and also pushed into the array.

The last step in the process of generating our pre-built templating function is to deal with code blocks delimited by {% %} characters. It is the last two replacement patterns that achieve the goal:

					.split("{%").join("');")
					.split("%}").join("_.push('")

The code first closes previous push() statement using "');" sequence once the opening characters of the code block {%  are encountered. We always have this previous push() statement in place even if the template begins with the code block. It is because the first push() is hardcoded in the function body right after the inner with statment:

"with($.tmplFn){with($data){_.push('" +

And then we start another push statement using  "_.push('" sequence when closing characters of the code block %} are encountered. Again, we do not need to worry that some push() statement might stay open since our template generation code closes the last push() explicitly:

+ "');}}return $(_.join('')).get();");

Because the whole generation code is now revealed, we are ready to process the entire template with it. If you remember, our original version of the template was:

    <script id="template" type="text/html">	    
	<div> 
	    Title: <span class='title'>{%= title %}</span>
    	    <hr />
		{%= 'Pages: ' + pages + '; Year:' + year %}
	    <div class='price'>
		price: {%= price %}
	    </div>
	    Reviews: 
	    <ul>
		{% for (var i=0,review; review=reviews[i++];) { %}
		    <li class='review'>
			{%= review %}
		    </li>
		{% } %}
	    </ul>
	</div> 
    </script>

It contains HTML markup, inline expressions and code blocks. As a result of compiling this template we will have pre-built template rendering function ready to use:

    var fn = function(jQuery, $context) {
		    var $ = jQuery, 
			    $data = $context.dataItem,
			    $i = $context.index,
			    _ = $._ =[]; 
			    _.context = $context;
		    
		    with ($.tmplFn) { 
			    with ($data) {
				    '_.push('          <div>        Title: <span class=\'title\'>',
				    ( title ),
				    '</span>           <hr />    ',
				    ( 'Pages: ' + pages + '; Year:' + year ),
				    '       <div class=\'price\'>    Price: ',
				    ( price ),
				    '       </div>       Reviews:        <ul>    '); 
				    
					for (var i=0,review; review=reviews[i++];) { 
					    _.push('        <li class=\'review\'>     ',
					    ( review ),
					    '        </li>    '); 
				       } 
				    _.push('       </ul>   </div>       ');
			    }
		    }
		    return $(_.join('')).get();
	    }

Isn’t it beautiful?!

But we still have the last line of our tmpl() function to talk about. It is where the term “currying” comes into existence:

            // Provide some basic currying to the user
            return data ? fn(jQuery, { data: null, dataItem: data, index: i, options: options }) : fn;	    

As you can see, the tmpl() function can either return the generated template function, or - in case data parameter is provided - the result of the template function execution using the following context object:

{ data: null, dataItem: data, index: i, options: options }

The comment for the line states that the code provides “…some basic currying to the user”.

The term “currying”  basically means that we want to bind some arguments to a function and have a new function as a result which we can call later. The technique is achieved using closures. If you’ve ever used Function.prototype.bind() method of the prototype.js framework with more than one arguments applied, then you should know what I am talking about. Otherwise I will be better off to refer to the best minds of Generation JavaScript. Please use the source you are more comfortable with:

http://ejohn.org/blog/partial-functions-in-javascript/

http://www.dustindiaz.com/javascript-curry/

http://yuiblog.com/assets/pdf/zakas-projs-2ed-ch18.pdf

 

Frankly speaking this last line of tmpl() method code is not entirely clear to me. The reason for my doubts is that we do not actually have partially applied (curried) function in case these additional arguments are provided. We have the result of the function execution returned – the set of DOM elements. Such behavior – different types of results returned from the same method does not look accurate to me. But maybe I just did not get the higher motives of the code.

Now you should have complete understanding of how the heart of the jQuery templating plugin operates. I can only add that the best way to play with the template generation procedure is to feed different HTML templates to the jQuery.tmpl() function using Firebug console. Just start with something as simple as

jQuery.tmpl("<div>{%=title%}</div>").toString()

and, thanks to the toString() method, you’ll get complete picture of what your compiled template really is.

The last part of our journey will not require any additional mental efforts from your side. We have only one static utility method of jQuery function to talk about: jQuery.render(). On the other hand, it is the most important method from the API point of view. The code of the method is shown below:

		render: function( tmpl, data, options ) {
			var fn, node;
 
			if ( typeof tmpl === "string" ) {
				// Use a pre-defined template, if available
				fn = jQuery.templates[ tmpl ];
				if ( !fn && !htmlExpr.test( tmpl ) ) {
					// it is a selector
					node = jQuery( tmpl ).get( 0 );
				}
			} else if ( tmpl instanceof jQuery ) {
				node = tmpl.get( 0 );
			} else if ( tmpl.nodeType ) {
				node = tmpl;
			}
 
			if ( !fn && node ) {
				var elemData = jQuery.data( node );
				fn = elemData.tmpl || (elemData.tmpl = jQuery.tmpl( node.innerHTML ));
			}
 
			// We assume that if the template string is being passed directly
			// in the user doesn't want it cached. They can stick it in
			// jQuery.templates to cache it.
 
			fn = fn || jQuery.tmpl( tmpl );
 
			var rendering,
				rendered,
				context = {
					data: data,
					index: 0,
					dataItem: data,
					options: options || {}
				};
			if ( options ) {
				rendering = options.rendering;
				rendered = options.rendered;
			}
 
			function renderItem() {
				var dom = null;
				if ( !rendering || rendering( context ) !== false) {
					var dom = fn( jQuery, context );
					if ( rendered ) 
						rendered( context, dom );
				}
				return dom;
			}
 
			if ( jQuery.isArray( data ) ) {
				return jQuery.map( data, function( data, i ) {
					context.index = i;
					context.dataItem = data;
					return renderItem( );
				});
 
			} else {
				return renderItem( );
			}
		}

The first part of the code handles various possible types of the tmpl parameter:

			if ( typeof tmpl === "string" ) {
				// Use a pre-defined template, if available
				fn = jQuery.templates[ tmpl ];
				if ( !fn && !htmlExpr.test( tmpl ) ) {
					// it is a selector
					node = jQuery( tmpl ).get( 0 );
				}
			} else if ( tmpl instanceof jQuery ) {
				node = tmpl.get( 0 );
			} else if ( tmpl.nodeType ) {
				node = tmpl;
			}

This parameter can be interpreted in several different ways. If the argument is of string type, then we first assume that it is a key for the templates’ cache that can be found in new static member of jQuery object -  jQuery.templates. You can manually precompile a template and cache it using  jQuery.template property for future use:

jQuery.templates["books"] = jQuery.tmpl(jQuery("#template").html());

Now the template “books” can be consumed by jQuery.render() method like this:

jQuery(".titles").append(jQuery.render("books", books))

But let’s return to jQuery.render() method code. If tmpl argument is a string and jQuery.template object does not contain the property of the same name then htmlExpr regular expression is used to check whether the string contains HTML markup or not. If no traces of HTML is detected within the string then the string must contain valid selector which points to the node containing desired template markup:

node = jQuery( tmpl ).get( 0 );

If tmpl argument is not a string then we need to check whether it is an instance of jQuery wrapped element set. In such a case we just get the first element of the set as a potential container of the HTML markup:

node = tmpl.get( 0 );

Otherwise we check if tmpl references DOM element or not. If so, then again, it must be the required template container:

    } else if ( tmpl.nodeType ) {
	    node = tmpl;
    }

In case precompiled template was not found within jQuery.templates cache and we succeeded in interpreting tmpl argument as a DOM node we will try to build and cache the template using following code:

    if ( !fn && node ) {
	    var elemData = jQuery.data( node );
	    fn = elemData.tmpl || (elemData.tmpl = jQuery.tmpl( node.innerHTML ));
    }

Here the code retrieves data storage object associated with the given DOM element. If the storage already contains precompiled template (tmpl property of the data object), then this precompiled template will be used. Otherwise we generate the templating function by means of jQuery.tmpl() method using inner HTML of the template container node. In the latter case the generated template is cached in tmpl property of the data storage object associated with the DOM element for future use.

But there is still one missing case when tmpl argument passed to jQuery.render() method is a string, but it does contain HTML markup directly. The case is handled by the following line of code:

		fn = fn || jQuery.tmpl( tmpl );
The template is compiled from the HTML markup using jQuery.tmpl() method. In this case we do not cache the generated templating function since, according to comments, we assume that user does not want it cached.

The rest of the code is used to prepare context object, populate rendering and rendered variables with handler functions that can be supplied using options argument and finally render the template. Inner function renderItem() is defined to form a closure. It makes it possible for the function to gain access to the context, rendering and rendered variables when it runs:

			function renderItem() {
				var dom = null;
				if ( !rendering || rendering( context ) !== false) {
					var dom = fn( jQuery, context );
					if ( rendered ) 
						rendered( context, dom );
				}
				return dom;
			}

There is almost nothing to comment regarding the renderItem() function itself. It fires rendering and rendered events in due course. But it is still important to note that it is possible to cancel the rendering phase completely in case the rendering handler explicitly returns false.

Finally the renderItem() function is executed either for each data item in the data collection (in case data object is an instance of Array) or only once otherwise:

			if ( jQuery.isArray( data ) ) {
				return jQuery.map( data, function( data, i ) {
					context.index = i;
					context.dataItem = data;
					return renderItem( );
				});
 
			} else {
				return renderItem( );
			}

In the former case the context object is adjusted to contain correct dataItem and index prior to each iteration.

We are almost done. The only member we left untouched is tmplFn object containing pre-built functions which are available within our compiled templating function. The reason why the members of tempFn object are available within the context of generated template is the same why properties of data item can be accessed directly. By virtue of with statement. Do you remember such line in the body of the compiled template?

				with ($.tmplFn) { 

It is the line that does the trick. There are currently two pre-built functions available: html() and text(). Both of them serve as a means to modify resulting templating string - push() method is called on the instance of the templating string storage:

jQuery._.push.apply( jQuery._, arguments );

 

In case you do not remember, we stored the reference to jQuery._ array at the beginning of the code of our generated templating function:

	var fn = function(jQuery, $context) {
				var $ = jQuery, 
					$data = $context.dataItem,
					$i = $context.index,
					_ = $._ =[]; 
					_.context = $context;

We can also access the context object within our pre-built helper functions using jQuery._.context.

The only difference between html() and text() functions is that the text() was meant to provide encoding of HTML entities:

			text: function() {
				jQuery._.push.apply( jQuery._, jQuery.map(arguments, function(str) {
					return document.createTextNode(str).nodeValue;
				}) );
			}

At the moment of this writing it does not serve its purpose because obviously

document.createTextNode(str).nodeValue

returns exactly the same text as str argument had initially. It still is not encoded.

As far as I know, plugin authors are aware of this bug.

For the time being, before some really efficient encoding algorithm is suggested we can always resort to the plain old:

return jQuery("<div>").text(str).html();

That’s all. Nothing to add. Well, actually there is but this post already exceeded all reasonable limits.

Hope this helps,

Alexei

1 Comment

Comments have been disabled for this content.