Remote Synthesis
Search my blog:
Viewing By Entry / Main
Sep 23, 2009

Building a jQuery Scheduler (without ColdFusion or a Database)

This post is covering a little experiment of sorts of mine that I built using some jQuery and straight JavaScript to add a very basic scheduler application to the RIA Unleashed agenda page (plug: coming to Boston November 13th) which lives within a Mango blog implementation. While most of you won't find the need to duplicate the exact functionality of this application, I do think it presents an opportunity to cover some really worthwhile techniques with jQuery. The interesting points to me is that jQuery/JavaScript made it easy for me to use existing content on the site to build this mini-application without the need for any server side calls or database data. Plus, as any of you familiar with jQuery might expect, it was really quick and easy to write.

Populating with Existing Content
One of the first requirements I had for building this application was that I didn't want to have to reenter existing data. I already had an agenda as well as a list of topics that each speaker was speaking on, and I really didn't feel like retyping all of that into a database or elsewhere. Luckily, using a combination of the jQuery load() method and some advanced selector techniques, I didn't need to. Here's how...

If you are familiar with jQuery you may already know that you can use the load() method on an object to populate its contents with the result of an HTML get. What you may not know, though, is that you can actually append selectors onto that request to filter the results. For instance, since the page I am getting is the entirety of my topics page including design, I only wanted the contents of the "content" div, and I did so by adding "#content" to my get request, like below.

$("#schedule").load('/page.cfm/topics #content');

In this way, I am placing only the content of that specific div (i.e. content) into a hidden div (i.e. schedule) on my page. I do this so that I can more easily and quickly parse the contents to get session descriptions which populate a popup. The popup is created using the popup() method from jQuery - the only caveat here is that, sincewe create it once when the page loads and change its contents as needed, we need to make it not open when created which is the default.

$("#sessionInfo").dialog({autoOpen: false,width: 440});

Overriding Existing Links
The existing agenda already linked the author name to the author session information on the topics page using an anchor. As I said, I didn't want to have to change the existing content. Instead, I decided to override the link behavior and capture which anchor you were loading to display the content in a popup. A portion of my agenda HTML code (which wasn't changed for this app other than the addition of the hidden divs) is shown below to offer context to this discussion.

<div id="schedule" style="display:none;">

</div>
<div id="sessionInfo" style="font-size:12px;"></div>
<table id="scheduleTable" border="0" cellspacing="0" style="font-family:Arial, Helvetica, sans-serif;font-size:12px;color:#FFFFFF;">
   <th></th>
   <th width="162">Flex and AIR Track</th>
   <th width="162">ColdFusion Track</th>
   <th width="162">UX & Related Tech Track</th>
   <tr>
      <td>10:40 am</td>
      <td rowspan="2" valign="top" style="border:1px solid #FFFFFF;text-align: center;"><a href="page.cfm/agenda/topics#jefftapper">Jeff Tapper</a></td>
      <td rowspan="2" valign="top" style="border:1px solid #FFFFFF;text-align: center;"><a href="page.cfm/agenda/topics#stevenerat">Steven Erat</a></td>
      <td rowspan="2" valign="top" style="border:1px solid #FFFFFF;text-align: center;"><a href="page.cfm/agenda/topics#rachellehman">Rachel Lehman</a></td>
   </tr>
   <tr>
      <td> </td>
   </tr>
</table>

jQuery makes it blazingly easy to capture every link's click behavior using a simple selector. What I do then is simply override that click with a JavaScript function that returns false, thereby telling the browser not to follow the link. Instead, we replace the click behavior with the JavaScript to open the popup. Here's the basic code for that function, though I left out the portion that grabs the content to populate the popup (which we will discuss in the next section). The important thing is that the $("#scheduleTable a").click() line replaces the click functionality of every link within my schedule table.

$("#scheduleTable a").click(function () {
   // populate the dialog
   $("#sessionInfo").dialog("open");
   return false;
});

In fact, I use the same technique to toggle the background color of the table cell when you click on it (to indicate it is your chosen session or not). The only other interesting item going on here is that I first check to see if it is a cell that has a link within it (i.e. it is an actual session). Using jQuery selectors, I am also able to remove any selections on adjoining table cells with a single line by simple selecting the siblings (i.e. other table cells in this row) of the selected cell. That, in my opinion, is some pretty powerful stuff. (Note: we'll get into what my saveSessions() method does in the persisting selections section).

$("#scheduleTable td").click(function () {
   if ($(this).children('a').length > 0) {
      $(this).toggleClass('selected');
      $(this).siblings().removeClass('selected');
      saveSessions();
   }
});

Parsing/Selecting HTML Content
jQuery makes most parsing or selecting content within the DOM into a one-liner, assuming you get used to using selectors and traversing the DOM. Of course, this is made much easier when you have a clean DOM (which isn't always the case with clients that edit their own HTML). It works fine when you don't but the type of assumed selections I will be doing here may break if the formatting isn't correct.

Let's take an example. In this updated version of the link click function I first parse the anchor portion of the link using a standard JavaScript string split() function. Next , I use some jQuery to, in one line, populate the popup with the appropriate section from our topics page content (recall, we stuck this in a hidden div in an earlier section). The line is: $("#sessionInfo").html($("a[name='" + a[1] + "']").parent().html()). What it does is set the html of the sessionInfo div to the html contents of the parent of the html anchor matching the anchor on the called link (yes, that's a little confusing). The selector $("a[name='" + a[1] + "']") says, get the <a> in the dom with a name attribute matching the second (remember JavaScript arrays start at 0) item in our split of the href attribute in the line above (which would be the anchor portion of the link). Using the parent() method, we choose the div containing this anchor and simply apply its contents. Now, when you click the link the popup will open with the session description populated for the appropriate session.

$("#scheduleTable a").click(function () {
   a = this.href.split('#');
   $("#sessionInfo").html($("a[name='" + a[1] + "']").parent().html());
   $("#sessionInfo").dialog("open");
   return false;
});

I use a similar way of traversing through the DOM on the saveSessions() method (which I will discuss in more detail in the next section). Let's look at this line and see what it does:

var thisTopic = $(sessions[i]).children('a:first')[0].href.split("#")[1];

In this case, I am getting an item array of DOM objects (session[i]). I am then telling jQuery to give me only the first child of type <a> by saying children('a:first') (which still returns an array, and thus the need for [0]). I am then getting the value of the href as we did in the earlier code and splitting out the anchor value (again, split() is not a jQuery thing, just straight JavaScript).

As you can see, its pretty straightforward to do complex DOM selectors in just single lines of code. Of course, its not always the most readable line of code, so be sure to comment liberally.

Persisting Selections
The cookie methods I used are createCookie, readCookie and eraseCookie and were taken from a Quirksmode post. There is a jQuery cookie plugin which I found out about afterwards, but these methods work fine for my purposes. What I do is simply put a comma-delimited list of the anchors for the chosen sessions into the cookie and then, when the page loads, read that list in and highlight the appropriate table cells. Let's look at how.

Earlier we mentioned the saveSessions() JavaScript function I created for persisting the selected items. At first I thought it would be easier simply to add and remove items from the list in the cookie, but this became problematic (though I cannot recall exactly why). In the end, I decided to create a method that loops through all the agenda table cells and see which ones you have selected by looking for cells that contain an <a> tag ($(sessions[i]).children('a').length > 0) and then check for the .selected class ($(sessions[i]).hasClass("selected") == true) and finally build the cookie list that way on each save.

function saveSessions() {
   // get the td children in our table
   var sessions = $("#scheduleTable td");
   var newCookieVal = "";
   for (var i = 0;i < sessions.length;i++) {
      if ($(sessions[i]).children('a').length > 0) {
         if ($(sessions[i]).hasClass("selected") == true) {
            var thisTopic = $(sessions[i]).children('a:first')[0].href.split("#")[1];
            newCookieVal += thisTopic + ",";
         }
      }
   }
   createCookie("riaUnleashedSch",newCookieVal,90);
}

Next, I needed to reload these selections when you return.

Inside of my $(document).ready() block (seen below), I first see if the cookie exists and, if so, read it in. If the cookie does not exists, I create one but set it to an empty value. Next, I get get an array of all the <td> elements within my schedule table (var sessions = $("#scheduleTable td");) and an array of the cookie values (var selectedSessions = cookieVal.split(",");). I loop through all the sessions and, if it contains an <a> element (if ($(sessions[i]).children('a').length > 0)), I get the anchor value and then loop through the cookie values to see if that value exists. If it does, I add the highlighted class (.selected) and make sure it is turned off for all the other <td> values in that row.

This code will ensure that if you have some selections persisted, they will reappear when you return to the page. Looking back, there are definitely ways to clean up this code but, keep in mind, this was a quick and dirty app.

$(document).ready(function () {
   var cookieVal = "";
   if (readCookie("riaUnleashedSch") == null) {
      createCookie("riaUnleashedSch",cookieVal,90);
   }
   else {
      cookieVal = readCookie("riaUnleashedSch");
   }

   // get the td children in our table
   var sessions = $("#scheduleTable td");
   var selectedSessions = cookieVal.split(",");
   for (var i = 0;i < sessions.length;i++) {
      if ($(sessions[i]).children('a').length > 0) {
         var thisTopic = $(sessions[i]).children('a:first')[0].href.split("#")[1];
         if (thisTopic.length) {
            for (var x = 0;x < selectedSessions.length;x++) {
               if (selectedSessions[x] == thisTopic) {
                  $(sessions[i]).toggleClass('selected');
                  $(sessions[i]).siblings().removeClass('selected');
               }
            }
         }
      }
   }
}

Finishing Up
Well, while this might not be the most groundbreaking application in the world, it did meet all my requirements: a) used existing content; b) no server-side requirement; c) no database needed. Could I have built this in pure JavaScript without jQuery? Of course, I could have, but then you'd be reading this sometime next year.

On another note, I am considering a post focusing specifically on advanced selector and DOM traversing techniques in jQuery if there is interest. I am not as involved in the jQuery community, so perhaps this topic is overdone...if so, provide feedback and let me know.

Comments
Dan Wilson
Very cool post Brian.


Write your comment



(it will not be displayed)