.png)
Building a Data-Driven Accordion with jQuery UI
Dino Esposito | January 29, 2010
Collapsible panels are a frequent feature in modern, cutting-edge Web sites. They allow you to display a short highlight—the header—and keep other text hidden and available on demand. Many JavaScript libraries, including the AJAX Control Toolkit, now embedded in the ASP.NET AJAX 4 package, offer ad hoc components to quickly and easily hide and display a block of markup. But what if you need to build a hierarchy of panels in the way that Outlook does with its applets (such as Inbox, Calendar, and Contacts)?
The Accordion widget allows you to group multiple collapsible panels in a single place and manages the collapsed/expanded state of each panel so that only one can be expanded at a time. If you look around, you can find many flavors of the Accordion widget in various libraries for ASP.NET. More often than not, the accordion functionality is offered as a declarative server control that builds the initial markup on the server and then does most of its interactive work on the client.
The jQuery UI library is kind of different. It offers an accordion component that is written entirely in JavaScript and, more importantly, creates its own user interface from the HTML you have in the page. Creating an Accordion in jQuery is straightforward, but only if you are content with static markup that never changes during interaction with the user.
In this article, I’ll first discuss the basic functionalities of a jQuery UI accordion, and then I’ll go beyond the basics by illustrating the steps required to build a data-driven accordion-based user interface. The sample pages are part of an ASP.NET MVC application, but any features discussed in the article apply to ASP.NET Web Forms, too.
The Accordion Component at a Glance
To set up a jQuery UI application, you download from jqueryui.com the script files and a proper graphical theme to give your pages a compelling appearance. In ASP.NET MVC, you typically copy the JavaScript files to the Scripts folder, whereas theme files go in the Content folder. Next, you link the main CSS file of the theme and jQuery script files from the master page. Note that to use jQuery UI, you also need to link the core jQuery library.
The following code shows the typical content of the HEAD section of the master page in a standard ASP.NET MVC application. The listing also includes a custom CSS class used by pages.
<head runat="server">
<title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
<link href="/Content/Site.css"
rel="stylesheet" type="text/css" />
<link href="/Content/jq-ui-Themes/sunny/jquery-ui-sunny.css"
rel="stylesheet" type="text/css" />
<script type="text/javascript" src="/Scripts/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="/Scripts/jQueryUI/jquery-ui-
1.7.2.custom.min.js"></script>
<style type="text/css">
.custContactLabel {
background-color:orange;
border-style:solid;
border-width:1px;
font-weight:bold;
}
.custContactText {
border-style:solid;
border-color:orange;
border-width:1px;
};
</style>
</head>
Just like any other widget in the jQuery UI library, the accordion component works by taking a DOM subtree and manipulating it to the expected user interface. Not just any DOM subtree is a good candidate to host an accordion. Here’s the required structure:
<div id="YourAccordion">
<h3>
<a href="#"> caption </a>
</h3>
<div>
content
</div>
:
</div>
The header of each tab is fully identified by a block element with a child anchor, most commonly a H3. In particular, the tab header shows any HTML you use as the text of the hyperlink—mostly text and icons. The body of the tab is defined by the content of the following element, most commonly a DIV. Subsequently, the whole accordion structure is then expressed as a sequence of pairs of H3 and DIV elements contained within a surrounding DIV. The ID of the surrounding DIV identifies the accordion as a programmable element.
Building a valid HTML structure, however, is not enough. You transform it into a working accordion by calling the accordion function on it. Here’s how to do it from the document’s ready event.
<script type="text/javascript">
$(document).ready(function() {
$("#YourAccordion").accordion();
});
</script>
By default, the first panel is automatically open. Note that the jQuery UI accordion doesn’t support multiple open panels. You can switch to a different panel by clicking, by hovering with the mouse, or programmatically. I’ll return to these points later. For now, let’s proceed with a basic example that demonstrates a static accordion.
Building a Static Accordion
As I mentioned, an accordion is defined by a fixed sequence of H3 and DIV elements. As long as the content of each panel is fixed, building the accordion is no big deal. Moreover, you can mix accordions with tabs, as the following code demonstrates:
<script type="text/javascript">
$(document).ready(function() {
$("#AppConsole").tabs();
$("#accCustomers").accordion();
});
</script>
<div id="AppConsole">
<ul>
<li><a href="#tabCustomers"><span><b>Customers</b></span></a></li>
</ul>
<div id="tabCustomers">
<% Html.RenderPartial("ucListCustomer1"); %>
</div>
</div>
The code shows the typical scaffolding of a tab container, except that the user control rendered inside the unique tab implements an accordion:
<h2> Some static content </h2>
<hr />
<div id="accCustomers">
<h3><a href="#"> ALFKI </a></h3>
<div>
Customer ALFKI
</div>
<h3><a href="#">BOTTM</a></h3>
<div>
Customer BOTTM
</div>
<h3><a href="#">CACTU</a></h3>
<div>
Customer CACTU
</div>
</div>
Figure 1 shows the final result
.png)
Figure 1. A simple accordion rendered with a tab container.
From a user perspective, the accordion is a vertical tab container. The user clicks a tab to select it. Any currently open tab is automatically closed when another tab is clicked. The widget also adapts graphical styles to the state of the tab and toggles icons to further indicate its open/closed state. The internal implementation is nothing really different from maintaining a list of DIVs and toggling their visibility state via CSS commands.
Commands for the Accordion Widget
You send commands to an accordion by using the same accordion method you use to set up the widget. The name of the command, as well as any required parameters, is specified as an argument. For example, here’s how you programmatically disable a working accordion:
$("#accCustomers").accordion("disable");
A disabled accordion is simply an accordion that ignores any click. The user interface doesn’t change significantly and doesn’t even look grayed out. You enable the accordion again by sending the enable command. Table 1 lists the commands supported by the Accordion widget in the current version of jQuery UI.
Table 1. Commands supported by the Accordion object
| Command | Description |
| destroy | Destroys the accordion. Any dynamic manipulation performed by the library is undone and the constituent HTML is rendered as usual. |
| disable | Disables the accordion. Nothing changes in the visual representation of the accordion. It only stops receiving and handling click notifications. In a way, a disabled accordion is one with a locked structure. Any links you may have in the visible panel work as usual. |
| enable | Enables a previously disabled accordion. It simply restores the expected behavior of the accordion. When first created, an accordion is automatically enabled. |
| option | Reads or writes any of the supported attributes. (More on this in a moment.) |
| activate | Activates a particular panel. You specify the panel using a zero-based index. If the index you specify is out of range it will be ignored. If you pass a Boolean value of false instead, all panels will be closed. |
To programmatically select the second tab, if there is one, you need the following code:
$("accCustomers").accordion("activate", 1);
You can close all panels with the following line:
$("accCustomers").accordion("activate", false);
Table 2 lists the most frequently used attributes supported by the accordion. You read and write these options by using the option command. You can pass a bunch of options when you create the accordion.
Table 2. Most commonly used options for the Accordion object
| Option | Description |
| active | Indicates the zero-based index of the panel to be initially selected. |
| animated | Indicates the animation effect to be used for transitions. |
| autoHeight | Set by default; indicates that all panels have the same height, determined by the highest of the specified panels. |
| collapsible | Indicates whether all panels can be closed at the same time. |
| event | Indicates the event to be used to switch between panels. It is click by default, but can be set to mouseover. |
| header | Indicates the HTML element to be used as the header. It defaults to H3. |
The following code shows how to set the autoHeight options:
$("#accCustomers").accordion("option", "autoHeight", false);
To read an option, you just omit the value; in this case, the accordion function returns the current value for the option.
Toward a Dynamic-Content Accordion
Let’s now build a richer example, where the panels (and related content) of the accordion are determined dynamically. In other words, I’m going to demonstrate how you can use the accordion as a special, data-bound drop-down list instead of as a static menu. As an example, I’ll build a button bar with which users can pick a letter and then fire a call to a remote service to grab all customers whose name begins with that letter. All downloaded customers are then shown in panels in a dynamically configured accordion.
The following jQuery code creates a button bar. Each client button is bound to the same JavaScript function. The code triggers a call to a service and populates the accordion with the results.
function buildButtonBar() {
for (var i = 0; i < 26; i++) {
var newButton = $('<input type="button" onclick="fillView(this.value)" />');
var text = String.fromCharCode('A'.charCodeAt(0) + i);
newButton.attr("value", text).appendTo("#menuBar").show();
}
}
Here’s the source code for the JavaScript click handler:
function fillView(selection) {
updateSelection(selection);
var data = loadFromCache(selection);
if (typeof (data) !== 'undefined') {
fillViewInternal(data, true);
return;
}
// Grab data from the server asynchronously
loadFromSource(selection);
}
The function first attempts to read customer information from a browser-side cache. If it fails, it then attempts to call a remote Web service. The browser-side cache is implemented on top of the jQuery cache services represented by the cache array. The next listing shows the code that initializes the jQuery cache.
var appCache = null;
function cacheInit() {
if (!jQuery.cache["Sample2"])
jQuery.cache["Sample2"] = {};
appCache = jQuery.cache["Sample2"];
}
function cacheGetKey(query) {
var key = "Customers_" + query;
return key;
}
In the document’s ready event handler, you initialize the jQuery cache as an internal array named Sample2. Next, you store a reference to the cache array in the appCache global variable. The cache is expected to contain small collections of customers—all with company names matching a given letter in the alphabet. Customers beginning with A are cached in a slot named Customers_A and so forth. The cache key is returned by the cacheGetKey helper function. To load data from the cache, you use the following code:
function loadFromCache(query) {
var key = cacheGetKey(query);
var cachedInfo = appCache[key];
return cachedInfo;
}
If the requested data is not in the cache, it is fetched directly from the source. In the following example, I’ve used a Web service referenced through the ScriptManager control. It goes without saying that any other approach would work as well, including using any of the APIs provided by the jQuery library for remote calls—ajax, getJson, load.
function loadFromSource(query) {
var filterString = query;
Demo.Nwind.LoadByName(filterString,
loadFromSource_onSuccess, null, query);
}
function loadFromSource_onSuccess(results, context) {
var key = cacheGetKey(context);
appCache[key] = results;
fillViewInternal(results, false);
}
The Web service (nwind.asmx) features one method, called LoadByName. The method gets a string parameter and returns customer records with a matching CompanyName column. As usual, the service is invoked asynchronously and triggers a callback once data has been successfully downloaded. The completion callback stores data in the local cache and orders a refresh of the view.
Refreshing the view actually means populating the accordion, as the following code snippet demonstrates:
function fillViewInternal(data) {
accordionReset();
accordionDataBind(data);
}
Every time the user clicks to select customers beginning with, say, A, the existing accordion is destroyed and rebuilt.
function accordionReset() {
$("#accCustomers").accordion('destroy');
$("#accCustomers").empty();
}
The jQuery UI accordion doesn’t support the programmatic addition and deletion of panels. For this reason, the only viable option is resetting any dynamic changes made to the DOM first and emptying the accordion’s DOM subtree later. Figure 2 shows the user interface of the sample page.
.png)
Figure 2. A data-driven accordion.
At this point, just one step is left, but it is also the most important and delicate—creating an accordion dynamically.
Manual Client-Side Data Binding
Because the accordion doesn’t support dynamic extension, to build one whose content is determined by a database query, you need to work at the HTML level. In other words, you loop through the collection of retrieved data (customers in this case), and for each build the HTML expected by the accordion for a panel. As mentioned, this HTML is commonly a pair of H3 and DIV tags placed side by side. The following code snippet shows one possible way of proceeding:
function accordionDataBind(data) {
for (var i = 0; i < data.length; i++) {
var h3 = '<h3><a href="#">' + data[i].CompanyName + ' </a></h3>';
var div = '<div>' + instantiateTemplate(data[i]) + '</div>';
var content = $(h3 + div);
content.appendTo("#accCustomers").show();
}
$("#accCustomers").accordion();
}
For each bound item, you first build a string having the following template: <H3><a> content </a></H3>. All this sets the header of the accordion panel, but what about the content? You create another string according to the template <div> content </div>. When you’re done, you simply concatenate the two strings and transform them into a DOM subtree by using the $ function of the jQuery library.
var content = $(h3 + div);
content.appendTo("#accCustomers").show();
As in the preceding code snippet, you use the appendTo method to attach a piece of HTML markup to a HTML container. In this way, you achieve an important result. The accordion shows as many panels as there are customers in the database whose names match the currently selected letter.
In general, an accordion shouldn’t pose any performance issue because it is mostly made of client-side code. However, client side widgets are not always lightweight aggregates of code and should always be used in pages with due forethought. This is to say that using accordions is safe, but to the extent possible, you should limit the number of panels it shows to just a few.
An accordion is made of panels and a panel is a DIV of any possible complexity. Loading too many panels and too many HTML elements simply leads to bloated pages with a tidy and clean interface. Don’t be fooled by the fact that your pages looks nice and that users are enthusiastic about them—a long list of accordion panels is hardly a great idea because it leads to heavyweight pages.
How can you generate a template on the fly to show any content associated with a customer?
The best you can do here is to create an HTML template and bind it to the current data item in a pure client-side data-binding approach. The jQuery library doesn’t offer anything specific as far as data binding and HTML templates are concerned. So you have two options. One is creating your templates and instantiation code manually. The other option entails using the newest ASP.NET AJAX 4 library and taking advantage of built-in template rendering and client-side binding features.
In this article, I’ll take the longer route and implement a handmade data binding engine.
The next code snippet shows the user control that represents the template for the customer displayed within an accordion’s panel:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<div id="customerTemplate" style="display:none;">
<table border="0">
<tr>
<td class="custContactLabel">Contact</td>
<td class="custContactText">#ContactName</td>
</tr>
<tr>
<td colspan="2">
<span>#Address</span><br />
<span>#City</span>, <span>#Country</span>
</td>
</tr>
<tr>
<td colspan="2">
<span>#Phone</span><br />
</td>
</tr>
</table>
</div>
The template is a plain chunk of HTML markup except that it contains some special placeholders in the form of #word expressions. A special JavaScript method then takes care of parsing this text and replaces any occurrences of #word with the value of the property word on the current data item object. This is exactly what happens in the following code:
function instantiateTemplate(dataItem) {
var pattern = /#\w+/g;
var template = $("#customerTemplate").html();
var matches = template.match(pattern);
for (i = 0; i < matches.length; i++) {
var valueMember = expand(dataItem, matches[i]);
template = template.replace(matches[i], valueMember);
}
return template;
}
function expand(dataItem, match) {
var memberName = match.slice(1);
return dataItem[memberName];
}
The function first loads the HTML from the user control. This is done through the html jQuery function. Next, JavaScript regular expressions are used to find all substrings in the template that match the #word pattern. (By the way, there’s no special reason for using the #word pattern. It is an entirely arbitrary choice that I made simply because I’m not that adept at dealing with more sophisticated regular expressions.)
For example, the #ContactName placeholder is replaced by the value returned by the following expression:
dataItem["ContactName"]
where dataItem is the customer object associated with the currently open accordion panel.
Wrapping Up
The result is that you can effectively bind downloaded data to an accordion without sacrificing flexibility. You can, in fact, change the customer’s template at will, and as long as you don’t alter the binding syntax, the rest of the code continues to work as usual. Furthermore, you can make your code as complex as you want; if the number of simultaneously displayed customers is small, the impact on the browser is limited. Finally, the use of the jQuery cache reduces significantly the workload on the server and keeps the application as responsive as possible for the user’s joy.
About the Author
Dino is the author of "Programming ASP.NET MVC" for Microsoft Press and also coauthored the bestseller "Microsoft .NET: Architecting Applications for the Enterprise" (Microsoft Press 2008). A long time author and experienced consultant and trainer, Dino lives in Italy (when not traveling) and plays tennis (when not injured).
Comments (0)