More than one way… (delegate edition)

Chris Coyier //

There was a question in the forums about affecting non-hovered items. The effect they were after is that they had an unordered list of items and when they were rolled over, they would all dim (lower opacity) except the one hovered.

This can be done with CSS, using pseduo-selectors.

ul li:not(:hover) { opacity: 0.5; }

However we know that pseudo-selectors don't have very good cross-browser support. And for that matter, opacity doesn't either. jQuery is pretty good at mitigating cross-browser problems, so l thought I might give that a spin. In attempting it, I had a nice little learning journey.

My first thought is that I needed to write a selector to select all list items except the one currently being hovered over. I had this come up recently for another reason, and I made a snippet for it: Excluding this from the selector. In this example:

$("ul li").not(this).css("opacity", "0.5");

We just need to wrap that in some kind of hover function. The most obvious:

$("li").hover(function() {
  $("li").not(this).css("opacity", 0.5);
}, function() {
  $("li").not(this).css("opacity", 1);
});

But I've been being taught that binding events like this is inefficient, since 1) it requires one event handler for every single element and 2) new elements appended to the page after this code runs will need to be re-bound. (Not to mention, you would definitely want to cache that selector above var $listItems = $("li");).

So I thought I'd go right for the new delegate function which we have mentioned here on CSS-Tricks a few times now. This is awesome because it solves both the issues mentioned above. This was my first (utterly broken) attempt.

$("ul").delegate("li", "hover", function() {
    $("ul li").not(this).css("opacity", "0.5");
}, function() {
    $("ul li").not(this).css("opacity", "1");
});

Don't try that at home, it's not going to work. Why not? James Padolsey reminded me that "hover" isn't an event. It's a jQuery function, but not a real event. You can use it with delegate, but not with the same syntax like I was attempting. Delegate is expecting just the three parameters: element, event, and function, not four parameters like I was passing it (assuming it would know the last function was supposed to be a callback/mouseleave).

Then the next most obvious transformation becomes this:

$("ul").delegate("li", "mouseenter", function() {
    $("ul li").not(this).css("opacity", "0.5");
}).delegate("li", "mouseleave", function() {
    $("ul li").not(this).css("opacity", "1");
});

That uses two delegate functions, this time with real events, to get the job done. This is fine, but we can make it a bit more efficient by mapping both events to a single delegate and then just testing to see what type of event was fired. David Link had this idea:

$("ul").delegate("li", "mouseover mouseout", function(e) {
    if (e.type == 'mouseover') {
      $("ul li").not(this).css("opacity", "0.5");
    } else {
      $("ul li").not(this).css("opacity", "1");
    }
});

Which James Padolsey had an even cleaner version:

$("ul").delegate("li", "mouseover mouseout", function(e) {
    $("ul li").not(this).css("opacity", e.type == 'mouseover' ? 0.5 : 1);
});

jQuery's live() function is also a good choice here, but has some quirks as well. It turns out you can pass "hover" to live, but you can still only provide a single function. The function will then fire on both mouseenter and mouseleave events, and you'll have to do event.type testing (like above) to figure out which it was and behave accordingly. Thanks to Paul Irish and Jeffrey Way for that one.

And remember that CSS selector from the very top? We can use that right in jQuery too:

$("ul li:not(:hover)").css("opacity", "0.5");

Quite the journey eh? Like all things web, always more than one way to skin the cat.