home

Foundations of Programming 2 - Appendix B - Advanced jQuery

21 Mar 2011

Appendix B - Advanced jQuery

jQuery is a rewarding library to learn, not only because what you learn in the first 30 minutes can be of real use, but also because the foundations you learn early on serves as the backbone for more advance usage. This chapter is dedicated to a few of those more advanced jQuery usages.

Custom Selectors

Our story, thus far, has been that jQuery uses CSS selectors (including some of the more advanced CSS3 selectors). While this is true, jQuery also has its own selectors which are quite handy. These custom selectors are easy to spot as they always begin with a colon. We already saw one in our final example of the previous chapter, :first. There's also a corresponding :last selector. Beyond these, and a handful of others there are a few which are worth pointing out.

:not

The :not selector looks for the inverse of the supplied selector. You use it like so:

var $notFirst = $('.tabs a').filter(':not(:first)');
//or
var $notFirst = $('.tabs :not(a:first)');

As you can see :not is special, though not unique, as it takes a sort of argument to work.

:eq

The :eq selector, like :not, takes an argument which is the index of the element we want to find:

//yes, it's base zero
var $secondLink = $('a:eq(1)');

It isn't too uncommon that you'll see and use this ugly code:

var $item = $('td:eq(' + i + ')');

Where i would be the index of a cell you want to find.

:visible and :hidden

:visible and :hidden aren't particularly advanced, but they are useful enough to point out. They work like you think, and they work surprisingly well. They both go beyond simply checking the CSS visibility of an item, but also its height and all types of things which really indicate whether the item is visible or not. In our tabs example from the previous chapter, instead of potentially hiding already hidden panels, we could have done:

//only hide the visible panels
$panels.filter(':visible').hide();

Though, to be honest, I think it's simpler just to hide them all.

Writing Your Own

Occasionally you might need to write your own selector. This doesn't happen often, but on a few occasions I've had need to find elements by their innerText value (there's a :contains selector, but that does a fuzzy search, I wanted an exact one). jQuery makes this easy:

$.expr[':'].textIs = function(obj, index, meta, stack){
        return obj.innerText == meta[3];
};

We attach our own function to jQuery's $.expr[:] object named textIs. The first parameter is the DOM element being compared (we could convert that to a jQuery object if we needed to). The second parameter is the index of this particular element with respect to all potential matches. The meta parameter is information about the actual selector - you can think of it as the captures from a regular expression, and you'll probably need to console.log(meta) to get a sense for what part you are interested in. The last parameter is an array of all the potentially found elements (so stack[index] == obj) - this is useful if your selector is doing something relative to other elements.

Hierarchy Flattening

There's at least one thing which isn't intuitive about how selectors work (well, to me at least). Despite the fact that the DOM is a hierarchy, the jQuery method always flattens results. What do I mean by this? Say we wanted to add a class to the last column of every row. You might be tempted to try:

$('tbody tr').children('td:last').addClass('last');

But if your table has more than 1 row, this isn't going to work. When you call children, the cells aren't grouped by rows, they are flattened into a single array. Therefore, the above code is only going to apply the last class to the very last cell of the entire table. The solution is to use the each method:

$('tbody tr').each(function(index, tr)
{
    $(tr).children(':last').addClass('last');
});

The each method, along with events, is where you are most likely to turn a DOM element into a jQuery object.

DOM Creation

We've seen how we can use the jQuery method to get a jQuery object form a CSS selector or from an existing DOM element. The method actually has a third usage: creating new elements. If you pass $ an HTML tag, it'll create the corresponding element:

var $div = $('<div>');

You can even get fancy:

var $div = $('<div class="neato">neat!</div>');
//or
var $div = $('<div>').addClass('neato').text('neat!');

Note that an element created this way isn't added to the DOM by default - only you know where it ought to go:

var $div = $('<div class="error">').appendTo($('#header'));

AJAX

jQuery has a handful of methods which makes writing ajax code simple. The first two are $.get and $.post which, as you can probably guess, issue an ajax get and post respectively. They both take 4 parameters: the URL, the data, a callback, and a return type:

$.get('/user', {id: 123}, function(response, status)
{
    //do something
}, 'json');

$.post('/game', {name: 'TileFlod', version: 2}, gameSaved, 'html');
function gameSaved(response, status)
{
    //do something
}

This is probably a good time to point out that given a form object, you can call serialize on it to get submittable data:

$.post('/game', $('#addGame').serialize(), ..., ...);

Both of the above methods actually abstract the more powerful $.ajax method. This method takes a single object literal:

$.ajax(
{
    async: false,
    cache: false,
    url: '/game',
    type: 'get',
    success: function(response, status) {

    },
    //and so on
});

Finally, the last ajax method that we'll look at is the load plugin. load abstracts $.get or $.post (depending on whether data is provided), and automatically loads the response into the calling object:

$('#myDiv').load('/about/moreinfo');

Which is equivalent to:

$.get('/about/moreinfo', null, function(response)
{
    $('myDiv').html(response);
}, 'html');

Delegates

Sooner or later you're going to want to sprinkle your jQuery goodness over dynamically loaded HTML. The most common example is a list of rows, with a click event (to see the details) which dynamically grows/changes. An ugly solution, which might be quite tricky at times, is to hook events as your new rows come in.

//called initially for the statically loaded rows
var $list = $('#myList');
$list.find('tr').click(showDetails);

function showDetails(row)
{
    //do something
}

//somewhere else in your code:
$('#add').click(function()
{
    $.post(URL, PARAMS, function(newRow)
    {
        $(newRow).click(showDetails).appendTo($list.find('tbody'));
    });
});

You aren't only repeating the code to set up the click, but you're also polluting the save process with details about the table's display. There's yet another problem with the above approach - it doesn't scale. I know talking about scalability with respect to "client-side" code might seem odd, but add enough event handlers and you really might start having performance issues.

The solution? jQuery has a delegate method which works by attaching a single event to a parent element which then watches (or delegates) to child elements. Let's rewrite the above code to use it:

var $list = $('#myList');
$list.delegate('tr', 'click', showDetails);

function showDetails(row)
{
    //do something
}

That's it. Rows which are dynamically added later are automatically covered by the delegate since it exists on their (future) parent table. The code says that whenever a tr element within #myList is clicked execute showDetails. The selector, the first parameter, can be any jQuery selector. The implementation relies on bubbling, which does mean that for some events it won't work, but for the most common (like click) it's an extremely powerful solution.

Its worth pointing out that jQuery also has a live method, which is very similar to delegate:

$('tr').live('click', showDetails);

function showDetails(row)
{
    //do something
}

While the syntax for live is nicer, it lacks a scope that delegate has. This results in poorer performance, and also less control (you might not want to apply this to every tr on the page).

Writing Plugins

So far we've looked at a number of built-in jQuery methods which, while useful on their own, truly shine when used within plugins. I like to think of plugins as belonging to one of two categories. The first category is for UI plugins, like tabs or dialogs. The second is for more task specific plugins, often bringing multiple UI plugins together to accomplish something pretty specific within a page/app. Both are built the exact same way, the only difference is that you want to make sure UI plugins are truly reusable. (It's worth noting that there's a large number of existing quality and free UI plugins available for jQuery, just google for them).

Whenever I write a plugin I start with a basic template. At first, parts of the template might seem difficult. We'll go over those difficult parts, but even if you don't fully understand it now, you can easily use it and safely ignore the plumbing. First though, when you call a method on a jQuery object, be it a built-in method or a plugin, that method exists within the the jQuery.fn object. So, a basic example might look something like:

//remember $ and jQuery are the same thing
$.fn.tabs = function()
{
        //do something
};

Since it's possible for another library to define $ (this was a common problem before jQuery become overwhelmingly popular), there's a safer way to write the same code:

(function($))
{
    $.fn.tabs = function()
    {

    };
})(jQuery);

I know it looks a little crazy, but it's quite neat and worth understanding. There's nothing more complicated here than defining a dynamic method and passing in a parameter. Look at a more explicit example:

(function(question, answer)
{
    alert(question + ' ' + answer);
})('its over', 9000)();

We define a method that takes 2 parameters, question and answer, and then invoke it with with two values its over and 9000.

The jQuery example is the same, except it's a single parameter which we name $ and we pass in the jQuery object. The effect is that even if $ is defined as something else globally, within our dynamic method (technically a closure), it's simply a parameter which we've assigned to jQuery (another library could always come along and redefine jQuery, but that's less likely).

The other thing that's important to remember when writing a plugin is that, like most built-in jQuery methods, you should write your plugin so that it both works on an array of jQuery objects and so that it returns the jQuery object (this allows your plugin to be used in a method chain).

With that out of the way our template (which follows the two rules we've discussed above) looks like:

(function($)
{
  var defaults = {};
  $.fn.PLUGINNAME = function(options)
  {
    var opts = $.extend({}, defaults, options);
    return this.each(function()
    {
      if (this.PLUGINNAME) { return false; }
      var self =
      {
        initialize: function()
        {

        }
      };
      this.PLUGINNAME = self;
      self.initialize();
    });
  };
})(jQuery);

All you need to do for your own plugin is copy the above and replace the three instances of PLUGINNAME with the name of your own plugin. The defaults variable is used for your plugins default values - which can be overwritten by supplying a options when creating your plugin. Notice the call return this.each, this is the magic that makes our plugin work against and array of jQuery object and which returns that array. We also store our plugin within the item by calling this.PLUGINNAME = self and when we setup the plugin, we return if this.PLUGINNAME is not null - this makes sure that, for a given element, our plugin is only defined once.

Let's use this template to rewrite our tabs code from the previous chapter to get a better idea of how this all actually works:

<style>
    a.active{font-weight:bold;}
</style>

<ul class="tabs">
    <li><a href="#main">main</a></li>
    <li><a href="#about">about</a></li>
    <li><a href="#download">download</a></li>
</ul>
<div id="mainPanels">
    <div id="main" class="panel">This is the main panel</div>
    <div id="about" class="panel">this is the about panel</div>
    <div id="download" class="panel">this is the download panel</div>
</div>

(function($)
{
  var defaults = {initialTab: ':first', panelContainer: null};
  $.fn.tabs = function(options)
  {

        //overwrites the detauls with the supplied options
    var opts = $.extend({}, defaults, options);

        return this.each(function()
        {
      if (this.tabs) { return false; }

            //we can define fields for our plugin to have access to
            var $tabContainer = $(this);
            var $links = $tabContainer.find('a');
            var $panelContainer = $(opts.panelContainer);

        var self =
        {
        initialize: function()
        {
                    self.hidePanels();
            $links.click(self.clicked).filter(opts.initialTab).click();
        },
                clicked: function()
                {
                    var $link = $(this);
                    $links.removeClass('active');
                    $link.addClass('active');
                    self.hidePanels();
                    $panelContainer.find($link.attr('href')).show();
                },
                hidePanels: function()
                {
                    $panelContainer.find('.panel').hide();
                }
      };
      this.tabs = self;
      self.initialize();
    });
  };
})(jQuery);

We've made some slight modifications to make the plugin more reusable, but for the most part not much has changed. We can call this plugin by simply doing:

$('.tabs').tabs({panelContainer: '#mainPanels'});

Most of my plugins follow the same pattern. The real trick, as I mentioned before, is to keep plugins focused. As you start to write plugins, there'll likely be some discomfort about purpose. Each of your page will likely have unique needs and it might be tempting to write a large plugin for each. You can start this way, but try to refactor common code out into their own plugins.

In This Chapter

Over the last two chapter's we've seen the power of jQuery. Yet, for all its flexibility, the truth is that jQuery is an extremely simple and focused library. If you understand and remember the fundamentals - how the $ can be used, understanding the relationship between a DOM element and its jQuery wrapper, and knowing what this means in a given context - then you'll easily master jQuery. It's a library worth knowing, not just because of how useful it is, but also because it showcases what a good library should be like - focused and simple, yet somehow easily extensible. Don't get overwhelmed though, since we did cover a lot of material. Start small and, at your own pace, move forward.

blog comments powered by Disqus