Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Efficient, concise way to find next matching sibling?

Sticking to the official jQuery API, is there a more concise, but not less efficient, way of finding the next sibling of an element that matches a given selector other than using nextAll with the :first pseudo-class?

When I say official API, I mean not hacking internals, going straight to Sizzle, adding a plug-in into the mix, etc. (If I end up having to do that, so be it, but that's not what this question is.)

E.g, given this structure:

<div>One</div> <div class='foo'>Two</div> <div>Three</div> <div class='foo'>Four</div> <div>Five</div> <div>Six</div> <div>Seven</div> <div class='foo'>Eight</div> 

If I have a div in this (perhaps in a click handler, whatever) and want to find the next sibling div that matches the selector "div.foo", I can do this:

var nextFoo = $(this).nextAll("div.foo:first"); 

...and it works (if I start with "Five", for instance, it skips "Six" and "Seven" and finds "Eight" for me), but it's clunky and if I want to match the first of any of several selectors, it gets a lot clunkier. (Granted, it's a lot more concise than the raw DOM loop would be...)

I basically want:

var nextFoo = $(this).nextMatching("div.foo"); 

...where nextMatching can accept the full range of selectors. I'm always surprised that next(selector) doesn't do this, but it doesn't, and the docs are clear about what it does, so...

I can always write it and add it, although if I do that and stick to the published API, things get pretty inefficient. For instance, a naïve next loop:

jQuery.fn.nextMatching = function(selector) {     var match;      match = this.next();     while (match.length > 0 && !match.is(selector)) {         match = match.next();     }     return match; }; 

...is markedly slower than nextAll("selector:first"). And that's not surprising, nextAll can hand the whole thing off to Sizzle, and Sizzle has been thoroughly optimized. The naïve loop above creates and throws away all sorts of temporary objects and has to re-parse the selector every time, no great surprise it's slow.

And of course, I can't just throw a :first on the end:

jQuery.fn.nextMatching = function(selector) {     return this.nextAll(selector + ":first"); // <== WRONG }; 

...because while that will work with simple selectors like "div.foo", it will fail with the "any of several" option I talked about, like say "div.foo, div.bar".

Edit: Sorry, should have said: Finally, I could just use .nextAll() and then use .first() on the result, but then jQuery will have to visit all of the siblings just to find the first one. I'd like it to stop when it gets a match rather than going through the full list just so it can throw away all results but the first. (Although it seems to happen really fast; see the last test case in the speed comparison linked earlier.)

Thanks in advance.

like image 766
T.J. Crowder Avatar asked Feb 08 '11 12:02

T.J. Crowder


2 Answers

You can pass a multiple selector to .nextAll() and use .first() on the result, like this:

var nextFoo = $(this).nextAll("div.foo, div.something, div.else").first(); 

Edit: Just for comparison, here it is added to the test suite: http://jsperf.com/jquery-next-loop-vs-nextall-first/2 This approach is so much faster because it's a simple combination of handing the .nextAll() selector off to native code when possible (every current browser) and just taking the first of the result set....way faster than any looping you can do purely in JavaScript.

like image 174
Nick Craver Avatar answered Sep 21 '22 12:09

Nick Craver


How about using the first method:

jQuery.fn.nextMatching = function(selector) {     return this.nextAll(selector).first(); } 
like image 31
lonesomeday Avatar answered Sep 19 '22 12:09

lonesomeday