Monday, September 23, 2013

Handlebars & jQuery UI Accordion Part 1

This post is modeled after something I have recently done at work. Here is the rough description....

The application is an ASP.NET Web Forms application that is at least five years old. The DevExpress tool suite is used heavily throughout the site. The business wanted to make a presentation change of a grid, thing is, the DevExpress grid could not do it. I was tasked with handling this so I went with Handlebars.js and the Accordion control from jQuery UI. It was necessary to use templating as the data was dynamic. This ended up being a very nasty task due to various button click handlers and the nasty postback behavior of ASP.NET.

For this walkthrough, I'm going with ASP.NET MVC just to simplify things. Make sure to add Handlebars through NuGet to the solution. Let's start with the models we will be using.....
public class Material
{
   public string MaterialDescription { get; set; }
   public int RequestQty { get; set; }
   public int PackQty { get; set; }
   public int PackSize { get; set; }
   public int TotalApproved { get; set; }
   public int AvailableInv { get; set; }
}

I'm adding a view model to send to the client because generally, I do not like returning anonymous objects. When I can, I always like to be explicit and declare the type. The view model:
public class IndexViewModel
{
   public string Headers { get; set; }
   public IEnumerable<Material> Contents { get; set; }
}

The Index method of whatever controller you are using can be left as is. Add the view. The view will contain the target div for the accordion as well as the Handlebars template. The view:
@model IEnumerable<YourSolution.Models.IndexViewModel>
@{
   ViewBag.Title = "Handlebars Accordion";
}


<div id="target"></div>

<script id="testTmpl" type="text/x-handlebars-template">
   <div id="test">
      {{#each this}}
         <h3><a href="#">{{this.Headers}}</a><h3>
         <div>
            <table class="table">
               <tr>
                  <th>Request Qty</th>
                  <th>Pack Qty</th>
                  <th>Pack Size</th>
                  <th>Total Approved</th>
                  <th>Available Inv</th>
               </tr>
               {{#each this.Contents}}
                  <tr>
                     <td>{{this.RequestQty}}</td>
                     <td>{{this.PackQty}}</td>
                     <td>{{this.PackSize}}</td>
                     <td>{{this.TotalApproved}}</td>
                     <td>{{this.AvailableIn}}</td>
                  </tr>
               {{/each}}
            </table>
         </div>
      {{/each}}
   </div>
</script>

Handlebars is pretty slick and easy to use. To loop through a collection is fairly straightforward, just use #each and this, where this of course refers to the current item. This also shows a nested data structure and how to perform nested looping. We have two properties in the view model, Headers and contents. In the first each, we are just writing out the Headers property. To access any properties you just use dot syntax. When you want to access the nested object, just embed another each, and you will have access to the nested property/object.
Let's move on to the JavaScript. For simplicity, I am putting it in the view as well.
<script type="text/javascript">
   $(function() {
      $.ajax({
         url: '/AccordionTmpl/GetData',
         type: 'GET',
         contentType: 'application/json; charset=utf-8',
         dataType: 'json',
         success: function(data) {
            var source = $('#testTmpl')html();
            var template = Handlebars.compile(source);
            $('#target').html(template(data));

            $('#test').accordion({
               active: false,
               header: 'h3',
               collapsible: true,
               icons: {
                  "header": "ui-icon-plus",
                  "headerSelected": "ui-icon-minus"
               }
            });
         },
         error: function(response) {
            alert(response.responseText);
         }
      });
   });
</script>

If you've done any templating this should look familiar. Grab the html of the template, compile it, feed the data into the template, take the html and add it to the target. The accordion configuration is pretty basic as well.
There is one css class in our template. We will add some styling to the table, headers and rows.
.tbl {
   border-collapse: collapse;
   /* center the table */
   margin: 10px auto 10px auto;
}

.tbl th {
   background: #002a5f;
   color: white;
   padding-left: 20px;
   padding-right: 20px;
   border: 1px solid black;
}

.tbl td {
   border: 1px solid black;
   text-align: center;
}
We have to go back to the server and write the method to return data:
[HttpGet]
public ActionResult GetData()
{
   List<Material> materials = new List<Material>()
   {
      new Material(){MaterialDescription = "Big Bang Theory Sheldon Shirt", RequestQty = 3, PackQty = 3, PackSize = 3, TotalApproved = 3, AvailableInv = 6},
      new Material(){MaterialDescription = "Big Bang Theory Leonard Shirt", RequestQty = 2, PackQty = 2, PackSize = 2, TotalApproved = 2, AvailableInv = 7},
      new Material(){MaterialDescription = "Big Bang Theory Penny Shirt", RequestQty = 1, PackQty = 1, PackSize = 1, TotalApproved = 1, AvailableInv = 9},
      new Material(){MaterialDescription = "Big Bang Theory Kripke Shirt", RequestQty = 1, PackQty = 1, PackSize = 1, TotalApproved = 1, AvailableInv = 10},
   };

   IEnumerable<IndexViewModel> data = materials
                                            .GroupBy(x => x.MaterialDescription)
                                            .Select(y => new IndexViewModel()
                                            {
                                               Headers = y.Key, 
                                               Contents = y.Select(z => z).ToList()
                                            });

   return Json(data, JsonRequestBehavior.AllowGet);
}

Let's run and see what we got...
 

Not too shabby. We did some good work, should probably reward ourselves with some pizza.

*One hour later*

It seems our friends in the business saw what we did and they have a request. Since the list of items can be quite a few, they would like it if more than one panel could stay open so a user can remember where they were.

This can be done even though the accordion was not designed to do this. The solution, initially, seems very simple. The answer lies in the structure of the accordion. You have to change the target of the accordion. If you look at the documentation for the accordion, the structure of your html has to look like this:

<div id="accordion">
   <h3>First Header</h3>
   <div>First Content Panel</div>
   <h3>Second Header</h3>
   <div>Second Content Panel</div>
</div>

Our structure is different but still worked. We are going to change the target for the accordion from $('#test) to $('#test > div'). Our target is now the child div of the test div. The only other change we need to make is to add some css to the anchor tag, right after the accordion declaration. Make the changes:
$('#test > div').accordion({
   active: false,
   header: 'h3',
   collapsible: true,
   icons: {
      "header": "ui-icon-plus",
      "headerSelected": "ui-icon-minus"
   }
});

$('a').css({ "display": "inline-block" });

Now to run...
 
Looks good! The css that was added to the anchor tag is very, very important. If you are ever using the accordion and your icons are mashed in with the text, applying that css to the anchor will solve it. Another solution is the following:
<h3>
   <span style="display: inline-block;" />
   <a href="#">{{this.Headers}}</a>
</h3>

Really the only difference is to what element you apply the css. Again, just inlining the css for illustrative purposes.

In the next post, we will revisit this scenario as our friends from the business have another request.