Just the weekly tutorials.

Building the New Next and Prev jQuery Methods

A jQuery CSS selection hierarchy is separated by the space character. The following code doesn't select just any paragraph. But the paragraph within a table, within a tr, within a td tag. These "within's" are indicated by the often overlooked space character: which in its own right is an instruction to the internal CSS selector processor.

$("table tr td p");

We know that when we select an HTML element using a CSS selector, there is a distinction between the hierarchy (elements that lead to the target element) and the target element itself, which is the last selector in the hierarchy:

$("table tr td"); // Select all td's in all tables (table and tr will not be selected) $("table tr td:last input"); // Select all input elements within the last td in a table

That sounds simple. But there is one more detail that is often overlooked.

Selecting next and previous children

Being mindful of the type of the target element is not necessary here. All children, regardless of type count. This may sound like a small detail but it can cause confusion if not fully understood.

After I learned the basics of jQuery methods next and prev, and conceptually understood them, I still sometimes find myself running into some inconsistencies when using them. Below I break down the problem into basic blocks anyone can easily understand.

The documentation says: Just use next and prev to iterate to the next or previous element counting from the currently selected target element.

But here is the key. The previous element does not have to share the type of the selected element.

In both cases the jQuery selector ignores the type of the target element. The type of the element becomes irrelevant. Let's take a look.

Considering we have this HTML:

<table><tr><td>table 1</td></tr></table> <table><tr><td>table 2</td></tr></table> $("table").prev(); // Select the table, then select the previous table. $("table").prev().css("background", "red"); // Select the table, then select the previous table and color its background red. Note, the first table will be affected, but not the second one. If the page contains only one table, the red background will not be applied to it at all. If the page contains two tables, only the first one will be affected (because it is the previous table counting back in hierarchy from the second table.) The first table is selected too, but because there is no previous table before it, there is nothing to apply the CSS code to -- and method prev() would not return any elements in that case at all. $("table tr td").prev(); // Select each td in any and all tables, and select only the previous td counting from it. In a way, the target element is now indicated by the prev method. Not the last element (td) in selector's hierarchy as it was in previous examples.

Note in the code above, the target element is no longer td. So this time, the prev method selects the previous td (not table, like in the example we've just seen above it.) The target element that is selected becomes the previous td itself.

But here is the catch. The method prev would select the previous element within the table. Even if it is not of type td! For example, it would select th too. As long as it is the previous child.

What happens here? The type of the target element is not preserved. Similarly, if you used the next() method, the next element of any type in the hierarchy would be selected (counting from the currently selected one, which, only in this case any and all td elements in any and all tables on the page.) Whatever is the last element, that is the element we will be iterating forward and backward with these methods.

The circumstances:

In normal circumstances, next and prev are used to iterate through the next and previous elements of the same type. Or at least it may only seem so. What happens if the previous element is not of the type td? Will it not be selected? Consider this HTML:

<span>A text span</span> <p>Paragraph1</p>

What will happen if we select a paragraph (<p>) and then use prev to go a step back? Will it still select the previous element even though it is of type span? Let's do an experiment:

$("p").prev().css("background", "red"); // What will happen?

What will happen is that span turns red. This proves that the type of the element prev selects is completely irrelevant. What I mean is, that if you have a hierarchy of the same elements (let's say a number of <p>'s or a series of LI tags in a row -- that happens often!) then you most definitely should use next and prev. And the outcome will always be predictable. But if the elements are of not the same type, then the results are not predictable. Remember we do live in a dynamic DOM world (who knows? Sometimes DOM is modified on the fly, and the HTML structure changes, so your next/prev methods will no longer select quite the same element before and after the DOM change!)

So why am I breaking this down in such detail? That's because it is important. We are not dealing just with next and prev methods. But with the structure of HTML itself, which can have any form, either determined by us or not. That's the key. It can be dynamic. And in this one case I outline below, it can definitely be confusing. Let's see.

<span>a</span> <p>b</p> <span>c</span> $("span").next().css("background", "red");

In this case only the p tag is colored red. Why can this be confusing? Because the CSS selector $("span") does not even talk about paragraphs. And the next element can be anything.

Most people don't pay any attention to this and try to "calibrate" their selectors on the fly. Something doesn't work? Add or remove a space, try another method, swap the methods around, etc. But knowing that the selector does not retain the type of the target element when it comes to next and prev methods is great knowledge in our hands.

I see a situation in which it would be useful to be able to select the previous or next element of the same type. However, jQuery itself does not offer such a solution to us. We could use a filter like this:

$("p").prev("p").css("background", "red");

But that will select a previous <p> element only if it is indeed a <p> element. If the previous element is not a p element, then none will be selected even if there are paragraph elements prior to it (past a few non-p elements). And it will not "skip over" any number of non-p elements to find one of type <p>.

What can we do? Let's create our own plugin that does that. Now that would be interesting. Let's do it! We will use the $.extend function to create our plugin. It will select all next elements within the hierarchy, skipping over however many non-p elements. In other words, it will find that next element no matter what, if it exists within the same child hierarchy.

Preparation for our custom plugin: The type-aware "next" method.

In this section you will learn about a few JavaScript tricks by creating a simple plugin. What goes into the creation of the plugin is relevant to script programming in general.

We want to create an alternative to the existing jQuery methods next and prev. But with a custom condition. The next element must be of the same type as the one specified with the selector itself. I called the new methods next_type and prev_type.

// Select the next p tag after the one being selected $("p").next_type().css("background", "red");

First, we need to write a custom helper function. The way it works is quite simple. We are given an array of HTML element objects. We have a seed, which is an element of a certain type such as paragraph tag, span tag, etc. In JavaScript we have a function typeof that determines the type of an element:

<p id = "para"></p> typeof(document.getElementById("para")); // returns "object"

...but there is a small problem. Using typeof on a JavaScript object... or even on a jQuery object returned from the $ function will not identify the element's type with enough precision that we need because what is returned is a custom JavaScript object (aka the jQuery object.):

typeof($("p#para")); // still returns "object", not "paragraph" that we need

We can solve this case by turning to little known JavaScript property called tagName:

document.getElementById("para").tagName; // Now it returns P... just what we need

jQuery simplified getting tagName by offering its own way of getting the value. In order to do that we will use the method $.fn.prop('tagName'); But first let's do a brief experiment:

<p id = "para">hello</p> <b>again</b> <span>a span</span> <p>second</p> <p>third</p>

Keeping this HTML in mind, let's do the following to select all elements using the star (*) selector:

var all = $("*"); // Select absolutely all elements

We have 5 HTML elements, so the length of the returned object should say 5, right?

alert(all.length); // 14!

Wrong! That's because the star (*) selector selects everything, including the tag, the <style> tags and even the <doctype> tag. Everything.

We can use a trick to avoid this situation. Naturally we want to grab only the 5 elements from within the assumed tag, which most of the time we don't have to work with anyway, because it is the main container for all tags.

In order to do that, we will correct the $("*") selector by adding context to it:

var onlybody = $("body"); // Create a new context out of <body> var all = $("*", onlybody); // Search only within the context (the <body> tag)

Now all.length returns 5. Just as expected. There are only 5 elements within the body.

Here is another interesting thing. A jQuery object returned from $("*") or any other selector for that matter, contains a custom JavaScript object of type jQuery. However, this object acts as an array. This is why we can access its length property. It contains the number of elements that were selected. It is only so by custom design.

jQuery object is designed to imitate arrays. By itself the jQuery "array"-like object is not an array. It only imitates array's function. For example the selected elements can be accessed using the [0] brackets via 0-based index. The object stored at these indices are actually JavaScript objects, and not jQuery ($) objects -- we cannot run jQuery methods on them.

What is the key to this? Let's do this quick example:

var obj = new Object(); // create a custom JavaScript object obj.P = 10; // assign custom property P to the object alert(obj.P); // retrieve the property P

This is an example of using a custom object as an associative array, even though it isn't. The jQuery object is also a JavaScript object and can be used in the same way.

In addition you can use the following syntax to add associative entries to an object-array:

obj["P"] = 10; // assign using brackets[] alert(obj.P) // 10

And to check if "P" exists in that object, we can use the in keyword:

alert("P" in obj); // There is a property named "P" -- returns true alert("A" in obj); // returns false -- no property named "A" was found

Note that this won't work for values:

alert("10" in obj); // false alert(10 in obj); // false

These tips will become useful later as we build the plugin.

Consider the previous example where we chose all elements using the star selector. Let's "dereference" it into a plain JavaScript object pertaining to the element that was selected:

var all = $("*", $("body")); // Search for any elements within body var first_element = all[0]; // Select first 0-index based element from selection as a JavaScript HTML element, equivalent to document.getElementById("p#para"); (See the HTML with 5 elements above where the first one is p#para) first_element.tagName; // returns "P" $(first_element).prop("tagName"); // returns "P" $(first_element).attr("id"); // returns "para"

Notice that first_element was dereferenced into a JavaScript object first from all[0]. This gives us direct access to the object related to the first HTML element. Then passed to jQuery's $ function again, so we can get its property tagName and name of the id attribute.

Let's see how we can loop through the entire list of returned elements from a CSS selection and display each element's tag name:

for (var i = 0; i < all.length; i++) { var el = all[i]; // get the javascript object related to the selected element at this index var type = el.tagName; // get tag name of this element (P,SPAN,etc.) alert(type); // display the type in an alert box }

This code will display 5 alert boxes showing the types of the elements that were selected in the following sequence: P,B,SPAN,P,P. Because this is the order in which the HTML elements appear, in the earlier example. So we have 3 P tags, 1 B tag and a SPAN tag. For other selections of course this will be different.

Going back to where it all began: we were going to create a plug in that chooses next or previous elements with a catch: the next or previous element must be of the same type compared to the one that is being selected by jQuery's command such as $("p") or even $("p,span") which selects elements of both types P and SPAN.

We will need both next_type and prev_type functions. They are quite similar. Based on what we just learned above let's write the plugin and discover these interesting techniques in terms of practical implementation.

First we will need to write a function that browses through the entire list of array entries, identifies its tag name and compares it with the tag name that was passed to it. It then returns a "next" element on the same list with index greater than the one that was passed to it. In other words, it returns the next element of type X, where X is whatever we are looking for:

$(document).ready(function() { // next_type: get next element(s) of selected type(s) // Example: // $("p").next_type(true); // -- select all next p's (counting from first selected) // -- in the same order as they appear on the page. // -- regardless of their position within parents // Helper function // Find 1 next element of type matching that of the seed function next_of_type(arr, types, brk) { var ret = []; // A very interesting way to list through the entire custom object using for-loop directive // As you can see for(var i=0;i<10;i++) is not the only format we can use when using for. // The property var will become the actual property name, types is the object. for (var property in types) { var i = 0; var ri = 0; while(arr[i]) { if (i > types[property]) { if (property == arr[i].tagName) { ret[ri++] = arr[i]; if (brk) break; } } i++; } } // We have to wrap ret in $() object. This way we return a series of jQuery objects for further processing by the methods in the chain. Had we not done it, we would not be able to use the method next_type as part of a method chain. return $(ret); } // Extend jQuery with the new plugin method $.fn.extend( { next_type:function(flood) { var arr = []; var types = new Object; // first occurances of type var types_i = 0; this.each(function(index, object) { // Identify the first occurrence of the type at index. This is interesting. We are gathering the type of the element and only store it once in the types array, even if it continues to occur in the received array containing all selected elements. So if the type is already stored at types["P"] (for paragraph for example) we no longer need to store it. Finally, the types object if (this.tagName in types == false) { types[this.tagName] = index; types_i++; } // Collect this item to be a return value later arr[arr.length] = this; }); // Use the helper function we previously built. This is the core of plugin. return next_of_type(arr, types, !flood); } }); // Using the new plugin, // Let's try to select some of the B, P and SPAN elements // -- and then select all next elements of the same type (excluding the original) $("b,p,span").next_type(true).css("background", "red"); });

In the example just above, notice that I passed true to next_type method. This is the flood parameter. If it is set to true, all next elements of the same type (until the very end of the web page) are selected. In other words the selection "floods" the rest of the elements on the page. If it is set to false, only the following (the first next) element is selected.

Adding prev_type method is similar. It does pretty much the same thing, except it iterates the objects from bottom up.

I created a jsFiddle to demonstrate it. Both examples are provided here for educational purposes only.

First attempt:


Final version:


Just the weekly tutorials.