Minimize Code by Using jQuery and Data Templates
I’m currently working on a heavily AJAX-oriented ASP.NET MVC web application for a business client and using jQuery to call controller actions, retrieve JSON data and then manipulate the DOM to display the data. Several of the pages have quite a bit of dynamic HTML that has to be generated once a JSON object is returned from an MVC controller action which generally leads to a lot of custom JavaScript. After working through my first page on the project I realized that I was creating a maintenance nightmare due to the amount of JavaScript being written and decided to look into other options.
The first thing I looked for was some type of JavaScript template that would work much like GridView templates in ASP.NET. I wanted to be able to define a template in HTML and then bind a JSON object against it. That way I could easily tweak the template without having to actually touch my JavaScript code much. I found several potential template solutions (and Microsoft will be releasing a nice option with ASP.NET 4.0 as well) that were nice but many were so CSS class centric that they ended up being a turn off since I felt like I had to learn yet another coding style just to use them. I eventually came across one by John Resig (creator of jQuery and overall JavaScript genius) that was so small that I wasn’t sure it would even be viable. I mean we’re talking tiny as far as code goes…so tiny that I figured it wouldn’t work well for what I needed. After doing more searching and research I came across a post by my world famous buddy Rick Strahl (if you don’t currently follow his blog you won’t find a better one out there IMHO) that mentioned John’s micro template technique and had a few tweaks in it. I tried it and was instantly hooked because it gave me the power to use templates yet still embed JavaScript to perform basic presentation logic (loops, conditionals, etc.) as needed.
The Template Engine (chipmunk power)
So here’s how the template works. The first thing I did was add the template function as a jQuery extension so that I could get to it using familiar jQuery syntax. This isn't required at all, it’s just something I wanted to do. I ended up going with Rick’s slightly tweaked version and I only changed how the error was reported. I’m not going to go into how to extend jQuery in this post, but here’s what the extension function looks like:
$.fn.parseTemplate = function(data) { var str = (this).html(); var _tmplCache = {} var err = ""; try { var func = _tmplCache[str]; if (!func) { var strFunc = "var p=[],print=function(){p.push.apply(p,arguments);};" + "with(obj){p.push('" + str.replace(/[\r\t\n]/g, " ") .replace(/'(?=[^#]*#>)/g, "\t") .split("'").join("\\'") .split("\t").join("'") .replace(/<#=(.+?)#>/g, "',$1,'") .split("<#").join("');") .split("#>").join("p.push('") + "');}return p.join('');"; //alert(strFunc); func = new Function("obj", strFunc); _tmplCache[str] = func; } return func(data); } catch (e) { err = e.message; } return "< # ERROR: " + err.toString() + " # >"; }
That’s all the code for the template engine. Unbelievable really…runs on chipmunk power.
Creating a Template
Once the extension function was ready I had to create a template in my MVC view (note that this works fine in any web application, not just ASP.NET MVC) that described how the JSON data should be presented. Templates are placed inside of a script tag as shown next (I chopped out most of the template to keep it more concise).
<script id="MenuSummaryTemplate" type="text/html"> <table style="width:100%;"> <tbody> <tr> <td class="OrderHeader">Totals:</td> </tr> <tr> <td style="font-size:12pt;"> <table style="width:400px;"> <tr> <td style="width:50%;">Sub Total:</td> <td>$<span id="FinalSubTotal"><#= FinalSubTotal #></span></td> </tr> <tr> <td>Sales Tax:</td> <td>$<span id="FinalSalesTax"><#= FinalSalesTax #></span></td> </tr> <# if (DeliveryFee > 0) { #> <tr> <td>Delivery Fee:</td> <td>$<span id="DeliveryFee"><#= DeliveryFee #></span></td> </tr> <# } #> <tr> <td>Admin Fee:</td> <td>$<span id="AdminFee"><#= AdminFee #></span></td> </tr> <tr style="border-top:1px solid black;"> <td>Total:</td> <td>$<span id="FinalTotal"><#= FinalTotal #></span></td> </tr> <tr> <td colspan="2"> </td> </tr> <tr> <td colspan="2">Will be charged to your credit card ending with <#= CreditCard #></td> </tr> </table> </td> </tr>
<!-- More of the template would follow --> </tbody> </table> </script>
You can see that the script block template container has a type of text/html and that the template uses <#= #> blocks to define placeholders for JSON properties that are bound to the template. The text/html type is a trick to hide the template from the browser and I suspect some may not like that…you’re call though…I’m just showing one option. The template supports embedding JavaScript logic into it which is one of my favorite features.
After a little thought you may wonder why I didn’t simply update the spans and divs using simple JavaScript and avoid the template completely. By using a template my coding is cut-down to 2 lines of JavaScript code once the JSON object is created (which you’ll see in a moment) and this is only part of the template. Here’s another section of it that handles looping through menu items and creating rows:
<# if (MainItems == null || MainItems.length == 0) { #> <tr> <td>No items selected</td> </tr> <# } else { for(var i=0; i < MainItems.length; i++) { var mmi = MainItems[i]; #> <tr> <td> <#= mmi.Name #>: <#= mmi.NumberOfPeople #> ordered at $<#= mmi.PricePerPerson #> per person </td> </tr> <# } } #>
Binding Data To a Template
To bind JSON data to the template I can call my jQuery extension named parseTemplate(), get back the final HTML as a string and then add that into the DOM. Here’s an example of binding to the template shown above. I went ahead and left the JSON data that’s being bound in so that you could see it, but jump to the bottom of LoadApprovalDiv() to see where I bind the JSON object to the template….it’s only 2 lines of code.
function LoadApprovalDiv() { var subTotal = parseFloat($('#SubTotal').text()); var salesTaxRate = parseFloat($('#SalesTaxRate').val()) / 100; var salesTaxAmount = subTotal * salesTaxRate; var deliveryFee = parseFloat($('#DeliveryFee').val()); var adminFee = (subTotal + salesTaxAmount + deliveryFee) * .05; var total = subTotal + salesTaxAmount + deliveryFee + adminFee; var deliveryAddress = $('#Delivery_Street').val() + ' ' + $('#Delivery_City').val() + " " + $('#Delivery_StateID option:selected').text() + ' ' + $('#Delivery_Zip').val(); var creditCard = $('#Payment_CreditCardNumber').val(); var abbrCreditCard = '*' + creditCard.substring(creditCard.length - 5); var json = { 'FinalSubTotal' : subTotal.toFixed(2), 'FinalSalesTax' : salesTaxAmount.toFixed(2), 'FinalTotal' : total.toFixed(2), 'DeliveryFee' : deliveryFee.toFixed(2), 'AdminFee' : adminFee.toFixed(2), 'DeliveryName' : $('#Delivery_Name').val(), 'DeliveryAddress': deliveryAddress, 'CreditCard' : abbrCreditCard, 'DeliveryDate' : $('#Delivery_DeliveryDate').val(), 'DeliveryTime' : $('#Delivery_DeliveryTime option:selected').text(), 'MainItems' : GenerateJson('Main'), 'SideItems' : GenerateJson('Side'), 'DesertItems' : GenerateJson('Desert'), 'DrinkItems' : GenerateJson('Drink') }; var s = $('#MenuSummaryTemplate').parseTemplate(json); $('#MenuSummaryOutput').html(s); }
You can see that I call parseTemplate(), pass in the template to use and JSON object and then get back a string. I then add the string into a div with an ID of MenuSummaryOutput using jQuery. Here’s a sample of what the template generates:
Going this route cut down my JavaScript code by at least 75% over what I had originally and makes it really easy to maintain. If I need to add a new CSS style or modify how things are presented I can simply change the template and avoid writing custom JavaScript code. By using the template and AJAX calls I’ve been able to significantly minimize the amount of server code being written and meet the client’s requirement of having an extremely fast and snappy end user experience. If you’re writing a lot of custom JavaScript currently to generate DOM objects I’d highly recommend looking into this template or some of the other template solutions out there. I can’t say I’ve tested performance but can say that I’m working with some fairly large templates which are loading in < 1 second. I personally feel it’s the way to go especially if you want to minimize code and simplify maintenance. I think Microsoft’s entry into this area with ASP.NET 4.0 further validates the usefulness of client-side templates.
For more information about onsite, online and video training, mentoring and consulting solutions for .NET, SharePoint or Silverlight please visit http://www.thewahlingroup.com/.