O'Reilly logo

jQuery Cookbook by Cody Lindley

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. jQuery Utilities

Jonathan Sharp

Introduction

Often, when thinking and talking about jQuery, the main concepts that come to mind are DOM and style manipulation and behavior (events). Yet there are also a number of “core” features and utility functions tucked away for the developer’s benefit. This chapter is focused on exposing, disclosing, and explaining these not-so-common utility methods of jQuery.

4.1. Detecting Features with jQuery.support

Problem

You need to attach a special click handler to all anchor tags that have just a hash for the current page, and you don’t want to risk it breaking because of browser support issues.

Solution

(function($) {
    $(document).ready(function() {
     $('a')
         .filter(function() {
             var href = $(this).attr('href');
             // Normalize the URL
             if ( !jQuery.support.hrefNormalized ) {
                 var loc = window.location;
                 href = href.replace( loc.protocol + '//' + loc.host + loc.pathname, 
'');
             }
             // This anchor tag is of the form <a href="#hash">
             return ( href.substr(0, 1) == '#' );
        })
        .click(function() {
            // Special click handler code
        });
    });
})(jQuery);

Discussion

The jQuery.support object was added in version 1.3 and contains Boolean flags to help write code using browser feature detection. In our example, Internet Explorer (IE) has a different behavior in how it handles the href attribute. IE will return the full URL instead of the exact href attribute. Using the hrefNormalized attribute, we have future-proofed our solution in the event that a later version of IE changes this behavior. Otherwise, we would have needed a conditional that contained specific browser versions. While it may be tempting, it is best to avoid this approach because it requires future maintenance as new versions of browsers are released. Another reason to avoid targeting specific browsers is that it is possible for clients to intentionally or unintentionally report an incorrect user agent string. In addition to the hrefNormalized attribute, a number of additional attributes exist:

boxModel

True if the browser renders according to the W3C CSS box model specification

cssFloat

True if style.cssFloat is used to get the current CSS float value

hrefNormalized

True if the browser leaves intact the results from getAttribute('href')

htmlSerialize

True if the browser properly serializes link elements with the innerHTML attribute

leadingWhitespace

True if the browser preserves leading whitespace when innerHTML is used

noCloneEvent

True if the browser does not clone event handlers when elements are cloned

objectAll

True if getElementsByTagName('*') on an element returns all descendant elements

opacity

True if the browser can interpret the CSS opacity style

scriptEval

True if using appendChild for a <script> tag will execute the script

style

True if getAttribute('style') is able to return the inline style specified by an element

tbody

True if the browser allows <table> elements without a <tbody> element

4.2. Iterating Over Arrays and Objects with jQuery.each

Problem

You need to iterate or loop over each element in an array or attribute of an object.

Solution

(function($) {
    $(document).ready(function() {
        var months = [  'January', 'February', 'March', 'April', 'May',
                        'June', 'July', 'August', 'September', 'October',
                        'November', 'December'];
        $.each(months, function(index, value) {
            $('#months').append('<li>' + value + '</li>');
        });

        var days = {    Sunday: 0, Monday: 1, Tuesday: 2, Wednesday: 3,
                        Thursday: 4, Friday: 5, Saturday: 6 };
        $.each(days, function(key, value) {
            $('#days').append('<li>' + key + ' (' + value + ')</li>');
        });
    });
})(jQuery);

Discussion

In this recipe, we iterate over both an array and an object using $.each(), which provides an elegant interface to the common task of iteration. The first argument to the $.each() method is the array or object to iterate over, with the second argument being the callback method that is executed for each element. (Note that this is slightly different from the jQuery collection method $('div').each(), whose first argument is the callback function.)

When the callback function defined by the developer is executed, the this variable is set to the value of the element currently being iterated. Thus, the previous recipe could be rewritten as follows:

(function($) {
    $(document).ready(function() {
        var months = [  'January', 'February', 'March', 'April', 'May',
                        'June', 'July', 'August', 'September', 'October',
                        'November', 'December'];
        $.each(months, function() {
            $('#months').append('<li>' + this + '</li>');
        });

        var days = {    Sunday: 0, Monday: 1, Tuesday: 2, Wednesday: 3,
                        Thursday: 4, Friday: 5, Saturday: 6 };
        $.each(days, function(key) {
            $('#days').append('<li>' + key + ' (' + this + ')</li>');
        });
    });
})(jQuery);

4.3. Filtering Arrays with jQuery.grep

Problem

You need to filter and remove elements in an array.

Solution

(function($) {
    $(document).ready(function() {
        var months = [  'January', 'February', 'March', 'April', 'May',
                        'June', 'July', 'August', 'September', 'October',
                        'November', 'December'];
        months = $.grep(months, function(value, i) {
            return ( value.indexOf('J') == 0 );
        });
        $('#months').html( '<li>' + months.join('</li><li>') + '</li>' );
    });
})(jQuery);

Discussion

This recipe uses the $.grep() method to filter the months array so that it only includes entries that begin with the capital letter J. The $.grep method returns the filtered array. The callback method defined by the developer takes two arguments and is expected to return a Boolean value of true to keep an element or false to have it removed. The first argument specified is the value of the array element (in this case, the month), and the second argument passed in is the incremental value of the number of times the $.grep() method has looped. So, for example, if you want to remove every other month, you could test whether ( i % 2 ) == 0, which returns the remainder of i / 2. (The % is the modulus operator, which returns the remainder of a division operation. So, when i = 4, i divided by 2 has a remainder of 0.)

(function($) {
    $(document).ready(function() {
        var months = [  'January', 'February', 'March', 'April', 'May',
                        'June', 'July', 'August', 'September', 'October',
                        'November', 'December'];
        months = $.grep(months, function(value, i) {
            return ( i % 2 ) == 0;
        });
        $('#months').html( '<li>' + months.join('</li><li>') + '</li>' );
    });
})(jQuery);

4.4. Iterating and Modifying Array Entries with jQuery.map

Problem

You need to loop over each element in an array and modify its value.

Solution

(function($) {
    $(document).ready(function() {
        var months = [  'January', 'February', 'March', 'April', 'May',
                        'June', 'July', 'August', 'September', 'October',
                        'November', 'December'];
        months = $.map(months, function(value, i) {
            return value.substr(0, 3);
        });
        $('#months').html( '<li>' + months.join('</li><li>') + '</li>' );
    });
})(jQuery);

Discussion

In this recipe, $.map() is iterating over the months array and returns the abbreviation (first three characters). The $.map() method takes an array and a callback method as arguments and iterates over each array element executing the callback as defined by the developer. The array entry will be updated with the return value of the callback.

4.5. Combining Two Arrays with jQuery.merge

Problem

You have two arrays that you need to combine or concatenate.

Solution

(function($) {
    $(document).ready(function() {
        var horseBreeds = ['Quarter Horse', 'Thoroughbred', 'Arabian'];
        var draftBreeds = ['Belgian', 'Percheron'];

        var breeds = $.merge( horseBreeds, draftBreeds );
        $('#horses').html( '<li>' + breeds.join('</li><li>') + '</li>' );
    });
})(jQuery);

Discussion

In this example, we have two arrays that contain a list of horse breeds. The arrays are combined in the order of first + second. So, the final breeds array will look like this:

['Quarter Horse', 'Thoroughbred', 'Arabian', 'Belgian', 'Percheron']

4.6. Filtering Out Duplicate Array Entries with jQuery.unique

Problem

You have two jQuery DOM collections that need to have duplicate elements removed:

(function($) {
    $(document).ready(function() {
        var animals = $('li.animals').get();
        var horses = $('li.horses').get();
        $('#animals')
            .append( $(animals).clone() )
            .append( $(horses).clone() );
    });
})(jQuery);

Solution

(function($) {
    $(document).ready(function() {
        var animals = $('li.animals').get();
        var horses = $('li.horses').get();
        var tmp = $.merge( animals, horses );
        tmp = $.unique( tmp );
        $('#animals').append( $(tmp).clone() );
    });
})(jQuery);

Discussion

jQuery’s $.unique() function will remove duplicate DOM elements from an array or collection. In the previous recipe, we combine the animals and horses arrays using $.merge(). jQuery makes use of $.unique() throughout most of its core and internal functions such as .find() and .add(). Thus, the most common use case for this method is when operating on an array of elements not constructed with jQuery.

4.7. Testing Callback Functions with jQuery.isFunction

Problem

You have written a plugin and need to test whether one of the settings is a valid callback function.

Solution

(function($) {
    $.fn.myPlugin = function(settings) {
        return this.each(function() {
            settings = $.extend({ onShow: null }, settings);
            $(this).show();
            if ( $.isFunction( settings.onShow ) ) {
                settings.onShow.call(this);
            }
        });
    };
    $(document).ready(function() {
        $('div').myPlugin({
            onShow: function() {
                alert('My callback!');
            }
        });
    });
})(jQuery);

Discussion

While the JavaScript language provides the typeof operator, inconsistent results and edge cases across web browsers need to be taken into account. jQuery provides the .isFunction() method to ease the developer’s job. Worth pointing out is that since version 1.3, this method works for user-defined functions and returns inconsistent results with built-in language functions such as this:

jQuery.isFunction( document.getElementById );

which returns false in versions of Internet Explorer.

4.8. Removing Whitespace from Strings or Form Values with jQuery.trim

Problem

You have an input form and need to remove the whitespace that a user may have entered at either the beginning or end of a string.

Solution

<input type="text" name="first_name" class="cleanup" />
<input type="text" name="last_name" class="cleanup" />

(function($) {
    $(document).ready(function() {
        $('input.cleanup').blur(function() {
            var value = $.trim( $(this).val() );
            $(this).val( value );
        });
    });
})(jQuery);

Discussion

Upon the user blurring a field, the value as entered by the user—$(this).val()—is retrieved and passed through the $.trim() method that strips all whitespace characters (space, tab, and newline characters) from the beginning and end of the string. The trimmed string is then set as the value of the input field again.

4.9. Attaching Objects and Data to DOM with jQuery.data

Problem

Given the following DOM code:

var node = document.getElementById('myId');
node.onclick = function() {
    // Click handler
};
node.myObject = {
    label: document.getElementById('myLabel')
};

you have metadata associated with a DOM element for easy reference. Because of flawed garbage collection implementations of some web browsers, the preceding code can cause memory leaks.

Solution

Properties added to an object or DOM node at runtime (called expandos) exhibit a number of issues because of flawed garbage collection implementations in some web browsers. jQuery provides developers with an intuitive and elegant method called .data() that aids developers in avoiding memory leak issues altogether:

$('#myId').data('myObject', {
    label: $('#myLabel')[0]
});

var myObject = $('#myId').data('myObject');
myObject.label;

Discussion

In this recipe, we use the .data() method, which manages access to our data and provides a clean separation of data and markup.

One of the other benefits of using the data() method is that it implicitly triggers getData and setData events on the target element. So, given the following HTML:

<div id="time" class="updateTime"></div>

we can separate our concerns (model and view) by attaching a handler for the setData event, which receives three arguments (the event object, data key, and data value):

// Listen for new data
$(document).bind('setData', function(evt, key, value) {
    if ( key == 'clock' ) {
        $('.updateTime').html( value );
    }
});

The setData event is then triggered every time we call .data() on the document element:

// Update the 'time' data on any element with the class 'updateTime'
setInterval(function() {
    $(document).data('clock', (new Date()).toString() );
}, 1000);

So, in the previous recipe, every 1 second (1,000 milliseconds) we update the clock data property on the document object, which triggers the setData event bound to the document, which in turn updates our display of the current time.

4.10. Extending Objects with jQuery.extend

Problem

You have developed a plugin and need to provide default options allowing end users to overwrite them.

Solution

(function($) {
   $.fn.myPlugin = function(options) {
        options = $.extend({
            message: 'Hello world',
            css: {
                color: 'red'
            }
        }, options);
        return this.each(function() {
            $(this).css(options.css).html(options.message);
        });
    };
})(jQuery);

Discussion

In this recipe, we use the $.extend() method provided by jQuery. $.extend() will return a reference to the first object passed in with the latter objects overwriting any properties they define. The following code demonstrates how this works in practice:

var obj = { hello: 'world' };
obj = $.extend(obj, { hello: 'big world' }, { foo: 'bar' });

alert( obj.hello ); // Alerts 'big world'
alert( obj.foo ); // Alerts 'bar';

This allows for myPlugin() in our recipe to accept an options object that will overwrite our default settings. The following code shows how an end user would overwrite the default CSS color setting:

$('div').myPlugin({ css: { color: 'blue' } });

One special case of the $.extend() method is that when given a single object, it will extend the base jQuery object. Thus, we could define our plugin as follows to extend the jQuery core:

$.fn.extend({
    myPlugin: function() {
        options = $.extend({
            message: 'Hello world',
            css: {
                color: 'red'
            }
        }, options);
        return this.each(function() {
            $(this).css(options.css).html(options.message);
        });
    }
});

$.extend() also provides a facility for a deep (or recursive) copy. This is accomplished by passing in Boolean true as the first parameter. Here is an example of how a deep copy would work:

var obj1 = { foo: { bar: '123', baz: '456' }, hello: 'world' };
var obj2 = { foo: { car: '789' } };

var obj3 = $.extend( obj1, obj2 );

Without passing in true, obj3 would be as follows:

{ foo: { car: '789 }, hello: 'world' }

If we specify a deep copy, obj3 would be as follows after recursively copying all properties:

var obj3 = $.extend( true, obj1, obj2 );
// obj3
{ foo: { bar: '123', baz: '456', car: '789 }, hello: 'world' }

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required