The Ol' Switcheroo

You have an element and within that element you want an item to be clicked on and set as active. Then you want to click on another item and have it to be set as active. Maybe it's navigation, maybe it's tabs. This is a common pattern. So much so, that I've rewritten this code a few times but keep losing track of it.

This is a jQuery plugin that does the ol' switcheroo and is very simple. When you click on an element, it'll set it as active. When you click on another, it'll remove the active class from the currently active item and then enable the active item. Super simple, nothing more, nothing less.

(function($){
   $.fn.switcheroo = function(els){
      this.each(function(){
         var p = $(this);
         $(els||'li', p).click(function(){
            $('.active', p).removeClass('active');
            $(this).addClass('active');
         });
      });
   }
}(jQuery));

Then, it can be attached to an element using a couple different approaches:

$('ul').switcheroo();
$('ul').switcheroo('a');

Since this is most often used with lists, the first line of code omits any parameter as the plugin automatically looks for LIs contained within the parent element. However, if you want to be specific, you can specify the selector as the one and only parameter. That's all there is to it.

This can, of course, be used as a basic framework for building additional functionality in, like callback functions and animations. In any case, you can check out this demo page to see it in action.

Published November 05, 2008
Categorized as JavaScript
Short URL: https://snook.ca/s/915

Conversation

25 Comments · RSS feed
Chris W. said on November 05, 2008

That's useful.

Cal Wilson said on November 05, 2008

That's damn useful - thanks Jonathan! *adds to bookmarks*

Kyle Slattery said on November 05, 2008

I've always wondered: what's the purpose of the (function($){}(jQuery)) syntax?

Also, your comment form appears to be broken: http://img.skitch.com/20081106-et1bbbhufbwwstw9w5jsx5afpi.jpg

Jonathan Snook said on November 05, 2008

@Kyle: The function self instantiates itself, passing in the jQuery object, aliasing it to the dollar sign function. But it's within the scope of the function, preventing it from conflicting with other libraries.

Thanks for pointing out the error. I'll be moving servers soon and will be addressing a couple issues like that.

sho'fr said on November 05, 2008

what about some "return false" action so we don't have to see the "#" in the URL?

Gerry Vandermaesen said on November 06, 2008

I have some suggestions for improvements, here's my modified version of your script:


(function($){
   $.fn.switcheroo = function(els){
      return this.each(function(){
         var p = $(this);
         p.children(els||"li").click(function(){
            p.children().removeClass('active');
            $(this).addClass('active');
         });
      });
   }
}(jQuery));

I return the original result set to not break chaining, and also only bind the event on direct descendants to avoid unexpected behaviors with nested items.

Ricardo said on November 06, 2008

Great piece of code.

Gilbert said on November 06, 2008

Isn't this want .toggleClass() is for?

Rémi said on November 06, 2008

You could also use this:

jQuery('a').click(function() {
    $(this).addClass('active').parent().siblings().find('a').removeClass('active');
})
Jonathan Snook said on November 06, 2008

@sho'fr: Sure. You can either throw a return false into the switcheroo or add an extra click event in the chaining. $('ul').click(function(){return false}).switcheroo();

@Gerry: Yes, I forgot to add the return but I didn't use children on purpose. If you notice the first example in the demo, your code wouldn't work because the links aren't direct descendants. I see what you mean though about nested items and I'd have to try some stuff out to avoid that (like maybe $('> li', p))

@Gilbert: no, this is different. toggleClass toggles the class on one element. In this case, we're turning a class off one element and turning it on another.

Jonathan Snook said on November 06, 2008

@Rémi: The problem with your code is that it isn't isolated, meaning you'd have to duplicate the block for each block of code on the page. It also only works if you have a very specific HTML structure. For the LI example, you'd have to restructure the code since the relationship between the elements is different. I do like the succinctness though and could see me using that.

Gerry Vandermaesen said on November 06, 2008

@Jonathan: Yes, I understand your point about not using children(). How about this:

(function($){
   $.fn.switcheroo = function(selector){
      selector = selector || "li";
      return this.each(function(){
         var elements = $(this).find(selector+":first").siblings(selector);
         elements.click(function(){
            elements.removeClass('active');
            $(this).addClass('active');
         });
      });
   }
}(jQuery));
Josh Langner said on November 06, 2008

I typically use a slightly different technique. Realizing that I may have more than one set of tabs on a page, I will contain a set of "a" or "li" tags within a "div" or "ul", and then parse only through that set (instead of going across the entire page).


$('.tab').click(function() {
    $(this).siblings().removeClass('active');
    $(this).addClass('active');
});

And yes, each section would remember its active state.

Michael Thompson said on November 06, 2008

@sho'fr

Here's the proper jQuery way to prevent the click event from finishing and showing a pound sign (changes from original in bold):


(function($){
   $.fn.switcheroo = function(els){
      this.each(function(){
         var p = $(this);
         $(els||'li', p).click(function(e){
            $('.active', p).removeClass('active');
            $(this).addClass('active');
            e.preventDefault();
         });
      });
   }
}(jQuery));

The "e" passed into the anonymous click function will be the click event object, to which jQuery conveniently appends the preventDefault() method. This added method standardizes event cancellation across browsers so you can avoid multiple if/else statements and effectively makes your code behave like the user didn't do anything.

For more information about jQuery's event system and normalization:
jQuery Documentation: Events Guide

Josh Langner said on November 06, 2008

Michael Thompson, I've noticed that "return false;" also prevents showing a # sign / jumping to a URL as a browser would expect. So for example:

$('.tab').click(function() {
    $(this).siblings().removeClass('active');
    $(this).addClass('active');
    return false;
});

Would this not also be correct?

Josh Langner said on November 06, 2008

@Michael Thompson eh... nevermind. Reread your post for the 5th and I finally got what you were saying. :)

Thomas Eilander | Santhos said on November 07, 2008

Seems like a very smart and easy to use solution! Thanks for sharing!

olivier said on November 07, 2008

why "p" ?

Jonathan Snook said on November 07, 2008

p = parent. Admittedly, I tend to be concise in my variable naming. I probably shouldn't be when it comes to sharing code. :)

Rasmus said on November 07, 2008

Why not cache the last active element? Or is that too obvious?

I'm no jQuery-guy so I'll just post the lines you'd need. Not sure you'd need the parent at all then. I hope it makes sense ;)


      var active; // define variable to contain last active element in a parent scope
...
            if (active) active.removeClass('active'); // remove class from last active element
            active = $(this).addClass('active');
Sérgio Jardim said on November 09, 2008

I'd like to pass the class name also. So I can use any name, instead of .active.

(function($){
$.fn.switcheroo = function(els, activedClass){
if(!activedClass) {
activedClass= ".active"; //it's now a suggestion, not a fixed one
} else { //concatenate a dot
activedClass= "."+activedClass;
};
this.each(function(){
var p = $(this);
$(els||'li', p).click(function(e){
$(activedClass, p).removeClass(activedClass);
$(this).addClass(activedClass);
e.preventDefault();
});
});
}
}(jQuery));

Abba Bryant said on November 14, 2008

Is it actually necessary to concatenate the "." into the className above?
I thought the addClass function would take care of that.

test said on November 23, 2008

TEST1

Jack F said on December 08, 2008

Hey Jonathan,

I'm looking to really go further with JQuery and wondering how you learnt so much - was there a specific resource you used, such as a website/book etc?

Thanks!

Jack F

Jonathan Snook said on December 08, 2008

@Jack F: I'm self-taught so no specific site except the jQuery docs and learning from what other people put out. Half the battle is understanding the methodology of the library (for jQuery, knowing that most(/all?) calls return the jQuery object, how plugins work, learning the API, etc).

Sorry, comments are closed for this post. If you have any further questions or comments, feel free to send them to me directly.

Want to learn about scaling CSS for large projects?

I'm available for full and half-day workshops on scalable CSS architecture. I can provide on-site training for your team. Interested?
Get in touch.