agile-ajax

Implementing linked multiselects with jQuery, LiveQuery, and Low Pro: Part 2: First pass at the actual code

Low Pro for jQuery

In last week's post, I introduced the linked multiselect widget I was asked to implement on a tight deadline for an unexpected project assignment. I showed some demo code in action and discussed the user experience issues that shaped my requirements. This week, I'll walk through the actual code - or at least my first pass at it.

Like a lot of developers who should know better, I sometimes shirk the technical design phase on quick projects, then regret it later. The code I handed off for this project got the job done, but it wasn't very DRY or elegant. Luckily, I've continued to refine it into something I'm not ashamed to blog about. Next week, I'll show off the final, refactored code and try to draw some conclusions about the entire experience. But first - the original, unrefactored code:

The good

Working with new jQuery plugins always makes a project more interesting. I decided now was the time to get started with Dan Webb's Low Pro for jQuery, a library that adds class-based inheritance and other syntactic conveniences to the jQuery object, including an attach method for linking DOM nodes to prepackaged behaviors. The best, most reusable code I wrote for my initial pass at this project was constructed using Low Pro.

One requirement for my project was the creation of a simple color fade effect to signal DOM updates to the user. Using Low Pro, I was able to abstract this single behavior into its own reusable class for deployment within my larger application. The code looks like this:

//a reusable class to add
//a classic Web 2.0 yellow fade
//to DOM elements that have changed
SignalChange = $.klass({
  defaults: {
    //length of the fadeout
    duration: 1250
    //baseline and highlight colors
    , defaultBorderColor: "#ccc"
    , defaultBackgroundColor: "#eee"
    , highlightBorderColor: "#000"
    , highlightBackgroundColor: "#ffa"
  }
  , initialize: function(options) {
    var opts = $.extend({}, this.defaults, options);
    $.extend(this, opts);//just copy over all the options
  }
  , onredraw: function() {
    var that = this;
    this.element
      //cancel previous events of this type
      .stop("onredraw")
      .css({
        backgroundColor: that.highlightBackgroundColor
        , borderColor: that.highlightBorderColor
      })
      .animate({
        backgroundColor: that.defaultBackgroundColor
        , borderColor: that.defaultBorderColor
      }, this.duration)
    ;
  }
});

$('.multiselect .scrollbox').attach(SignalChange);

jQuery pros will notice that the CSS magic in my onredraw method relies on jQuery Color Animations, a tiny little add-on by jQuery creator John Resig that extends the library's core FX mechanism to handle color transformations. In fact, I had to hack the plugin itself, since it handles borderBottomColor, borderLeftColor, borderRightColor and borderTopColor - but not borderColor. Strange, that, but I got over it. I wanted my color fade effect to support borders as well as backgrounds, and it seemed easier to add a single item to an array in the plugin than to handle each section of the border separately in my own behavior.

The syntax of Low Pro's klass method looks pretty similar to Prototype's Class mechanism, which makes sense. The original Low Pro is a library for making Prototype more like jQuery, while Low Pro for jQuery is designed to give Prototype-style support for large, complex and subclassable objects to jQuery.

Notice some of the elegance in Webb's syntax: Any method that conforms to the onwhatever naming convention is assumed to be an event listener - even if it's a custom event such as my own onredraw. Once I've pointed my SignalChange behavior at a DOM node using the attach method, I just need to fire that event to trigger my custom listener. The same is true for native events.

Given the simplicity of my behavior, it could easily have been constructed as a plain old jQuery plugin. But even for such a low-level behavior, Low Pro offers some advantages:

  • If Live Query is running, then any calls to Low Pro's attach method are routed through Live Query so they'll be applied to all matching DOM nodes, even those constructed subsequently via DHTML or Ajax.
  • I can also subclass my behavior more easily using Low Pro than jQuery's built-in plugin mechanism.

This inheritance becomes all the more important when your behaviors are more complex, with multiple linked event listeners. Because you can just subclass a Low Pro klass, you don't need to overload it with some insane options hash. You can just create a new subclass for each slightly different implementation. I'll take advantage of this mechanism in the refactored code I show off next week.

The not-so-good

In the meantime, I have to take responsibility for the other Low Pro klass I created in this initial version: Subscriptions, which you can view in full in the source code. Before I get into this code's deficiencies, let's look at what it does right:

  • It makes great use of jQuery Templates to simplify the construction of DOM nodes. Templates such as the following keep me from having to concatenate strings to create my on-the-fly markup:
      this.tmplMemberView = $.template(
        '<div class="item">' +
          '${name}' +
          ' <span class="info">(' +
          '${groupCount:pluralize(group)})</span>' +
          '</div>'
      )

    I even added some useful helper methods to the template method so I could customize the formatting of my templates:

    //add some additional template helpers
    //for the template plugin;
    //use these to format our markup
    $.extend($.template.helpers, {
      addConditionalClass: function(value, trueClass, falseClass) {
        return value === true ? trueClass : falseClass;
      }
      , addIfTrue: function(value, conditionalText) {
        return value === true ? conditionalText : '';
      }
      , addIfFalse: function(value, conditionalText) {
        return value === false ? conditionalText : '';
      }
      //only works for standard plurals
      , pluralize: function(value, label) {
        return value + " " + label + (value != 1 ? "s" : "");
      }
      , count: function(value, label) {
        var r = value.length || 0;
        if (label) {
          r = $.template.helpers.pluralize.call(this, r, label);
        }
        return r;
      }
    });
  • It leverages Live Query to attach permanent behaviors to DOM nodes that get destroyed and recreated many times over the application lifecycle.
  • It does a good job of wiring up the complex interactions between two interdependent linked multiselects.

However, my code accomplishes most of these things in a most inelegant way. Looking back, I should have modeled a more complex object hierarchy with some sort of reusable LinkedMultiselect object. My Subscriptions object should have been responsible solely for wiring together two of these LinkedMultiselects and enforcing data dependencies between them.

By starting with one great big all-encompassing class, I ended up having to everything four times: once each for the "selected" and "unselected" states of each of the two linked multiselects. The result is code like this, in which I have to create a single method and then create four more methods to bind it to different circumstances:

, getSubGroup: function(coll, prop, state) {
  return $.grep(coll, function(n) {
    return n[prop] === state;
  });
}
, getSubscribedMembers: function() {
  return this.getSubGroup(this.members, "subscribedNow", true);
}
, getUnsubscribedMembers: function() {
  return this.getSubGroup(this.members, "subscribedNow", false);
}
, getEligibleGroups: function() {
  return this.getSubGroup(this.groups, "eligibleNow", true);
}
, getIneligibleGroups: function() {
  return this.getSubGroup(this.groups, "eligibleNow", false);
}

By now, we've all been exposed to enough proper object-oriented JavaScript to do better than this. I know I certainly have, but I fell into the trap of coding rather than designing first. I look forward to showing you the refactored version next week.

Leave a comment

Powered by WP Hashcash

Who is Pathfinder?

Topics

Search

WordPress

Comments about this site: info@pathf.com