jsTree tri-state checkboxes with ASP.NET MVC revisited

26 Comments

My original jsTree checkboxes post is still my most popluar, yet it is 18 months old and no longer works with the latest version of jsTree (pre 1.0). Since that post is so popular I thought I should update it.

My first surprise was how much the jsTree guys changed their API between releases – I was only going from version 0.9.9 to version 1.0, but  I had to practically start from scratch.

So, without further ado:

jsTree is a jQuery plugin for creating a treeviews, and jsTree’s checkbox plugin allows you to create a treeview with tri-state checkboxes, like so:

Notice how “Origination” appears half-checked because only some of its children are checked.

Getting started

For this demo I am using ASP.NET MVC 3 and jsTree pre 1.0 fixed. Let’s start with a new “ASP.NET MVC 3 Web Application”, choose Internet Application, name it jsTreeDemo, and add the required jsTree files to our solution:

In the View, create a div which you want to become a treeview. I’ll name mine demoTree. Also add references to the jsTree script, and add a new index.js file for our custom javascript.

Views/Home/Index.cshtml

@{
 ViewBag.Title = "Home Page";
}

<h2>@ViewBag.Message</h2>

<div id="demoTree">

</div>
<script type="text/javascript">
treeModel = [{"data":"Confirm Application","attr":{"id":"10"},"children":null},{"data":"Things","attr":{"id":"20"},"children":[{"data":"Thing 1","attr":{"id":"21"},"children":null},{"data":"Thing 2","attr":{"id":"22"},"children":null},{"data":"Thing 3","attr":{"id":"23"},"children":null},{"data":"Thing 4","attr":{"id":"24"},"children":[{"data":"Thing 4.1","attr":{"id":"241"},"children":null},{"data":"Thing 4.2","attr":{"id":"242"},"children":null},{"data":"Thing 4.3","attr":{"id":"243"},"children":null}]}]},{"data":"Colors","attr":{"id":"40"},"children":[{"data":"Red","attr":{"id":"41"},"children":null},{"data":"Green","attr":{"id":"42"},"children":null},{"data":"Blue","attr":{"id":"43"},"children":null},{"data":"Yellow","attr":{"id":"44"},"children":null}]}];
</script>
<script src="@Url.Content("~/Scripts/jquery.jstree.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/index.js")" type="text/javascript"></script>

Scripts/index.js

/// <reference path="jquery-1.7.1-vsdoc.js" />

$(function () {
   $("#demoTree").jstree({
      json_data : {
         data : treeModel
      },
      plugins : ["themes", "json_data", "ui", "checkbox"]
 });
});

Briefly, jstree has a number of ways of poplulating the tree with data - in my previous jsTree post I was populating the tree via AJAX but for now the tree is populated via hardcoded JSON (using the built-in "json_data" plugin). Don't forget to specify the "checkbox" plugin too.

Populate the tree from a viewmodel

Just like last time, let's create a viewmodel for creating the tree structure.

Models/JsTreeModel.cs

namespace jsTreeDemo.Models
{
  public class JsTreeModel
  {
    public string data;
    public JsTreeAttribute attr;
    public JsTreeModel[] children;
  }
  
  public class JsTreeAttribute
  {
    public string id;
    public bool selected;
  }
}

Now let's change our controller to create a viewmodel, serialize it to JSON, and pass that string to our View.
Controllers/HomeController.cs

public ActionResult Index()
{
    ViewBag.Message = "Welcome!";
            
    var model = GetTreeData();
            
    string jsonModel = new JavaScriptSerializer().Serialize(model);
            
    return View("Index", "_Layout", jsonModel);
}

private JsTreeModel[] GetTreeData()
{
var tree = new JsTreeModel[] 
{
    new JsTreeModel { data = "Confirm Application", attr = new JsTreeAttribute { id = "10", selected = true } },
    new JsTreeModel 
    { 
        data = "Things",
        attr = new JsTreeAttribute { id = "20" },
        children = new JsTreeModel[]
            {
                new JsTreeModel { data = "Thing 1", attr = new JsTreeAttribute { id = "21", selected = true } },
                new JsTreeModel { data = "Thing 2", attr = new JsTreeAttribute { id = "22" } },
                new JsTreeModel { data = "Thing 3", attr = new JsTreeAttribute { id = "23" } },
                new JsTreeModel 
                { 
                    data = "Thing 4", 
                    attr = new JsTreeAttribute { id = "24" },
                    children = new JsTreeModel[] 
                    { 
                        new JsTreeModel { data = "Thing 4.1", attr = new JsTreeAttribute { id = "241" } }, 
                        new JsTreeModel { data = "Thing 4.2", attr = new JsTreeAttribute { id = "242" } }, 
                        new JsTreeModel { data = "Thing 4.3", attr = new JsTreeAttribute { id = "243" } }
                    },
                },
            }
    },
    new JsTreeModel 
    {
        data = "Colors",
        attr = new JsTreeAttribute { id = "40" },
        children = new JsTreeModel[]
            {
                new JsTreeModel { data = "Red", attr = new JsTreeAttribute { id = "41" } },
                new JsTreeModel { data = "Green", attr = new JsTreeAttribute { id = "42" } },
                new JsTreeModel { data = "Blue", attr = new JsTreeAttribute { id = "43" } },
                new JsTreeModel { data = "Yellow", attr = new JsTreeAttribute { id = "44" } },
            }
    }
};

return tree;
}

Views/Index.cshtml

@model string
       
@{
    ViewBag.Title = "Home Page";
}

...

<script type="text/javascript">
    treeModel = @Html.Raw(Model);
</script>

Determining which items are checked when posting

Let’s put our tree inside a <form> and submit it.
Views/Index.cshtml

...
@using (Html.BeginForm("Results", "Home", FormMethod.Post))
{ 
    <div id="demoTree">
    </div>
    <br />
    <div>
        <input type="submit" value="Submit" id="btnSubmit" />
    </div>
} 
...

And now let's add the Results method to the HomeController

Controllers/HomeController.cs

[HttpPost]
public ActionResult Results(FormCollection form)
{
    return View(form);
}

and the View

Views/Home/Results.cshtml

@model FormCollection

@{
    ViewBag.Title = "Results";
}

<h2>Results</h2>
<p>
    You chose:
    @foreach (var item in Model.AllKeys)
    {
        @Model[item]@: 
    } 
</p>

<p>
    @Html.ActionLink("Home", "Index")
</p>

If you press the Submit button, nothing will happen as nothing is passed through in the FormCollection to Results() in HomeController. Despite appearances, jsTree doesn't by default render any HTML <input>s for the checkboxes. But it's easy enough to tell it to render them using the real_checkboxes flag:

Scripts/index.js

$(function () {
    $("#demoTree").jstree({
        json_data: {
            data: treeModel
        },
        checkbox: {
            real_checkboxes: true,
            real_checkboxes_names: function (n) {
                return [("check_" + (n[0].id || Math.ceil(Math.random() * 10000))), n[0].id]
            }
        },
        plugins: ["themes", "json_data", "ui", "checkbox"]
    });
});

Now when we submit our form, we should see the ids of the values which were checked:

What about telling the tree to pre-check some items?

So how do we render the tree with some items already checked? Notice how we added "public bool selected" to JsTreeAttribute? This doesn't do anything as far as the checkboxes are concerned, but it does add a custom property called "selected" to each node's <li>. We can use that to tell the jsTree to check the given node, by binding to the 'loaded.jstree' event.

Scripts/index.js

$(function () {
    $("#demoTree").jstree({
        json_data: {
            data: treeModel
        },
        checkbox: {
            real_checkboxes: true,
            real_checkboxes_names: function (n) {
                return [("check_" + (n[0].id || Math.ceil(Math.random() * 10000))), n[0].id]
            }
        },
        plugins: ["themes", "json_data", "ui", "checkbox"]
    }).bind("loaded.jstree", function (event, data) {
        $('#demoTree').jstree('check_node', 'li[selected=selected]');
    });
});

You’ll see that the nodes that are marked with selected=true in GetTreeData() are now pre-checked when you first load the page.

Here’s a link to the solution (VS 2010).

jQuery UI not working with ASP.NET MVC3 partial views

2 Comments

I was playing with ASP.NET MVC3 (RC2) and I couldn’t get jQuery UI’s datepicker to work for me in a partial view. It would always say datepicker is not a function.

It took me a while to figure out the problem is.

Firstly I was referencing jQueryUI’s css and jQuery UI in my layout page


_Layout.cshtml:

<head>
 <title>@ViewBag.Title</title>
 <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
 <link href="@Url.Content("~/Content/themes/base/jquery-ui.css")" rel="stylesheet" type="text/css" />
 <script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
 <script src="@Url.Content("~/Scripts/jquery-ui.min.js")" type="text/javascript"></script></head>

Then, at the bottom of my partial view I was calling datepicker like so:

Job.cshtml:
<div>
    @Html.LabelFor(model => model.DueDate)
</div>
<div>
    @Html.EditorFor(model => model.DueDate)
    @Html.ValidationMessageFor(model => model.DueDate)
</div>
<script language="javascript" type="text/javascript">
    $(document).ready(function () {
        $("#DueDate").datepicker();
    });
</script>

The problem was I didn't notice that the MVC scaffolding had added a reference to jQuery to the top of my partial view. This reference to jQuery was wiping out the earlier reference to jQuery UI in layout page.

Job.cshtml:
@model JobSystem.Web.Models.Job

<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

The solution was to remove the reference to jquery in my partial view (Job.cshtml).

I also decided to remove the reference to jQuery UI from the layout page (_Layout.cshtml) and add it to the partial view (Job.cshtml).

Using jsTree to get a Treeview with checkboxes in ASP.NET MVC

23 Comments

UPDATE 17 December 2011!

After one year this is still the most popular post on my blog. However, this uses jsTree v0.9.9a2 and jsTree is now in version v1.0 which has changed completely. This post does not work with jsTree v1.0!!!

I have written a post using the latest version of jsTree here

jsTree is a jQuery plugin for creating a treeviews, and jsTree’s checkbox plugin allows you to create a treeview with tri-state checkboxes, like so:

Notice how “Origination” appears half-checked because only some of its children are checked.

Getting started

For this demo I am using ASP.NET MVC 2 and jsTree v0.9.9a2. Let’s start with a new “ASP.NET MVC 2 Web Application”, name it jsTreeDemo, and add the required jsTree files to our solution:

In the View, create a div which you want to become a treeview. I’ll name mine demoTree. Also add references to the required jQuery and jsTree scripts:

Views/Home/Index.aspx

<asp:content id="Content2" runat="server" contentplaceholderid="MainContent">
 <h2><%: ViewData["Message"] %></h2>
 <div id="demoTree">
 </div>
 <script src="../../Scripts/jquery-1.4.1.js" type="text/javascript"></script>
 <script src="../../Scripts/jquery.tree.js" type="text/javascript"></script>
 <script src="../../Scripts/plugins/jquery.tree.checkbox.js" type="text/javascript"></script>
</asp:content>

Next, create index.js, add a reference to it in Index.aspx, and add the following code:

Scripts/index.js

/// <reference path="http://ajax.microsoft.com/ajax/jQuery/jquery-1.4.1-vsdoc.js"/>

$(function () {
  $("#demoTree").tree({
   ui: {
     theme_name: "checkbox"
   },
   data: {
     type: "json",
     opts: {
       static: [
           {
              data: "Origination",
              children: [
                { data: "New Connection" },
                { data: "Disconnection" },
                { data: "Load Change" },
                { data: "Corporate" },
              ]
            },
            {
              data: "Confirm Application"
            }
       ]
     }
   },
   plugins: {
     checkbox: {}
   }
  });
});

This should create a basic treeview with checkboxes, with static data. Not very interesting yet.

Populating the tree with an AJAX request

Let's create an ActionMethod to return some JSON data and use that data to populate our treeview. The jsTree documentation specifies how the JSON data should look but to help clarify, here's an example:

[{"data":"Origination","attributes":{"id":"10"}",children":[
                         {"data":"New Connection","attributes":{"id":"11"}},
                         {"data":"Disconnection","attributes":{"id":"12"}},
                         {"data":"Load Change","attributes":{"id":"13"}},
                         {"data":"Corporate","attributes":{"id":"14"}}
                         ]},
{"data":"Confirm Application","attributes":{"id":"20"}}
...]

While it might be possible to write a LINQ query with anonymous types to return this data, it quickly gets rather hairy, so to simplify things let's create a model to help us construct the data correctly. In the Models folder, add the following classes:

Models/JsTreeModel.cs

namespace jsTreeDemo.Models
{
 public class JsTreeModel
 {
   public string data;
   public JsTreeAttribute attributes;
   public JsTreeModel[] children;
 }

 public class JsTreeAttribute
 {
   public string id;
   public bool selected;
 }
}

In Controllers/HomeController.cs add the following function:

Controllers/HomeController.cs

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult GetTreeData()
{
 var tree = new JsTreeModel[]
 {
   new JsTreeModel {
     data = "Origination",
     attributes = new JsTreeAttribute  { id="10"},
     children = new JsTreeModel[]
     {
       new JsTreeModel { data = "New Connection", attributes = new JsTreeAttribute { id="11"} },
       new JsTreeModel { data = "Disconnection", attributes = new JsTreeAttribute { id="12", selected=true } },
       new JsTreeModel { data = "Load Change", attributes = new JsTreeAttribute { id="13"} },
       new JsTreeModel { data = "Corporate", attributes = new JsTreeAttribute { id="14", selected=true} },
     }
   },
   new JsTreeModel {
     data = "Confirm Application",
     attributes = new JsTreeAttribute { id="20" }
   },
   new JsTreeModel {
     data = "Things",
     attributes = new JsTreeAttribute  { id="30", selected=true },
     children = new JsTreeModel[]
     {
       new JsTreeModel { data = "Thing 1", attributes = new JsTreeAttribute { id="31"} },
       new JsTreeModel { data = "Thing 2", attributes = new JsTreeAttribute { id="32"} },
       new JsTreeModel { data = "Thing 3", attributes = new JsTreeAttribute { id="33"} },
       new JsTreeModel { data = "Thing 4", attributes = new JsTreeAttribute { id="34"} },
     }
   },
   new JsTreeModel {
     data = "Colors",
     attributes = new JsTreeAttribute  { id="40"},
     children = new JsTreeModel[]
     {
       new JsTreeModel { data = "Red", attributes = new JsTreeAttribute { id="41"} },
       new JsTreeModel { data = "Green", attributes = new JsTreeAttribute { id="42"} },
       new JsTreeModel { data = "Blue", attributes = new JsTreeAttribute { id="43"} },
       new JsTreeModel { data = "Yellow", attributes = new JsTreeAttribute { id="44"} },
     }
   }
 };

 return Json(tree);
}

Now we need to change our javascript to tell it to use GetTreeData:

Scripts/index.js


  $("#demoTree").tree({
   ui: {
     theme_name: "checkbox"
   },
   data: {
     type: "json",
     opts: {
         method: "POST",
         url: "/Home/GetTreeData"
     }
   },
...

Now our tree should be populated with the same data as Figure 1.

Determining which items are checked when posting

Let's put our tree inside a <form> and submit it.

Views/Home/Index.aspx

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
 <h2><%: ViewData["Message"] %></h2>

 <% using (Html.BeginForm("Submit", "Home", FormMethod.Post, new { id = "frmTree" }))
 { %>
   <div id="demoTree" style="height:300px">
   </div>
   <div>
     <input type="submit" value="Submit" id="btnSubmit" />
   </div>
 <% } %>

 <script src="../../Scripts/jquery-1.4.1.js" type="text/javascript"></script>
 <script src="../../Scripts/jquery.tree.js" type="text/javascript"></script>
 <script src="../../Scripts/plugins/jquery.tree.checkbox.js" type="text/javascript"></script>
 <script src="../../Scripts/index.js" type="text/javascript"></script>

</asp:Content>

And now let's add the Submit method to the HomeController

Controllers/HomeController.cs

[AcceptVerbs(HttpVerbs.Post)]
 public ActionResult Submit(FormCollection form)
 {
   return View(form);
 }

and the View

Views/Home/Submit.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<FormCollection>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
 Submit
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

 <h2>Submitted</h2>
 You chose:
 <% foreach (var item in Model)
    { %>
      <%: item %>
 <% } %>
</asp:Content>

If you press the Submit button, nothing will happen as nothing is passed through in the FormCollection to Submit() in HomeController. Despite appearances, the jsTree doesn't actually render any HTML <input>s for the checkboxes. So we need to write some javascript to call jsTree's get_checked() function to figure out which nodes are checked, and then add them to the form somehow. One way to add stuff to the form is to generate a hidden field, since hidden fields within a form are posted. This code generates hidden <input>s with the same name as each of the checked items, and adds them to the form.

Scripts/index.js

function generateHiddenFieldsForTree(treeId) {
  $.tree.plugins.checkbox.get_checked($.tree.reference("#" + treeId)).each(function () {
    var checkedId = this.id;
    $("<input>").attr("type", "hidden").attr("name", checkedId).val("on").appendTo("#" + treeId);
  });
}

and bind it to the form's submit event:

Scripts/index.js

$(function () {
  $("#frmTree").submit(function () { generateHiddenFieldsForTree("demoTree"); });
}

Now when we submit our form, we should see the ids of the values which were checked:

What about telling the tree to pre-check some items?

So how do we render the tree with some items already checked? My colleague Paul came up with this problem and the solution. Notice how we added "public bool selected" to JsTreeAttribute? This doesn't do anything as far as the checkboxes are concerned, but it does add a custom property called "selected" to each node's <li>. We can use that to tell the jsTree to check the given node, by using the jsTree's callback option.

Scripts/index.js


$("#demoTree").tree({
   ui: {
     theme_name: "checkbox"
   },
   data: {
     type: "json",
     opts: {
         method: "POST",
         url: "/Home/GetTreeData"
     }
   },
   plugins: {
     checkbox: {}
   },
   callback: {
     onload: function (tree) {
       $('li[selected=true]').each(function () {
         $.tree.plugins.checkbox.check(this);
       });
     }
   }
  });

You'll see that the nodes that are marked with selected=true in GetTreeData() are now checked when you load the page.

Here's a link to the solution (VS 2010) with code samples.

UPDATE 17 December 2011!

After one year this is still the most popular post on my blog. However, this uses jsTree v0.9.9a2 and jsTree is now in version v1.0 which has changed completely. This post does not work with jsTree v1.0!!!

I have written a post using the latest version of jsTree here

Enabling browser back button on cascading dropdowns with jQuery BBQ plugin

8 Comments

What’s this? I know you thought this was supposed to be a .NET blog, yet so far all I’ve posted is jQuery and iPhone stuff. Well the idea of this blog is that I will post stuff I’m learning on the job, and at the moment I’m learning a lot about jQuery, so here we go.

This continues on from the Knowledge Base project I used in a previous post. In this example, I start out on the “Help” page. I navigate to the Browse page, and then within the Browse page I we have a page which shows/hides different sections when the mouse is clicked.

In this example, I start on the Help page and navigate to a FAQ using cascading drop-downs. I choose a value from my “level1″ dropdown, Animal Control. The “level2″ drop down is populated with Animal Control-related topics, and I choose Dogs. The results pane is loaded with Dog-related FAQs, and I expand the first result, about micro-chipping dogs. I click a link to view the Dogs topic, and then click the browser back button. The browser takes me back to the Help page, but my previous state (viewing the “Do the dogs need to be microchipped” FAQ) isn’t displayed, instead the Help page is in its initial state.

We want to make it so that when the back button is clicked, we’ll be returned to the FAQ we were viewing on the Help page.

Existing Code

$(document).ready(function() {
  $('.dropdown').change(onSelectChange);
  loadServices(); // loads all the Services into the level1 dropdown
}

function onSelectChange() {     
   var selected = $("#" + this.id + " option:selected").val(); // the new selected value     
   if (this.id === 'level1') { // 'this' is the dropdown that was changed       
      loadTopicsForService(selected); // loads topics into the level2 dropdown with AJAX     
   } else if (this.id === 'level2') {
      showAnswers(selected); // shows FAQs for the selected topic with AJAX
   }
}

Plugins

To get the back button working, we need to kind of trick the browser into thinking that we're navigating up and down within the same page by adding # tags to the address. There are a number of jQuery plugins for doing this and I tried out a few - jquery history plugin, history, and jQuery BBQ.

I originally got it working with jQuery history, but it wouldn't work properly for IE7. So I tried again with jQuery BBQ and it worked fine across all our target browsers - IE6, IE7, IE8 and FireFox.

Code changes

First, I changed the dropdown change event. Instead of populating the drop downs by calling loadTopicsForService() or showing FAQs by calling showAnswers, I’ll call bbq.pushState:

$(document).ready(function() {     
  $(window).bind("hashchange", historyCallback); // needed for bbq plugin to work     
  
  $('.dropdown').change(function() {
    var dropdown = $(this);
    var id = this.id;
    var selected = dropdown.find("option:selected").val();
    if (selected != 0) {
      if (dropdown === "level1") {
        $.bbq.pushState({ level1: selected }, 2); // merge_mode=2 means wipe out other params (e.g. level2)
      } 
      else if (dropdown === "level2") {
        $.bbq.pushState({ level2: selected });
    }
  });

  loadServices(); // loads all the Services into the level1 dropdown
  historyCallback(); // manually call the bqq history callback, to process the # tags if any were previously 
                          // added. This is called when we load the page by pressing back, fwd, or refresh.
}

Now when I select "Animal Control" from the level 1 (aka Service) drop-down, jQuery.BBQ will 1. add "#level1=1" to the current page location, i.e. the address bar in the browser, and 2. call my historyCallback function which will do the work that used to be defined in the dropdown change event. #1 tricks the web browser into thinking that I'm now on a different page, which adds a new entry into the browser’s history so that clicking Back in the browser will take me to the previous page.

Now we need to define our callbacks which actually do the work of populating the cascading drop downs and displaying FAQs. Remember, historyCallback() will now do the work that used to be defined in onSelectChange().

function historyCallback(e) {
  levelOneCallback();     
  levelTwoCallback();
}

function levelOneCallback() {
  var L1 = $.bbq.getState("level1");     
  if (L1 != undefined && L1 != 0) {
    $('#level1').val(L1); // set dropdown value - needed for when fwd is clicked
    loadTopicsForService(L1); // loads topics into the level2 dropdown with AJAX
  }
  else { // level1 not specified so reset the page         
    $('#level1').val(0);
    $('#row2').hide();
  }
}

levelOneCallback() asks jQuery.BBQ if the level1 has been selected, and if it has been it loads up the level2 drop down. If level1 isn't specified then we should explicitly reset the level1 dropdown.

The rest of the code is here:

function levelTwoCallback() {
  var L2 = $.bbq.getState("level2");
  if (L2 != undefined && L2 != 0) {
    $('#level2').val(L2); // set dropdown - needed when fwd is clicked
    showAnswers($("#level1").val(), selected);
  } 
  else { // level2 not in querystring, so reset level2
    $('#level2').val(0);
  }
}

So, when I choose a value from the first dropdown, #level1=3 is added to the URL of the page. When I choose a value from the second dropdown, #level2=7 is added. For brevity I have omitted the code which adds #question=55 to the URL when a question is clicked.

Here's a video of the final result:

Once again I navigate through the drop downs, as before, but this time when I click back, we're brought back to the previous state which is viewing the question about microchipping dogs. As I keep clicking back, each previous state is loaded. And then when I click Fwd, my previous actions with the dropdowns are repeated!

I'd like to thank "Cowboy" Ben Alman for the jQuery BBQ library and also for suggesting some improvements to my code.

Hope that helps someone.

Generating and injecting HTML content with Javascript and jQuery

Leave a comment

Although I am a .NET developer, a recent small project I was working on wasn’t .NET but just plain ol’ HTML pages. I’m our team’s jQuery expert but that’s not saying much at all – I only earned that title by buying jQuery in Action and I have it sitting on my desk.

DISCLAIMER: I’m not a Javascript or jQuery expert! I’m still learning this stuff.

The project was a Knowledge Base. The requirements were that the user would choose a topic from a drop-down, then a subtopic, then a sub-sub topic and finally the “Questions” relating to that topic would be displayed. So away we went, writing and implementing “GetSubTopicsForTopic” web services which would populate the cascading drop downs. Fine and dandy.

Obviously, depending on which sub-subtopic they choose, the number of “Questions” returned will vary. Some subtopics (e.g. Contact Us”) will only have one Question while others would have 5 or 6. We would need to dynamically generate the HTML to display each Question (and it’s Answer) and then inject it.

Step 1: Getting the Answers

Here’s our jQuery.ajax call to get the Questions for the sub-subtopic. We first used .getJSON but were having some concurrency issues with it, so changed to .ajax and set async to false.


function showAnswers(subSubTopicID) {
$.ajax({
    async: false,
    url: '/_layouts/kb/services/getquestions.ashx',
    data: { subSubTopicid: subSubTopicID },
    type: 'GET',
    dataType: 'json',
    success:
       function(questions) {
          if (questions == null || questions.length == 0) {
             return;
          }

          // set header text
          var resultText = "Your query returned " + questions.length +  ((questions.length == 1) ? " result." : " results.");
          $('.header').text(resultText);
          renderQuestions(questions);
       }
   });
}

Now that we've gotten an array of Questions back we need to render them.

Here's what it looks like again:

The HTML we need to generate for each Question and Answer (Q&A) is quite long, about 70 lines, as follows:

<ul id="results-list">
   <li style="border-bottom: #c4c3c3 1px dotted" id="Question77">
      <a id="question_77" href="#">
         What developments will contributions be charged on?
      </a>
      <div style="zoom: 1; display: block">
         <div class="answerText">
             Contributions will be charged on any development or change in use or service connection that creates demand on the city’s infrastructure. The only exception is for residential
   ...

A parent <li> followed by a bunch of child nested <div>, all within a <ul>.

Generating HTML from Javascript was something I'd never done before,  but I had worked on existing projects which do this.

Option 1: Embedding HTML in the Javascript

I've seen this done on a number of projects. e.g.

function renderQuestion(question) {
   // generate the html for the question
   var html = '<li style="border-bottom: #c4c3c3 1px dotted" id="Question' + question.QuestionID + '">';
   html += '<a id="question_' + question.QuestionID + '" href="#">';
   html += question.QuestionText;
   html += '</a>';
   html += '<div style="filter: ; zoom: 1; display: block">';
   html += '<div class="answerText">';
   html += question.Answer;
   html += '</div></div></li>';

   // add it to the DOM tree
   var results = document.getElementById('results-list');
   results.innerHTML += html;
}

The thing I don't like about this is you're mixing the HTML in with the Javascript, so that if the HTML design needs to change then you need to change the Javascript too. On the bright side, it should be reasonably simple to change the HTML, but as a programmer it smells bad.

Option 2: Creating DOM elements

Use Javascript (or jQuery) to create the <li> and <div> and then append them, like so.

function renderQuestion(question) {
   var listItem = document.createElement('li');
   listItem.setAttribute('style', 'border-bottom: #c4c3c3 1px dotted');
   listItem.setAttribute('id', 'Question' + question.QuestionID);

   var a = document.createElement('a');
   a.setAttribute('href', '#');
   a.setAttribute('id', 'question_' + question.QuestionID);
   a.innerHTML = question.QuestionText;
   listItem.appendChild(a);

   var answerDiv = document.createElement('div');
   answerDiv.className = 'answerText';
   answerDiv.innerHTML = question.Answer;
   listItem.appendChild(answerDiv);

   var results = document.getElementById('results-list');
   results.appendChild(listItem);
} 

Option 2 is more code than Option 1. As a programmer it feels cleaner, since I'm manipulating properties on objects rather than having hard-coded HTML. But it's less readable than Option 1 and would be more difficult to change the HTML.

Option 3: Duplicate an HTML sample row and add it to the DOM

This was an idea that I came up with, it's probably been done somewhere else before but I've never seen it.

Update: Ha, although I came up with this idea myself, Rick Strahl blogged this exact same technique way back in October 2008, he calls it 'Manual' Templating.

I'll have an invisible "sampleQuestion" which contains the <li> and all its <div> embedded in the HTML.

help.html:
<ul id="results-list">
   <!--    Sample Question used for cloning   -->
   <li id="sampleQuestion" style="display: none;border-bottom:1px dotted #c4c3c3">
      <a href="#" class="questionLink"></a>
      <div style="zoom: 1; display: block">
      <div class="answerText"></div>
...

I'll use jQuery to make a copy of this sample Question, change some attributes, make it visible, and then add it to the DOM.

help.js:
function renderQuestion(question) {
   var sampleQuestion = $("#sampleQuestion");
   var listItem = sampleQuestion.clone(false);
   listItem.attr('id', 'Question' + question.QuestionID);

   var questionLink = listItem.find('.questionLink');
   questionLink.attr('id', 'question_' + question.QuestionID);
   questionLink.text(question.QuestionText);

   listItem.find(".answerText").text(question.Answer);

   listItem.show();
   listItem.insertBefore(sampleQuestion);
}

I like this approach because the page design and layout is kept in the HTML file where it belongs. If your designer wants to change the layout he can, as long as he doesn't change the ids and the class names too much. The only thing the Javascript does is set the IDs and set the content.

Update - Option 4: jQuery Templating

I only just found out about this, but a number of existing jQuery solutions exist for dealing with this exact scenario. Rick Strahl's blog post looks at a few, and now Microsoft have proposed their own solution which might be added to jQuery in the future.

Summary

On the Knowledge Base project I implemented a 'manual' templating solution because I didn't know better, but jQuery has a number of solutions for generating and injecting HTML content which are worth further investigation.

  1. John Resig's Micro-Templating looks like a good option
  2. jTemplates
  3. PURE (Pure Unobtrusive Rendering Engine), a Javascript library (not jQuery specific)

Follow

Get every new post delivered to your Inbox.