Learning jQuery http://www.learningjquery.com Tips, techniques, and tutorials for the jQuery JavaScript library Sun, 14 Apr 2013 23:29:24 +0000 en-US hourly 1 http://wordpress.org/?v=3.5.1 Using jQuery’s .pushStack() for reusable DOM traversing methods http://www.learningjquery.com/2011/12/using-jquerys-pushstack-for-reusable-dom-traversing-methods http://www.learningjquery.com/2011/12/using-jquerys-pushstack-for-reusable-dom-traversing-methods#comments Tue, 20 Dec 2011 18:51:07 +0000 Karl Swedberg http://www.learningjquery.com/?p=1324 The .pushStack() method has been in jQuery since before version 1.0, but it hasn't received a whole lot of attention outside of core developers and plugin authors. While its usefulness may not be immediately apparent, it can come in really handy in some situations, so I'd like to take a quick look at what it does, how it works, and how we can use it.

pushStack Basics

At its most basic level, the .pushStack() method accepts an array of DOM elements and "pushes" it onto a "stack" so that later calls to methods like .end() and .andSelf() behave correctly. (Side note: As of jQuery 1.4.2, you can pass in a jQuery object instead of an array, but that isn't documented and jQuery itself always uses an array, so that's what we'll stick to here.)

Internally, jQuery uses .pushStack() to keep track of the previous jQuery collections as you chain traversing methods such as .parents() and .filter(). This lets us traverse through the DOM, do some stuff, "back up" to previous collections within the same chain using .end(), and then do something else. Here is a somewhat contrived example:

JavaScript:
  1. // select some divs
  2. $('div.container')
  3.   // find some spans inside those divs and add a class to them
  4.   .find('span').addClass('baby')
  5. // pop those spans off the "stack",
  6. // returning to the previous collection (div.container)
  7. .end()
  8.   // add a class to the parent of each div.container
  9.   .parent().addClass('daddy');

Because .find() returns the result of a .pushStack() call to keep track of the previous collection (as does .parent()), we can use .end() in the above example to return to the container divs.

Using pushStack for Fun and Profit

So, this is great for jQuery, but what can .pushStack() do for me and my code? Well, it can help me write specialized DOM traversal plugins that act just like jQuery's own traversal methods. In other words, I can stop chaining the same sets of traversal methods together and instead write a reusable function that still works with with .end() and all that. For example, let's say I often have a need to find an element's grandparent. While I could write $('#myElement').parent().parent() every time, it might be nice to just be able to write $('#myElement').grandparent() instead. A naïve way to write a grandparent plugin would look like this (changing the method name to "grandpa" for this example):

JavaScript:
  1. // NOT recommended!
  2. (function($) {
  3.   $.fn.grandpa = function() {
  4.     return this.parents().parents();
  5.   };
  6. })(jQuery);

The problem here is that two new jQuery object instances are added to the stack. So, let's see what happens when we use it:

JavaScript:
  1. // The DOM looks like this:
  2. // <div class="grandpa">
  3. //  <div class="pa">
  4. //    <div class="child son"></div>
  5. //  </div>
  6. // </div>
  7.  
  8. var elem = $('div.son').grandpa().end();
  9. $('div.son').text( elem.attr('class') );

Without seeing the plugin, we would expect to see "child son" inserted into <div class="son">, but "pa" is inserted instead. Each .parent() call in the plugin adds to the stack, so using .end() only pops the second one off.

If we use .pushStack() instead, however, we can achieve the expected behavior:

JavaScript:
  1. (function($) {
  2.   $.fn.grandma = function() {
  3.  
  4.     var els = this.parent().parent();
  5.     return this.pushStack( els.get() );
  6.   };
  7. })(jQuery);

Within a plugin function, one that is a method of $.fn, the this keyword refers to the jQuery object; therefore, the els variable refers to a jQuery object, as well. To convert it to an array, we use jQuery's .get() method, and we pass that array to .pushStack(). Let's see if .grandma() works any better than .grandpa().

JavaScript:
  1. // The DOM looks like this:
  2. // <div class="grandma">
  3. //  <div class="ma">
  4. //    <div class="child daughter"></div>
  5. //  </div>
  6. // </div>
  7.  
  8. var elem = $('div.daughter').grandma().end();
  9. $('div.daughter').text( elem.attr('class') );

Here, "child daughter" is inserted, which means that .end() works as expected, changing the jQuery collection from the result of .grandma() to the result of $('div.daughter'). So, we've just successfully written a DOM traversal plugin, albeit a very simple one.

The Simplest DOM Traversal Methods

If the plugin only uses one DOM traversal method, then .pushStack() isn't really necessary. The HTML5 data filter plugin written by Elijah Manor illustrates this point nicely:

JavaScript:
  1. (function($) {
  2.   $.fn.filterByData = function( type, value ) {
  3.     return this.filter(function() {
  4.       return value != null ?
  5.         $(this).data( type ) === value :
  6.         $(this).data( type ) != null;
  7.     });
  8.   };
  9. })(jQuery);

Only one new jQuery collection is added to the stack, via .filter(), so using .end() simply pops that one off, and our job is done.

Filtering grandparents

For the sake of completeness, it would be nice for this DOM traversal plugin to allow optional "filtering" of the parent and grandparent elements. After all, jQuery's .parent() and .parents() allow filtering. For example, if I were to write $('div.child').parent('.daddy'), the jQuery collection would only contain an element if div.child had a parent element and if that parent had a class of "daddy."

There are plenty of reasonable ways one could include the filters, but for my purposes I'm going to have a .grandparent() method optionally accept two arguments. If only one argument is provided, it will filter the grandparent element only; if two are provided, the first will filter the parent and the second will filter the grandparent. Here is the full plugin plugin:

JavaScript:
  1. (function($) {
  2.   $.fn.grandparent = function( parentFilter, grandFilter ) {
  3.     if ( !grandFilter ) {
  4.       grandFilter = parentFilter;
  5.       parentFilter = undefined;
  6.     }
  7.  
  8.     var els = this.parent( parentFilter ).parent( grandFilter );
  9.     return this.pushStack( els.get() );
  10.   };
  11. })(jQuery);

Finally, we have a nice .grandparent() plugin that adheres to the contract set by other jQuery DOM traversal methods—one that works with both filters and the .end() method. Here is what it could look like in use.

]]>
http://www.learningjquery.com/2011/12/using-jquerys-pushstack-for-reusable-dom-traversing-methods/feed 7
Using jQuery’s Data APIs http://www.learningjquery.com/2011/09/using-jquerys-data-apis http://www.learningjquery.com/2011/09/using-jquerys-data-apis#comments Fri, 02 Sep 2011 16:52:09 +0000 Dave Methvin http://www.learningjquery.com/?p=1346 In the beginning (well, beginning with jQuery 1.2.3 in early 2008) there was the jQuery.data() API. It offers a way to associate JavaScript data — strings, numbers, or any object — with a DOM element. As long as you manipulate the DOM element with jQuery, the library ensures that when the DOM element goes away, the associated data goes away as well. This is especially important for older versions of IE that tend to leak memory when JavaScript data is mixed with DOM data.

Most jQuery code sets data values using the higher-level .data() API; for example, $("div").data("imaDiv", true) sets a boolean value on every div in the document. This API, in turn, calls down to jQuery.data() with each element to set the value. For completeness, there are also jQuery.removeData() and .removeData() to remove data elements, and jQuery.hasData() to determine if any data is currently set for an element.

So to recap: At the inception of these APIs, they were only about getting and setting values associated with DOM elements in memory. Most importantly, the data was managed to ensure no memory would leak when the DOM elements were removed. Many internal jQuery features such as event handling and toggle state memory use these data APIs and their benefits.

Enter HTML5

A few years later, HTML5 became popular and associated another concept with the word "data" through its data-* attributes and the associated DOM .dataset property. This isn't quite the same as jQuery's original idea of data: It involves values being associated with HTML elements in markup and not DOM elements in memory. But they are logically close enough that we added the ability to read HTML5 data-* attributes into jQuery's data object starting with version 1.4.

It's not a perfect marriage, though. HTML5 data-* attribute names are more like CSS names; a name like data-shrivel-up is turned into shrivelUp when read in JavaScript-land. No such rules ever applied to jQuery data names in the past, which means we may have to try both shrivel-up and shrivelUp to find a match. We know it's not ideal, but it's a consequence of trying to fit two concepts with differing semantics into a single API.

Rules of the Road for Data APIs

With that history in mind, there are a few important things you should know in order to use the .data() and jQuery.data() APIs effectively. To give you a better sense of what's going on, the items are illustrated with some code. Assume that each code block runs independently of the others and that they all refer the following HTML:

HTML:
  1. <div id="novel" data-novelist='{"firstname": "Jose", "lastname": "Saramago"}'>Blindness</div>
  2. <div id="poem" data-poet="Edna St. Vincent Millay">Sonnet 18</div>
  3. <div id="story" data-story-writer="Raymond Carver">A Small, Good Thing</div>

Here are the rules of the road:

  1. Only the .data() API reads HTML5 data-* attributes, and it does so once.

    The in-memory data object for an element is initialized from those data-* attributes the first time you call .data() for the element. Any subsequent changes to the attributes are ignored, since jQuery has already cached the data.

    Rule: If HTML5 data-* attributes change during program execution, use jQuery's .attr() method to get the current values.

    JavaScript:
    1. console.log( $.data( document.getElementById('poem'), 'poet' ) );
    2. //>> undefined
    3.  
    4. console.log( $('#poem').data('poet') );
    5. //>> "Edna St. Vincent Millay"
    6.  
    7. // Change the HTML5 data-poet attribute
    8. $('#poem').attr('data-poet', 'Edmund Spenser');
    9.  
    10. console.log( $('#poem').data('poet') );
    11. //>> "Edna St. Vincent Millay"

  2. The .data() API converts HTML5 data-* values to Javascript types whenever possible.

    That means sequences of digits or exponential-looking values like "11E5" are translated to a Javascript Number type, the string "true" becomes Boolean true, and a valid JSON string becomes a JavaScript object.

    Rule: To get HTML5 data-* attributes as strings without data conversion, use jQuery's .attr() method.

    JavaScript:
    1. console.log( $('#novel').data('novelist') );
    2. //>> Object> {"firstname": "Jose", "lastname": "Saramago"}
    3.  
    4. console.log( $('#novel').attr('data-novelist') );
    5. //>> '{"firstname": "Jose", "lastname": "Saramago"}'

  3. The lower-level jQuery.data() API does not read HTML5 data-* attributes.

    However, if the .data() API has been called already on that DOM element, jQuery.data() will "see" the values that it has already read from the data-* attributes. Conversely, if jQuery.data() sets a value with the same name as an HTML5 data-* attribute and .data() later reads them, the HTML5 attribute is ignored.

    Rule: To prevent confusion, do not use similar names for HTML5 data-* attributes and strictly internal data stored using jQuery.data() or .data() on the same elements.

    JavaScript:
    1. // Before reading with .data()
    2. console.log( $.data( document.getElementById('poem'), 'poet' ) );
    3. //>> undefined
    4.  
    5. console.log( $('#poem').data('poet') );
    6. //>> "Edna St. Vincent Millay"
    7.  
    8. // After reading with .data()
    9. console.log( $.data( document.getElementById('poem'), 'poet' ) );
    10. //>> "Edna St. Vincent Millay"

  4. No jQuery data API ever changes HTML5 data-* attributes.

    Most uses of .data() and .removeData() are still for the original purpose of associating data with DOM elements in memory. Updating DOM attributes each time data was changed would slow things down for no good reason. Also, it's not even possible to serialize all data types that might be attached to a DOM element, such as functions, references to other DOM elements, or custom JavaScript objects.

    Rule: To update or remove HTML5 data-* attributes, use jQuery's .attr() or .removeAttr() methods.

    JavaScript:
    1. console.log( $('#poem').data('poet') );
    2. //>> "Edna St. Vincent Millay"
    3.  
    4. console.log( $('#poem').attr('data-poet') );
    5. //>> "Edna St. Vincent Millay"
    6.  
    7. // Change the HTML5 data-* attribute
    8. $('#poem').attr('data-poet', 'William Shakespeare');
    9.  
    10. console.log( $('#poem').data('poet') );
    11. //>> "Edna St. Vincent Millay"
    12.  
    13. console.log( $('#poem').attr('data-poet') );
    14. //>> "William Shakespeare"
    15.  
    16. // Change .data('poet')
    17. $('#poem').data('poet', 'Edmund Spenser');
    18.  
    19. console.log( $('#poem').data('poet') );
    20. //>> "Edmund Spenser"
    21.  
    22. console.log( $('#poem').attr('data-poet') );
    23. //>> "William Shakespeare"

  5. All data-* names are stored in camelCase in the jQuery data object, using W3C rules.

    So, data-caMEL-case becomes the camelCase property in the data object and should be accessed using .data("camelCase"). Because many people will use .data("camel-case") instead, we convert that to camelCase as well, but only if no data item named camel-case is found so it's faster to use the first form. If you get the entire data object using code like data = jQuery.data(elem), you must use data.camelCase to access the data item.

    Rule: When accessing data taken from data-* attributes, and especially when accessing the data object directly, use the W3C camelCasing conventions.

    JavaScript:
    1. // Not recommended:
    2. console.log( $('#story').data('STORY-writer') );
    3. //>> "Raymond Carver"
    4.  
    5. // Better:
    6. console.log( $('#story').data('storyWriter') );
    7. //>> "Raymond Carver"
    8.  
    9. // Broken:
    10. console.log( $('#story').attr('dataStoryWriter') );
    11. //>> undefined
    12.  
    13. // Better:
    14. console.log( $('#story').attr('data-STORY-writer') );
    15. //>> "Raymond Carver"

Pick What You Like

Over time, jQuery's .data() API has taken on more responsibilities than it originally had when it was just a way to associate in-memory data with DOM elements and prevent IE leakage. If you need only a simple way to read HTML5 data-* attributes as strings, then the .attr() method may be the best choice, even though the siren-song-name .data() may be telling you otherwise. Whether you use .attr() or .data(), they work consistently across browsers all the way back to IE6 — even if the browser doesn't support HTML5 — so just choose the API with the feature set that works best for your needs.

]]>
http://www.learningjquery.com/2011/09/using-jquerys-data-apis/feed 20
jQuery.map() in 1.6 http://www.learningjquery.com/2011/05/jquery-map-in-16 http://www.learningjquery.com/2011/05/jquery-map-in-16#comments Tue, 03 May 2011 20:39:43 +0000 Jordan Boesch http://www.learningjquery.com/?p=1287 Among all of the great fixes and additions to jQuery 1.6, I'm happy to say that jQuery.map() now supports objects! The previous map only supported arrays. With other libraries already offering object support for map, it was a nice addition.

Let's say you want to collect an array of object keys from a JSON object.

JavaScript:
  1. var myObj = {
  2.     "name": "jordan",
  3.     "hair_color": "brown",
  4.     "eye_color": "ravishing"
  5. };

Here's how you would have done it in older versions of jQuery (prior to 1.6):

JavaScript:
  1. var objKeys = [];
  2. $.each( myObj, function( key, value ) {
  3.     objKeys.push( key );
  4. });
  5. // objKeys == [ "name", "hair_color", "eye_color" ]

Here's the new way (just a little more convenient):

JavaScript:
  1. var objKeys = $.map( myObj, function( value, key ) {
  2.     return key;
  3. });
  4. // objKeys == [ "name", "hair_color", "eye_color" ]

Going inside the new jQuery.map()

For those that are curious and a little more advanced, there are some neat things going on under the hood of the new jQuery.map() that I would like to talk about.

Adding object support seems pretty trivial at first since jQuery.each() is already doing it - so it must be an easy patch, right? Well, not really. Let's look at how jQuery.each() is doing it. If you look at the jQuery source on github, you'll see that it's doing:

JavaScript:
  1. length = object.length,
  2. isObj = length === undefined || jQuery.isFunction( object );

Can you see the flaw in this? It's going to treat the variable object (could be an array or object) like an object if length is undefined. What happens when I have an object with a "length" property? It dies a horrible death. Some have reported this issue.

For the new jQuery.map(), we wanted support for objects and also be able to pass an object with a length property and not have it blow up like jQuery.each() does. Dan Heberden came to the rescue. Dan spent some time making sure that jQuery.map() didn't face the same fate while keeping performance in mind.

Here is what Dan did to see if elems is an array:

JavaScript:
  1. length = elems.length,
  2. isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length> 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ;

I should note that John Resig added:

JavaScript:
  1. || length === 0

To support empty node lists.

Let's look at the first part of our isArray variable. You'll notice it does a check for:

JavaScript:
  1. elems instanceof jQuery

We're checking to see if elems is an instanceof jQuery. We're doing this first because you're most likely dealing with a jQuery collection/array-like object. Since jQuery collections are treated like regular arrays (uses for loop), this is an optimized way of checking if we should treat elems like an array. Below is an example of jQuery.fn.map() in action.

JavaScript:
  1. $('div').map( function( i, element ) {
  2.     // under the hood, this call to map passes the
  3.     // isArray variable check right away because
  4.     // $('div') is an instance of jQuery
  5.     // using 'this' here refers to the DOM element
  6. });

Here is jQuery.map() with a jQuery collection passed as the first argument:

JavaScript:
  1. $.map( $('div'), function( element, i ) {
  2.     // does the same as above and will pass the "instanceof" check
  3.     // also, note that the arguments are backwards
  4.     // using 'this' here refers to the window object
  5. });

If what you passed is not an instance of jQuery but it passed the isArray variable check, it means your call to the jQuery.map() utility function probably looked something like this:

JavaScript:
  1. $.map( [1, 3, 5] , function(){} );

In this case it falls back to this rigorous check to see if it's an array. Note: I'm breaking it down into separate lines so it's easier to understand.

JavaScript:
  1. // make sure that we actually have a length property
  2. length !== undefined
  3.     // if it's a number, it could possibly be an array
  4.     // but still needs some more checking
  5.     && typeof length === "number"
  6.     && (
  7.         // make sure we're dealing with a set of non-empties
  8.         ( length> 0 && elems[ 0 ] && elems[ length -1 ] )
  9.         // if we're dealing with an empty node list - this is rare
  10.         || length === 0
  11.         // this check is a last resort and only gets hit if we
  12.         // pass an array like $.map(new Array(1), fn);
  13.         || jQuery.isArray( elems )
  14.     )

The rest of the logic is pretty straight forward - just some for loops and super fun for-in loops.

Hope you enjoy jQuery.map() with object support!

]]>
http://www.learningjquery.com/2011/05/jquery-map-in-16/feed 10
Merging jQuery Deferreds and .animate() http://www.learningjquery.com/2011/02/merging-jquery-deferreds-and-animate http://www.learningjquery.com/2011/02/merging-jquery-deferreds-and-animate#comments Thu, 17 Feb 2011 12:46:15 +0000 Dan Heberden http://www.learningjquery.com/?p=1261 Editor's Note: This article originally appeared on danheberden.com.

jQuery’s .animate() method, and the shorthand methods that use it, are fantastic tools to create animations. Creating animations that link together to achieve a particular effect, and do something specific at the end of the animation, can be a painful, messy task. Luckily, we have .queue() for mashing animations together.

But what happens when you want to bridge the gap between ajax requests and animating? When you want to queue a bunch of animations, get data from the server, and handle it all at once, without a crap-load of nested callbacks? That’s when jQuery.Deferred() puts on its cape, tightens its utility belt, and saves the day.

Disclaimer

I should note, however, that this is more-or-less giving an example of a pending feature request to add deferreds support in $.fn.animate. If the feature request is accepted and landed, it won’t show up until version 1.6 of jQuery. The principles, however, speak to jQuery’s flexibility and how to forge its multitude of great features into an even stronger tool.

While this works, its behavior isn’t consistent with that of jQuery. Namely, the new custom animate method doesn’t return ‘this’, but a Deferred object. However, that’s kind of the point of $.sub(): allowing you to copy the jQuery object and have your way with it. So, do try this at home – just don’t threaten my life if your site explodes.

The Demo

The following demo is a basic, distilled use-case for this kind of situation. Clicking the button opens a div that contains a loading message. While it’s opening, it is also querying the server for information to populate the box. Once both have finished, the loading div is hidden and the box with the retrieved data remains.

Modifying .animate()

Here is the code driving the change to .animate()

JavaScript:
  1. // create a sub of jquery (Basically, a copy we can mess with)
  2. var my$ = $.sub();
  3.  
  4. // make my$ have a modified animate function
  5. my$.fn.animate = function( props, speed, easing, callback ) {
  6.   // from jQuery.speed, forces arguments into props and options objects
  7.   var options = speed && typeof speed === "object" ?
  8.   jQuery.extend({}, speed) : {
  9.     complete: callback || !callback && easing ||
  10.     jQuery.isFunction( speed ) && speed,
  11.     duration: speed,
  12.     easing: callback && easing || easing &&
  13.     !jQuery.isFunction(easing) && easing
  14.   };
  15.  
  16.   // create the deferred
  17.   var dfd = my$.Deferred(),
  18.   // a copy of the complete callback
  19.   complete = options.complete,
  20.   // and the count of how many items
  21.   count = this.length;
  22.  
  23.   // make a new complete function
  24.   options.complete = function() {
  25.     // that calls the old one if it exists
  26.     complete && complete.call( this );
  27.     // and decrements count and checks if it's 0
  28.     if ( !--count ) {
  29.       // and when it is, resolves the DFD
  30.       dfd.resolve();
  31.     }
  32.   };
  33.  
  34.   // all the hooks have been made, call the regular animate
  35.   jQuery.fn.animate.call( this, props, options );
  36.  
  37.   // return the promise that we'll do something
  38.   return dfd.promise();
  39. };

While the comments explain just about everything, be sure to read up on $.sub() if you haven’t already. The new animate function on my$ simulates the method signature of the function, $.fn.animate, it’s attempting to replace. In short, speed, easing, and callback are forced into an options object — the same as if the second parameter was an object.

The deferred is created, a copy of the complete callback, and the count of how many items. Why? We want to fire the resolve() function on the deferred once all of the animations have finished. The complete() callback is replaced with a wrapper function that calls the original callback and decrements and checks the count. When the count reaches zero, all items have been animated and it’s safe to fire the resolve() function.

The untouched, standard version of $.fn.animate is called with the same ‘this’, properties, and the modified options object with the new complete wrapper function.

Returned is the promise, dfd.promise(), that lets $.when() do its awesomeness.

Putting it into action

If you haven’t familiarized yourself with deferreds, I highly recommend you read Eric Hynds’ fantastic article about it.

JavaScript:
  1. // retrieves content and updates a dom element
  2. // returns a promise
  3. function populateBox() {
  4.   return $.ajax({
  5.     url: 'your/server/url',
  6.     data: { },
  7.       type: "POST",
  8.       success: function( data ) {
  9.         $('#content').slideDown().html( data );
  10.       }
  11.     });
  12.   }
  13.  
  14. // the "Get Message" click handler
  15. $('button.load').click( function() {
  16.  
  17.   // save the button, box and loading as my$ objects
  18.   var $button = my$(this).hide(),
  19.   $box = my$('#box'),
  20.   $loading = my$('.loading');
  21.  
  22.   // when the functions are done
  23.   $.when(
  24.     // $box was created with my$, so it will
  25.     // use the custom animate function
  26.     $box.slideDown(),
  27.     populateBox()
  28.     // then run the 1st function on success
  29.     // and the second function if either fails
  30.   ).then(
  31.     function() {
  32.       // remove loading, we're done
  33.       $loading.slideUp();
  34.     },
  35.     function() {
  36.       // get that button back here
  37.       $button.show();
  38.       // and hide the box
  39.       $box.slideUp();
  40.     }
  41.   );
  42. });

Because the new animate function was created on my$, the copy of jQuery made using $.sub(), .hide(), .slideUp(), and other helper functions that use the custom .animate() function.

When the animation and ajax request both succeed, thus resolving the promise as true, the first callback function of $.then() is called. However, if one of them fails, the second will be called. You can re-run the demo (by clicking on the run button) and opt to fail the ajax request to see the second function get run. The .then() function is a handy mix of .done() and .fail(), which are useful if you want to provide multiple callbacks.

Summary

While I highly doubt this will be the solution to using deferreds with .animate(), I’m confident it’ll get the ball rolling. Too, it covers some other topics, such as $.sub(), deferreds, and wrapping functions with alternate behavior, that hopefully you found interesting.

]]>
http://www.learningjquery.com/2011/02/merging-jquery-deferreds-and-animate/feed 3
Introducing jQuery API Search http://www.learningjquery.com/2010/09/introducing-jquery-api-search http://www.learningjquery.com/2010/09/introducing-jquery-api-search#comments Fri, 10 Sep 2010 11:28:26 +0000 Karl Swedberg http://www.learningjquery.com/?p=1240 Half-baked tutorials and plugins have been stacking up for months in my virtual kitchen, waiting for me to fire up the oven, finish the cooking, and spread them out on the table. For some reason, though, I've become less and less sure about whether I've put all the right ingredients into the mix. It's irritating, to be sure, but I'm tired of fretting about it. I'm going to consider this the first of what I hope to be many "taste tests" — experiments in various degrees of completion thrown against the wall to see what, if anything, sticks.

As some of you may know, the online jQuery documentation went through a major overhaul in January of this year, coinciding with the release of jQuery 1.4. Packt Publishing "open sourced" the jQuery 1.4 Reference Guide that Jonathan Chaffer and I had been writing, allowing us to put its entire contents (and more) on api.jquery.com. Some of you may also know that the raw XML content of the site is available as a single file, which has allowed other sites such as jqapi.com and idocs.brandonaaron.net to provide alternative views of that content. But what most of you probably do not know is that the jQuery API has been available for quite some time as a searchable API that returns the results in JSON format.

Note: The reason you probably don't know about it is that it's half-baked. So, please go easy on it until some smart people have a chance to look under the hood at what I'm doing. Crazy abuse will surely bring it to its knees.

Motivation

I wanted to let people access the jQuery documentation via JavaScript from any other site and pull out exactly what is needed. I also wanted to play around with JSONP. This fits both of those desires.

URL

To access the searchable API, use the following URL:

http://api.jquery.com/jsonp/

Examples

You can tap into the API with one of jQuery's Ajax methods. If you use $.get() or $.getJSON(), you'll need to append "?callback=?" to the URL. With $.get(), you'll also need to set the dataType argument to "jsonp".

Find entries in the API that have "class" in their name and then do something with them:

JavaScript:
  1. $.get('http://api.jquery.com/jsonp/?callback=?',
  2.   {name: 'class'},
  3.   function(data) {
  4.     // do something with data
  5.   },
  6.   'jsonp');

Find all effects-category entries and then do something with them:

JavaScript:
  1. $.getJSON('http://api.jquery.com/jsonp/?callback=?',
  2.   {category: 'effects'},
  3.   function(json) {
  4.     // do something with json
  5. });

You can, of course, also use the low-level $.ajax() method.

Find entries with a title that ends in "ajax"; append a link for each to the document body:

JavaScript:
  1.   url: 'http://api.jquery.com/jsonp/',
  2.   dataType: 'jsonp',
  3.   data: {title: 'ajax', match: 'end'},
  4.   success: function(json) {
  5.     for (var i=0, len=json.length; i<len ; i++) {
  6.       var entry = json[i];
  7.       $('<a />', {
  8.         href: entry.url,
  9.         html: entry.title
  10.       }).appendTo('body');
  11.     }
  12.   }
  13. });

Search Types

The JSONP API is searchable by name, category, and version. Searching by more than one criterion returns results that match all of the criteria. All searches are case-insensitive.

  • Search by name:
    • Key: title
    • Value: the name of any jQuery method, property, or selector
    • Searches in: post title and post slug
    • Substitutions: Before querying the database, all "$" are converted to "jQuery" and each instance of one or more spaces is converted to a hyphen ("-") for both the search values and the the post title and slug.
    • Default: all titles
  • Search by category name
    • Key: category
    • Value: category name
    • Substitutions: Before querying the database, each instance of one or more spaces is converted to a hyphen ("-")
    • Searches in: category slug
    • Default: all categories
  • Search by version number
    • Key: version
    • Value: a jQuery version number
    • Searches in: category slug for all categories that are a child of the main "version" category
    • Default: all versions

String Matching

The "match" parameter lets you be confine the results to those that match the entire search term, or just the start or end of it.

  • Key: match
  • Value: one of "start", "end", or "exact" (for search by version number, one of "end" or "exact")
  • Default: "anywhere", except for version number, which has a default of "start"

Returned Data

Data is returned as an array of objects. Each item in the array is an object representing a single method, property, or selector. For example, a result with one item would have the following structure:

JavaScript:
  1. [
  2.   {
  3.     "url": "...",
  4.     "title": "...",
  5.     "type": "...", // "method", "property", or "selector"
  6.     "signatures": [
  7.     {
  8.       "added":"...",
  9.       "params": [
  10.       {
  11.         "name": "...",
  12.         "type": "...",
  13.         "optional": "...", // either "true" or an empty string
  14.         "desc": "..." // description of the parameter
  15.       }
  16.       ]
  17.     }
  18.     ],
  19.     "desc": "...", // short description
  20.     "longdesc": "...", // long description
  21.     "return": "..." // type of return value
  22.   }
  23. ]

Note that currently the examples/demos from each entry are not returned.

A (Fairly) Basic Demo

I put together a quick demo to give an idea of the API's flexibility. Type something into the "Name" field in the form below—say, "ajax"—and watch as the results come back. Inside the "Advanced" area, choose the "Matching... End" radio button and search again to see how the results differ.

✚ Search Again

jQuery API Search A demonstration

Advanced

Matching
Include in Results

Clear Results

A Little Less Basic Demo

I replicated this search form on a test server and added back-button support with Ben Alman's crazy delicious BBQ Plugin. Try out the demo.

But wait! There's more!

I put the whole shebang (well, except for the server-side stuff) in a GitHub repo. Clone it, fork it, have your way with it.

But Wait! There's Less!

As I mentioned above, this little project is incomplete. If you peek at the scripts, you'll quickly spot some major chunks of code in need of refactoring. Also, had I but world enough and time, I'd use a nice templating system such as mustache.js or the jQuery Templates plugin to output the search results. But I'll leave all that for another day (or another developer).

]]>
http://www.learningjquery.com/2010/09/introducing-jquery-api-search/feed 9
Great Ways to Learn jQuery http://www.learningjquery.com/2010/07/great-ways-to-learn-jquery http://www.learningjquery.com/2010/07/great-ways-to-learn-jquery#comments Wed, 07 Jul 2010 15:11:38 +0000 Marc Grabanski http://learningjquery.com/?p=1164 These jQuery resources will set you on the path towards mastering jQuery.

Written Articles

E-Books

  • jQuery Fundamentals - open-source e-book written by Rebecca Murphey in collaboration with other well-known members of the jQuery community.
  • jQuery Enlightenment - Cody Lindley's e-book covers advanced topics on jQuery with links to working code examples in jsbin.

jQuery Books

  • jQuery Cookbook - collaborative book by many jQuery team members with recipes on many jQuery topics.
  • Learning jQuery 1.3 - walkthrough of learning the jQuery JavaScript library through hands-on examples and explanations. Authored by Karl Swedberg & Jonathan Chaffer
  • jQuery Reference 1.4 - practical examples of the entire jQuery 1.4 API. Authored by Karl Swedberg & Jonathan Chaffer
  • jQuery in Action 2nd edition - a fast-paced introduction to jQuery that will take your JavaScript programming to the next level. Authored by Bear Bibeault and Yehuda Katz.
  • jQuery UI 1.7 - Build highly interactive web applications with ready-to-use widgets from the jQuery User Interface library. Authored by Dan Wellman.

Slideshows

Learning Screencasts

Attend a training course

Hire a Corporate jQuery Trainer

  • appendTo Training - the jQuery company offers corporate training.
  • Rebecca Murphey - started the open-source e-book jQuery Fundementals and is a member of the yayQuery podcast.
  • Karl Swedberg - wrote the Learning jQuery books and runs this website.
  • Marc Grabanski - creator of the jQuery UI Datepicker project and blogger.
  • Ordered List - all around great guys, creators of the Harmony CMS, and active speakers and writers.

Conferences

jQuery Meetups

Another great way to learn more jQuery is to start or join a jQuery Meetup. Check to see if there is a meetup near you, and if not, start one!

Podcasts

More Resources

Other Blogs

]]>
http://www.learningjquery.com/2010/07/great-ways-to-learn-jquery/feed 40
Autocomplete Migration Guide http://www.learningjquery.com/2010/06/autocomplete-migration-guide http://www.learningjquery.com/2010/06/autocomplete-migration-guide#comments Wed, 23 Jun 2010 13:28:54 +0000 Jörn Zaefferer http://www.learningjquery.com/?p=1106 The jQuery Autocomplete plugin got a successor recently, the jQuery UI Autocomplete. In this guide we'll look at the old plugin API step-by-step, and how to migrate to the new API.

At first it may look like the new plugin supports barely any of the old options. We'll see how all the old options can be implemented using the three new options and the six events.

The old plugin had two arguments: data or url, and options. Lets start with that data-or-url argument. With the new autocomplete plugin, you'll just pass the data or url to the source option.

So, with the old plugin you'd have this code:

JavaScript:
  1. $("input").autocomplete(["a", "b", "c"]);

And that becomes, easy enough:

JavaScript:
  1. $("input").autocomplete({
  2.   source: ["a", "b", "c"]
  3. });

The same applies if you provided a URL as the first argument, although there is a difference between the two plugins for remote data. The old plugin expected a special format with pipes to separate values and newlines to separate rows. That is gone for good, the Autocomplete widget now works with JSON by default. The simplest form is the same as in the example above, an array of string values.

Instead of an array of strings, the widget also accepts an array of objects, with at least a label or value property, or both, in addition to whatever else you need. More on that can be found in the documentation and various demos, eg. the Custom Data demo shows how to use custom properties and even display them.

Lets look through the rest of the options for the old plugin, and what to do with them for the new plugin:

  • autoFill: Gone with no immediate replacement available, for good reasons: The default behaviour when selecting items via keyboard now puts the focussed value into the input, like the Firefox Awesomebar does it. It's not the same as what the autoFill option did, but there should be no need to recreate that effect.
  • cacheLength: There is no built-in caching support anymore, but it's really easy to implement your own, as shown by the Remote with caching demo.
  • delay: Still exists with the same behaviour, but the default is always 300 milliseconds.
  • extraParams: Extra params and all other Ajax related options can be customized by using a callback for the source option. Use $.ajax to send the actual request, with the response callback argument passed to source as the success callback for the $.ajax call. The Remote JSONP datasource demo has an example for that.
  • formatItem, formatMatch, formatResult, highlight: All gone, instead use the source option to either provide the formatted data from your serverside, or implement a custom source to do special formatting. The combobox demo shows how to do that, with a more extensive explanation of that demo right on this site.
  • matchCase, matchContains, matchSubset: All gone, too. The builtin matcher for local data will do a case-insensitive match-contains, everything else has to be implemented on the serverside or using the source option. The combobox linked just above also has an example for that.
  • max: Gone; if your server sends too many items, pass a function for the source option that calls $.ajax and truncates or filters the resulting list.
  • minChars: Still present, but was renamed to minLength. Behaves just the same, even the default is still the same, with minLength: 1.
  • multiple, multipleSeperator: Not built-in anymore, but easy to recreate. There are two demos for this, once with local data, once with remote data.
  • mustMatch: Gone, but easy to implement with the select event. Once more, the combobox provides an example for that.
  • scroll, scrollHeight: These option are gone, but the underlying Menu widget actually has support for scrolling. If you have enough items and specify a height via CSS, the menu will scroll.
  • selectFirst: Similar to autoFill (at the top of this list), this option is gone and has now immediate replacement, nor a need for one. The behaviour for selecting values is solid enough to make this option redundant.
  • width: Gone and not required anymore. The menu will automatically be as wide as the input it completes, or wider, as the content requires. And you can always restrict with width using CSS.

And thats about it. If you're still looking for a particular replacement, take a look at the various events available, and study the use of the source-option within the various demos. If you still have a question, post on the Using jQuery UI forum. If you spot some mistake or see something that can be improved in this article, please let us know in the comments.

]]>
http://www.learningjquery.com/2010/06/autocomplete-migration-guide/feed 32
A jQuery UI Combobox: Under the hood http://www.learningjquery.com/2010/06/a-jquery-ui-combobox-under-the-hood http://www.learningjquery.com/2010/06/a-jquery-ui-combobox-under-the-hood#comments Tue, 08 Jun 2010 10:00:15 +0000 Jörn Zaefferer http://www.learningjquery.com/?p=1055 Update on 2010-08-17: This article was updated to reflect the changes to the combobox code in jQuery UI 1.8.4

The jQuery UI 1.8 release brings along the new autocomplete widget. An autocomplete adds a list of suggestions to an input field, displayed and filtered while the user is typing. This could be attached to a search field, suggesting either search terms or just matching results for faster navigation. But what if there is a fixed list of options, usually implemented as a standard HTML select element, where the ability to filter would help users find the right value way faster?

That's a "combobox." A combobox works like a select, but also has an input field to filter the options by typing. jQuery UI 1.8 actually provides a sample implementation of a combobox as a demo. In this article, we'll look under the hood of the combobox demo, to explore both the combobox widget and the autocomplete widget that it uses.

Let's starts with the initial markup:

HTML:
  1. <label>Your preferred programming language: </label>
  2. <select>
  3.   <option value="a">asp</option>
  4.   <option value="c">c</option>
  5.   <option value="cpp">c++</option>
  6.   <option value="cf">coldfusion</option>
  7.   <option value="g">groovy</option>
  8.   <option value="h">haskell</option>
  9.   <option value="j">java</option>
  10.   <option value="js">javascript</option>
  11.   <option value="p1">perl</option>
  12.   <option value="p2">php</option>
  13.   <option value="p3">python</option>
  14.   <option value="r">ruby</option>
  15.   <option value="s">scala</option>
  16. </select>

Nothing special there, just a label and a select element with a few options.

The code to apply the combobox widget to the select is quite simple, too:

JavaScript:
  1. $("select").combobox();

Let's look at the code for this combobox widget. First, the full code, to give you an overview. We'll dig into the details step-by-step afterwards.

JavaScript:
  1. $.widget( "ui.combobox", {
  2.   _create: function() {
  3.     var self = this;
  4.     var select = this.element.hide(),
  5.       selected = select.children( ":selected" ),
  6.       value = selected.val() ? selected.text() : "";
  7.     var input = $( "<input />" )
  8.       .insertAfter(select)
  9.       .val( value )
  10.       .autocomplete({
  11.         delay: 0,
  12.         minLength: 0,
  13.         source: function(request, response) {
  14.           var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
  15.           response( select.children("option" ).map(function() {
  16.             var text = $( this ).text();
  17.             if ( this.value && ( !request.term || matcher.test(text) ) )
  18.               return {
  19.                 label: text.replace(
  20.                   new RegExp(
  21.                     "(?![^&;]+;)(?!<[^<>]*)(" +
  22.                     $.ui.autocomplete.escapeRegex(request.term) +
  23.                     ")(?![^<>]*>)(?![^&;]+;)", "gi"),
  24.                   "<strong>$1</strong>"),
  25.                 value: text,
  26.                 option: this
  27.               };
  28.           }) );
  29.         },
  30.         select: function( event, ui ) {
  31.           ui.item.option.selected = true;
  32.           self._trigger( "selected", event, {
  33.             item: ui.item.option
  34.           });
  35.         },
  36.         change: function(event, ui) {
  37.           if ( !ui.item ) {
  38.             var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( $(this).val() ) + "$", "i" ),
  39.             valid = false;
  40.             select.children( "option" ).each(function() {
  41.               if ( this.value.match( matcher ) ) {
  42.                 this.selected = valid = true;
  43.                 return false;
  44.               }
  45.             });
  46.             if ( !valid ) {
  47.               // remove invalid value, as it didn't match anything
  48.               $( this ).val( "" );
  49.               select.val( "" );
  50.               return false;
  51.             }
  52.           }
  53.         }
  54.       })
  55.       .addClass("ui-widget ui-widget-content ui-corner-left");
  56.    
  57.     input.data( "autocomplete" )._renderItem = function( ul, item ) {
  58.       return $( "<li></li>" )
  59.         .data( "item.autocomplete", item )
  60.         .append( "<a>" + item.label + "</a>" )
  61.         .appendTo( ul );
  62.     };
  63.    
  64.     $( "<button> </button>" )
  65.     .attr( "tabIndex", -1 )
  66.     .attr( "title", "Show All Items" )
  67.     .insertAfter( input )
  68.     .button({
  69.       icons: {
  70.         primary: "ui-icon-triangle-1-s"
  71.       },
  72.       text: false
  73.     })
  74.     .removeClass( "ui-corner-all" )
  75.     .addClass( "ui-corner-right ui-button-icon" )
  76.     .click(function() {
  77.       // close if already visible
  78.       if (input.autocomplete("widget").is(":visible")) {
  79.         input.autocomplete("close");
  80.         return;
  81.       }
  82.       // pass empty string as value to search for, displaying all results
  83.       input.autocomplete("search", "");
  84.       input.focus();
  85.     });
  86.   }
  87. });

Let's break this down, piece by piece:

JavaScript:
  1. $.widget( "ui.combobox", {
  2.   _create: function() {
  3.     // all the code
  4.   }
  5. });

This defines a new widget, in the ui namespace (don't use this for your own widgets, it's reserved for jQuery UI widgets) and adds the only method, _create. This is the constructor method for jQuery UI widgets, and will be called only once. In versions prior to 1.8 it was called _init. The _init method still exists, but it is called each time you call .combobox() (with or without options). Keep in mind that our widget implementation is not complete, as it lacks the destroy method. It's just a demo.

Coming up next is the creation of an input element and applying the autocomplete to it, with data provided by the select element.

JavaScript:
  1. var self = this;
  2. var select = this.element.hide(),
  3.   selected = select.children( ":selected" ),
  4.   value = selected.val() ? selected.text() : "";
  5. var input = $("<input />")
  6.   .val( value )
  7.   .autocomplete({
  8.     delay: 0,
  9.     minLength: 0
  10.     source: function(request, response) {
  11.       // implements retrieving and filtering data from the select
  12.     },
  13.     select: function(request, response) {
  14.       // implements first part of updating the select with the selection
  15.     },
  16.     change: function(event, ui) {
  17.       // implements second part of updating the select with the selection
  18.     },
  19.   })
  20.   .addClass("ui-widget ui-widget-content ui-corner-left");

It starts with a few variable declarations: var self = this will be used inside callbacks below, where this will refer to something else. The var select references the select element on which the combobox gets applied. To replace the select with the text input, the select is hidden.

The selected and value variables are used to initialized the autocomplete with the current value of the select.

An input element is created from scratch, inserted after the select element into the DOM, and transformed into an autocomplete widget. All three autocomplete options are customized:

  • delay specifies the amount of time to wait for displaying data between each key press, here set to zero as the data is local
  • minLength is set to 0, too, so that a cursor-down or -up key press will display the autocomplete menu, even when nothing was entered.
  • source provides the filtered data to display

Let's break down the source implementation:

JavaScript:
  1. source: function(request, response) {
  2.   var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
  3.   response( select.children("option" ).map(function() {
  4.     var text = $( this ).text();
  5.     if ( this.value && ( !request.term || matcher.test(text) ) )
  6.       return {
  7.         label: text.replace(
  8.           new RegExp(
  9.             "(?![^&;]+;)(?!<[^<>]*)(" +
  10.             $.ui.autocomplete.escapeRegex(request.term) +
  11.             ")(?![^<>]*>)(?![^&;]+;)", "gi"),
  12.           "<strong>$1</strong>"),
  13.         value: text,
  14.         option: this
  15.       };
  16.   }) );
  17. },

There is a bit of matching and mapping involved here: At first, a regular expression object is defined, based on the entered term, escaped with the help of the $.ui.autocomplte.escapeRegex utility method. This regex gets reused in the function below. The response argument, a callback, gets called, to provide the data to display. The argument passed is the result of the call to select.find("option").map(callback). That finds all option elements within our original select, then maps each option to a different object, implemented in another callback passed to the map method.

This callback will return undefined, thereby removing an item, when a search term is present and the text of the option doesn't match the entered value. Otherwise (no term, or it matches), it'll return an object with three properties:

  • label: based on the text of the option, with the matched term highlighted with some regexing (another example of a write-only regular expression)
  • value: the unmodified text of the option, to be inserted into the text input field
  • option: the option element itself, to update the select (via the selected-property) or to pass on in custom events

The label and value properties are expected by the autocomplete widget, the option property has an arbitrary name, used here only by the combobox widget.

Before, I mentioned that the combobox widget customizes all three autocomplete options, but there were actually five options specified. The fourth and fifth properties, select and change, are event. This is the select implementation:

JavaScript:
  1. select: function(event, ui) {
  2.   ui.item.option.selected = true;
  3.   self._trigger( "selected", event, {
  4.     item: ui.item.option
  5.   });
  6. },

The ui.item argument refers to the data we provided in the source option. Via the ui.item.option property we can update the underlying select to the selected item, as well as triggering a selected events for further customization of the combobox widget.

To cover the case where no selection is made, the change event is used:

JavaScript:
  1. change: function(event, ui) {
  2.   if ( !ui.item ) {
  3.     var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( $(this).val() ) + "$", "i" ),
  4.     valid = false;
  5.     select.children( "option" ).each(function() {
  6.       if ( this.value.match( matcher ) ) {
  7.         this.selected = valid = true;
  8.         return false;
  9.       }
  10.     });
  11.     if ( !valid ) {
  12.       // remove invalid value, as it didn't match anything
  13.       $( this ).val( "" );
  14.       select.val( "" );
  15.       return false;
  16.     }
  17.   }
  18. },

Here the entered value is used to try and match a selection. If the value doesn't match anything, it'll get removed, and the underlying select is set to an empty value, too.

The next block customizes the _renderItem method of the underlying autocomplete. This is necessary to output the highlighting on each row, as autocomplete by default will escape any html.

JavaScript:
  1. input.data( "autocomplete" )._renderItem = function( ul, item ) {
  2.   return $( "<li></li>" )
  3.     .data( "item.autocomplete", item )
  4.     .append( "<a>" + item.label + "</a>" )
  5.     .appendTo( ul );
  6. };

And finally, the last block creates the button that opens the full list of options:

JavaScript:
  1. $( "<button> </button>" )
  2. .attr( "tabIndex", -1 )
  3. .attr( "title", "Show All Items" )
  4. .insertAfter( input )
  5. .button({
  6.   icons: {
  7.     primary: "ui-icon-triangle-1-s"
  8.   },
  9.   text: false
  10. })
  11. .removeClass( "ui-corner-all" )
  12. .addClass( "ui-corner-right ui-button-icon" )
  13. .click(function() {
  14.   // close if already visible
  15.   if (input.autocomplete("widget").is(":visible")) {
  16.     input.autocomplete("close");
  17.     return;
  18.   }
  19.   // pass empty string as value to search for, displaying all results
  20.   input.autocomplete("search", "");
  21.   input.focus();
  22. });

Another element is created on-the-fly. It gets tabIndex="-1" to take it out of the tab order, as it's mostly useful for mouse interactions. Keyboard interaction is already covered by the input element. It gets a title attribute to provide a tooltip and is inserted after the input element into the DOM. A call to .button() with some options together with a bit of class-mangling transforms the button into a Button widget that displays a down-arrow icon with rounded corners on the right (the input has rounded-corners on the left).

Finally a click event is bound to the button: If the autocomplete menu is already visible, it gets closed, otherwise the autocomplete's search method is called with an empty string as the argument, to search for all elements, independent of the current value within the input. As the input handles keyboard input, it gets focused. Having focus on the button would be useless or would require duplicate keyboard interaction that the input already supports.

And that's it! We can see that the autocomplete widget is flexible enough to allow all this with option customization, events, and calling a few methods. We don't have to "subclass" autocomplete (creating a new widget with the autocomplete as the parent prototype instead of $.widget). Instead, we can make the combobox mostly independent of any internal or private autocomplete methods. For the full experience and latest version, check out the combobox demo on the jQuery UI site.

]]>
http://www.learningjquery.com/2010/06/a-jquery-ui-combobox-under-the-hood/feed 72
Now you see me… show/hide performance http://www.learningjquery.com/2010/05/now-you-see-me-showhide-performance http://www.learningjquery.com/2010/05/now-you-see-me-showhide-performance#comments Tue, 04 May 2010 12:51:18 +0000 Josh Powell http://www.learningjquery.com/?p=1114 I just got back from the jQuery conference in San Francisco. Wow, what an event. In addition to some incredible talks, I had the opportunity to speak with Rey Bango, Johnathon Sharp, and, of course, John Resig. Any conference where you get to talk to some of the most influential people in jQuery is a win in my book. The "High Performance JQuery" presentation especially caught my attention when the speaker, Robert Duffy, said that .hide() and .show() were slower than changing the css directly. Not having occasion to ask him why, I benchmarked the various ways to hide DOM elements and looked into the jQuery source to find out what is going on.

The HTML I tested against was a page of 100 div tags with a class and some content, I cached the selector $('div') to use with each method to exclude the time needed to find all the div elements on the page from the test. I used jQuery 1.4.2 for the testing, but keep in mind that the algorithms behind the method calls can change dramatically from version to version. What is true for 1.4.2 is not necessarily true for other versions of the library.

The methods I tested were .toggle(), .show() & .hide(), .css({'display':'none'}) & .css({'display':'block'}), and .addClass('hide') & .removeClass('hide'). I also tested modifying an attribute of a <style> element.

.show() & .hide()

These were, in fact, comparatively slow methods of hiding DOM elements across all browsers. One of the main reasons is that .hide() has to save the notion of what the display attribute was before, so that .show() can restore it. It does this using the .data() jQuery method, storing that information on the DOM element. In order to do so, .hide() loops through every element twice: once to save the current display value, and then once to update the display style to none. According to a comment in the source, this prevents the browser from reflowing with every loop. The .hide() method also checks to see if you pass in a parameter to animate the hiding with an effect. Even passing in a 0 dramatically slows down the performance. Performance was slowest on the first call to .hide(); subsequent calls were faster.

Browser      hide/show
FF3.6 -         29ms / 10ms 
Safari 4.05 -   6ms / 1ms
Opera 10.10 -   9ms / 1ms
Chrome 5.0.3 -  5ms / 1ms
IE 6.0  -       31ms / 16ms 
IE 7.0  -       15ms / 16ms 

.toggle()

This was, by far, the slowest method of hiding all of the div elements. It iterates through every element returned by the selector, checks to see if the element is currently visible, and then calls .hide() on visible elements one at a time and .show() on hidden ones one at a time. It also has to check to see if you are passing in a boolean to force everything to .show() or .hide() and check to see if you are passing in functions to toggle instead of toggling visibility. There seems to be some opportunity for optimization of this function, as one could select all of the hidden elements of the selector and call .show() on them all at once and then select the remaining elements in the selector and call .hide() on them at the same time. If you are so inclined, I encourage you to check out the source and see if you can eke out any performance gains.

Browser      hide/show
FF3.6 -         80ms / 59ms 
Safari 4.05 -   24ms / 30ms 
Opera 10.10 -   67ms / 201ms
Chrome 5.0.3 -  55ms / 20ms 
IE 6.0  -       296ms / 78ms 
IE 7.0  -       328ms / 47ms 

.addClass() & .removeClass()

These are pretty snappy methods of hiding/showing elements of the DOM, twice as fast as .show() & .hide() in Firefox and three times as fast in Safari. The differences in IE 6, IE7, Chrome, and Opera are negligible. It's also worth noting that with 100 DOM nodes, we're talking a total difference of 18ms in Firefox and 4ms in Safari. The speed difference will only be relevant for very large selections. Adding and removing a class requires a bit more management on your part, since you have to create the class that has a display of none and then have to keep track of CSS priority to make sure your elements get hidden. The way jQuery adds and removes a class is through string manipulation, so I imagine that as the number of classes on an element grows, this method will get slower, but that is untested speculation on my part.

Browser      hide/show
FF3.6   -       11ms / 11ms 
Safari 4.05 -   2ms / 2ms
Opera 10.10 -   6ms / 3ms
Chrome 5.0.3 -  3ms / 1ms
IE 6.0  -       47ms / 32ms
IE 7.0  -       15ms / 16ms

.css({'display':'none'}) & .css({'display':'block'});

These methods were very snappy. They showed an improvement over .addClass() and .removeClass() in Opera and IE 6/7 and about the same in other browsers. They work great if you know the current display style of all the elements you are changing, or at least have not changed the display style inline. If you have changed the display style inline, then you will need to make sure you set the correct value when you make the element visible again. If you are just using the elements' default display value or set the display value in the css, then you can just remove the style like so, .css({'display':''}), and it will revert to whatever value it has in the css or by default. As a library, jQuery can't assume that the display element wasn't set inline, so it has to manually keep track of it. That is the main slowness you can avoid since you know you won't be setting the display inline.

Browser      hide/show
FF3.6   -       14ms / 12ms
Safari 4.05 -   2ms / 1ms
Opera 10.10 -   2ms / 2ms
Chrome 5.0.3 -  2ms / 1ms
IE 6.0  -       16ms / 16ms
IE 7.0  -       0ms / 0ms  // The usual caveat about inaccuracy of IE clocks applies.

Disabling stylesheets

For fun, I thought, "What if instead of manipulating every DOM node and changing things, we just futz with the stylesheet?" Could there be speed improvements there? I mean, the methods benchmarked above are plenty fast for everyday use, but what if I had 10,000 nodes on a page I wanted to show and hide? It would be slow just selecting them all. But, if I could manipulate the stylesheet, I could avoid the entire overhead. Let me just tell you that way is fraught with peril.

There are, of course, cross browser issues when manipulating stylesheets, since jQuery doesn't abstract them away for you. First, I tried to see if I could append a style tag with the css class as a string using jQuery, but got inconsistent results across browsers. Then I tried creating the stylesheet node and class using JavaScript, but there were different APIs and it ended up being too slow to justify. So finally, forgoing an attempt to do this in a programmatic way, I ended up just writing a style tag with a class in the head of the document. It's far too slow to create the stylesheet programmatically, but if it's already there then it is trivial to give it an ID and use its disabled attribute.

HTML:
  1. <style id="special_hide">.special_hide { display: none; }</style>
  2. <!--  ...  -->
  3. <div class="special_hide">Special hide DIV</div>

Then in javascript…

JavaScript:
  1. $('#special_hide').attr('disabled, 'true');

and BAM, you just displayed all of your elements with a class of “special_hide”. To hide them all again, just do…

JavaScript:
  1. $('#special_hide').attr('disabled', 'false');

and they are now all hidden. The total javascript processing time was 0-1ms across all browsers. The only javascript you are doing is changing an attribute. Of course, there is still the time the browser takes to reflow and repaint the page, but you've virtually eliminated all the javascript processing time. If you call any of the other methods, .toggle(), .hide(), or .css(), this method will stop working on those elements because they set the css style inline, which has higher precedence than other css. To make this method work again, simply do a .css(‘display’, ‘’) to remove the inline style. This method also requires the most work on your part, because you have to define the class and give the class to all of the elements on the page you want to show/hide at the same time, but if you are dealing with extremely large sets, this might just be worth it.

To recap, here is a list of methods to change the display of elements in order from fastest to slowest:

  1. Enabling/Disabling a stylesheet
  2. .css('display', ''), .css('display', 'none')
  3. .addClass(), .removeClass()
  4. .show(), .hide()
  5. .toggle()

Also note that for the majority of use cases, all of these methods are plenty fast to use. When you start having to manipulate large jQuery collections, .show() and .hide() might become too slow in IE, and you might need to bump up to .addClass() or .removeClass(). Enabling/disabling of stylesheets would only be necessary in the most extreme cases, but if things are hiding to slowly for you, you might want to give it a try.

]]>
http://www.learningjquery.com/2010/05/now-you-see-me-showhide-performance/feed 67
Accessible Showing and Hiding http://www.learningjquery.com/2010/04/accessible-showing-and-hiding http://www.learningjquery.com/2010/04/accessible-showing-and-hiding#comments Fri, 23 Apr 2010 13:42:31 +0000 Filament Group http://www.learningjquery.com/?p=1086 Editor's Note: When I started this blog nearly three years ago, one of the first things I did was write a series on showing and hiding elements on a page. The posts were very basic, as was my knowledge at the time. At best, they demonstrated an incomplete answer to the question of how to selectively reveal content based on user interaction. At worst, they encouraged a solution without any regard to accessibility. That's why I was thrilled when I saw an article on the Filament Group blog describing their accessible collapsible content widget. They've graciously accepted my request to re-post that article here, so without any further ado, here it is…

Collapsible content areas are frequently presented in web sites and applications as a way to let users to control how content is shown or hidden on the page. Also called collapsibles, spin-downs, toggle panels, twisties, and content disclosures, they're ideal for selectively displaying optional information — like instructional text or additional details, for example — so users can focus on the task at hand and view this content only as needed.

The collapsible content area widget is fairly simple — a couple of HTML elements controlled with minimal CSS and JavaScript — but when we were researching our book, Designing with Progressive Enhancement, we discovered that many common approaches to creating collapsible content fail to incorporate accessibility features. Happily, there is a way to build collapsible content with progressive enhancement so it delivers an optimal accessible experience for sighted and screen reader users alike.

The code example described in this article is one of the 12 fully-accessible, project-ready, progressive enhancement-driven widgets that accompanies our new book, Designing with Progressive Enhancement.

View a demo

Before we dive in to the details, let's take a look at the widget in question. Here's an error dialog we created for a photo site, which appears when a photo upload fails — it provides a high-level summary, and a collapsible Details block with supplemental content about which photos failed to upload and why:

NOTE on the demo's "View low bandwidth" link: This demo runs on our EnhanceJS framework, which adds a "View low-bandwidth version" link to allow users to toggle from a basic to enhanced view, and drops a cookie on change. If you click the link to view the low-bandwidth version of the demo, you'll need to click it again to view the enhanced version of this site on future views. (Learn more about EnhanceJS here.)

So what's the problem here?

When we first set out to build an accessible collapsible content widget, we thought that the best way to hide the content and keep it accessible to screen readers would be to position it off-screen. We didn't appreciate that while technically this does make the content accessible, positioning content off-screen doesn’t actually provide an accessible experience.

People using assistive technologies like screen readers want to be able to interact with the page just as sighted users do. As accessibility specialist Adam Spencer noted in a recent CBS News story, “True accessibility is giving blind people the same options to access information as sighted ones” (emphasis ours).

While it’s true that hiding collapsible content off-screen at least keeps it in the page and accessible at a baseline level, it’s decidedly sub-par compared with the standard web user’s experience. For example, when a user with a screen reader encounters an accordion widget with ten sections, just like a sighted user they want to hear only the open section, not all content in every section. Screen reader users also expect to use the keyboard to quickly hear the accordion section headings — the screen reader equivalent of quickly scanning the page. When you simply hide content off-screen, these actions aren’t possible; instead, the screen reader reads all content in all the hidden panes, and the screen reader user has no option but to wade through it all in the order it appears in the markup.

Ideally, the experience for both sighted and visually impaired users should be as functionally similar as possible. The W3C Web Content Accessibility Group (WCAG) has outlined a set of four principles that must be met to ensure that content is accessible. Quickly, they say that it should be:

  • Perceivable — Information and user interface components must be presentable to users in ways they can perceive.
  • Operable — User interface components and navigation must be operable.
  • Understandable — Information and the operation of user interface must be understandable.
  • Robust — Content must be robust enough that it can be interpreted reliably by a wide variety of user agents, including assistive technologies.

That’s all well and good, but these definitions are a little abstract. What does it really mean? WCAG provides extended definitions that give us some helpful clues — for example, Perceivable content “can't be invisible to all of their senses”; an Operable interface “cannot require interaction that a user cannot perform”; an Understandable interface’s “content or operation cannot be beyond their understanding”; and it is Robust if “as technologies and user agents evolve, the content should remain accessible”.

In light of these principles, many common techniques for presenting collapsible content fall short. Consider these scenarios:

  • Hiding content by default in the CSS, and relying on JavaScript events to let the user display it. If JavaScript is disabled or unavailable, the user has no means of accessing the content, and may not even know it's there. In this case, content is neither perceivable nor operable — or worse, if the design provides a hint (like an open/close icon or "View details" link), the hint is perceivable but the content non-functioning.
  • Hiding content by positioning it off the page (e.g., position: absolute; left: -9999px;). Doing this ensures that the content is available to screen readers. However, it's always available — the user has no ability to control showing or hiding, or manage whether it's is read aloud. As collapsible content it's not operable; and depending on the widget's content, presenting it all simultaneously may not be understandable.
  • Providing only a visual indicator, like an icon, on the clickable element to show that it can manipulate visibility of related content. This works for sighted users, but fails for screen readers. Unless the icon is accompanied by some adequate auditory feedback that content can be shown/hidden, the feature may not be fully perceivable or understandable to the screen reader user.
  • Applying JavaScript events to a non-natively-focusable element (such as a heading) to show/hide related content. While it works for mouse users, this approach does not guarantee that the widget is navigable for keyboard users (particularly in browsers that don't properly support the tabindex attribute), which are necessary for both screen readers and many mobile devices. In other words, it's potentially neither operable nor robust.

To create a collapsible content widget that works for everyone — and doesn't compromise the experience for screen reader users — we had to rethink what "accessibility" means when showing and hiding content.

Our approach

We start by marking up the page with semantic HTML elements for the heading and content blocks. For example, consider the collapsible widget portion of our error dialog, which consists of a heading element immediately followed by an unordered list:
HTML:
  1. <div id="message">
  2.  
  3. . . .
  4. <h2>Details</h2>
  5. <ul>
  6.     <li>purple-flower.tif <em>Not a supported file format</em></li>
  7.     <li>flower-photos.zip <em>Not a supported file format</em></li>
  8. </ul>
  9. </div>

This markup provides a usable and natively accessible basic experience in all browsers.

If JavaScript is present, we use it to apply enhancements that transform this markup into a functioning collapsible widget with a number of accessibility features. The enhancement script appends several attributes and elements to the basic markup:

  • classes are assigned to the heading element which hide the Details content and apply a visual cue (icon) to indicate that it can be expanded or collapsed
  • a span tag is appended to the heading immediately before the text label which contains the word "Show" followed by a single space if the content is hidden by default; when the content is shown, the script dynamically updates this word to "Hide." This span is intended only for screen readers to audibly describe the heading's function as a toggle link (i.e., "Show Details"), so it's hidden from standard browsers with absolute positioning and a large negative left value
  • a standard anchor link element is wrapped around the heading content to allow it to receive keyboard focus and be accessed with the Tab key
  • an aria-hidden attribute is assigned to the unordered list to ensure that it is truly hidden from ARIA-enabled screen readers when it is hidden from sight (aria-hidden="true"). While display: none; will sufficiently hide the content in current screen readers, future screen readers may not continue to obey visual styles like display, so adding this attribute is considered good, fail-safe practice.
  • classes are also appended to the unordered list to show or hide it visually with CSS. To hide the content, we use the display: none CSS property so that the content is completely hidden from all users

The resulting enhanced markup looks like this:

HTML:
  1. <div id="message">
  2.  
  3. . . .
  4. <h2 class="collapsible-heading collapsible-heading-collapsed"><a class="collapsible-heading-toggle" href="#">
  5. <span class="collapsible-heading-status">Show </span>
  6. Details</a></h2>
  7. <ul class="collapsible-content collapsible-content-collapsed">
  8.     <li>purple-flower.tif <em>Not a supported file format</em></li>
  9.     <li>flower-photos.zip <em>Not a supported file format</em></li>
  10. </ul>
  11. </div>

And the enhanced classes for providing feedback are structured as follows – notice that we use a single background image sprite (icon-triangle.png), and simply adjust the background position to show the appropriate state:

CSS:
  1. . . .
  2. .collapsible-heading {
  3. padding-left:15px;
  4. background:url(../images/icon-triangle.png) 0 6px no-repeat;
  5. }
  6. .collapsible-heading-collapsed {
  7. background-position:0 -84px;
  8. }
  9. .collapsible-heading-toggle {
  10. text-decoration:none;
  11. color:#333;
  12. }
  13. .collapsible-heading-status {
  14. position:absolute;
  15. left:-99999px;
  16. }
  17. .collapsible-content-collapsed {
  18. display:none;
  19. }
  20. . . .

NOTE on testing browser capabilities: To ensure that the browser can fully support all enhancements, we recommend using our EnhanceJS framework, which tests browsers' CSS and JavaScript capabilities and applies enhancements only after those tests are passed. Read more about EnhanceJS here: Introducing EnhanceJS: A smarter, safer way to apply progressive enhancement.

What we accomplish

When built as described above, our collapsible content widget incorporates specific features to meet the four accessibility principles outlined by WCAG:

  • To ensure that it’s perceivable and understandable, it includes cues for both visual and screen reader users that indicate the heading element will show and hide associated content. Visual users see an icon that changes orientation based on the content's state; for screen reader users, the script conditionally appends the word "Show" or "Hide" before the heading text to describe the content's state and provide appropriate language to indicate how the heading is used. Additionally, the widget provides feedback to ARIA-enabled screen readers with the ARIA attribute, aria-hidden; it's set to true when the content is hidden, and false when shown.
  • To make it operable, we ensure that the widget is fully navigable and controllable with the keyboard. When CSS and JavaScript enhancements are applied to the page, the script wraps each heading element's text with an anchor element so that the user can navigate to and focus on the heading with the Tab key, and hide or show related content with the Enter key.
  • And our progressive enhancement approach helps us ensure it’s robust: The foundation is semantic HTML, which is accessible and usable on all browsers; only when a browser is capable of enhancements is the markup transformed into a collapsible widget with enhanced styling and behavior.

Looking ahead

The HTML5 spec includes a new element called details, which if implemented as proposed, will provide a native collapsible widget that will require no JavaScript and will be accessible without requiring any additional work. No browsers have adopted the details element at this time, but it does present a potential alternative to this approach that we may be able to use in the future. Bruce Lawson provides an interesting summary of details and an argument in favor of using semantic interactive elements over JavaScript.

How to use the plugin

Download the collapsible content plugin script (see instructions below), and reference it and the jQuery library in your page. You'll also need to append the CSS classes listed above for the collapsed state of the widget. NOTE: You'll likely need to edit this CSS to fit with your own project's design.

Call the collapsible plugin on the heading or other element that you're using to show related content; the plugin then assumes that the next element in the source order is the content block to show/hide. For example, on DOM ready you can call the plugin on all H2 elements:

JavaScript:
  1. $(document).ready(function(){
  2. $('h2').collapsible();
  3. });

Where to download it

If you've already purchased Designing with Progressive Enhancement, you can download all twelve widgets at the code examples download page.

For the collapsible content plugin and all others that have been released publicly as open source, download the zip.

To learn more, check out Designing with Progressive Enhancement's open-source plugin Google Code repository: http://code.google.com/p/dwpe/.

Help us improve the code

We keep track of issues (feature requests or found defects) in the DWPE Google Code site. Please take a look at our list, and feel free to add to it if you find a bug or have a specific idea about how to improve the plugin that we haven't identified: http://code.google.com/p/dwpe/issues/list. If you think you can help on a particular issue, please attach a patch and we'll review it as soon as possible.

Thoughts? Feedback?

We'd love to hear what you think of this plugin. Please leave us a comment on our blog!

]]>
http://www.learningjquery.com/2010/04/accessible-showing-and-hiding/feed 0