79

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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875

3 Answers3

97

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.

Nick Craver
  • 623,446
  • 136
  • 1,297
  • 1,155
  • 1
    Sorry, I should have mentioned that in my question (and now have): I don't want to unnecessarily visit all the following siblings just to find the first one. – T.J. Crowder Feb 08 '11 at 12:59
  • @T.J. - Then, the answer is no, there's no better built-in way to do this. I requested a `.next(selector, true)` or similar overload for what you're talking about, traversing until it's found rather than returning *if* it's found, but it looks like that discussion isn't on the `.next()` documentation anymore: http://api.jquery.com/next/ – Nick Craver Feb 08 '11 at 13:01
  • @T.J. - I added the test case to the suite, you'll see it's way faster in every modern browser for the reasons added to the answer :) – Nick Craver Feb 08 '11 at 13:06
  • @Nick: LOL, so did I -- and just edited my question to reference that. :-) Yeah, looks like I might be being unreasonable. In any case, `nextAll(selector).first()` is certainly looking like the best implementation for `nextMatching` that fits my criteria. – T.J. Crowder Feb 08 '11 at 13:07
  • 2
    which is faster '.first()' or '.eq(0)'? – Mark Schultheiss Feb 08 '11 at 13:16
  • 5
    @Mark - `.eq(0)` would be the absolute fastest, since [what's what `.first()` calls](https://github.com/jquery/jquery/blob/1.5/src/core.js#L263-265), but the difference there in comparison to everything else is infinitesimal, so I'd go for the more readable code in this case. That being said, my example isn't a plugin...if people aren't using it directly, by all means go with `.eq(0)`. – Nick Craver Feb 08 '11 at 13:28
9

How about using the first method:

jQuery.fn.nextMatching = function(selector) {
    return this.nextAll(selector).first();
}
lonesomeday
  • 233,373
  • 50
  • 316
  • 318
  • Sorry, I should have mentioned that in my question (and now have): I don't want to unnecessarily visit all the following siblings just to find the first one. – T.J. Crowder Feb 08 '11 at 12:57
1

Edit, Updated

Utilizing Next Siblings Selector (“prev ~ siblings”)

jQuery.fn.nextMatching = function nextMatchTest(selector) {
     return $("~ " + selector, this).first()
};

http://jsperf.com/jquery-next-loop-vs-nextall-first/10

jQuery.fn.nextMatching = function nextMatchTest(selector) {
     return $("~ " + selector, this).first()
};
   var nextFoo = $("div:first").nextMatchTest("div.foo");
   console.log(nextFoo)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<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='goo'>Eight</div>

Note, Not yet added to or tried at comparison test. Not certain if actually more efficient than .nextAll() implementation. Piece attempts to parse selector string argument having multiple comma-separated selector's . Returns .first() element of single or comma-separated selectors provided as argument , or this element if no selector argument provided to .nextMatchTest(). Appear to return same results at chrome 37 , ie11

v2

$.fn.nextMatching = function (selector) {
    var elem = /,/.test(selector) ? selector.split(",") : selector
    , sel = this.selector
    , ret = $.isArray(elem) ? elem.map(function (el) {
        return $(sel + " ~ " + $(el).selector).first()[0]
    }) : $(sel + " ~ " + elem).first();
    return selector ? $(ret) : this
};

$.fn.nextMatching = function (selector) {
    var elem = /,/.test(selector) ? selector.split(",") : selector
    , sel = this.selector
    , ret = $.isArray(elem) ? elem.map(function (el) {
        return $(sel + " ~ " + $(el).selector).first()[0]
    }) : $(sel + " ~ " + elem).first();
    return selector ? $(ret) : this
};

var div = $("div:first")
    , foo = div.nextMatching()
    , nextFoo = div.nextMatching("div.foo")
    , nextFooMultiple = div.nextMatching("div.foo, div.goo");
nextFooMultiple.css("color", "green");
nextFoo.css("color", "blue");
console.log(foo);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<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='goo'>Eight</div>
guest271314
  • 1
  • 15
  • 104
  • 177
  • +1 That would be brilliant, if it worked on IE. But sadly, it doesn't, IE fails to find the element (I guess whatever workaround jQuery uses for the unrooted `~` combinator doesn't work on IE). (BTW, `$(selector, context)` is likely to go away at some point, and is just converted to `$(context).find(selector)` anyway, so using that directly will be **slightly** faster.) http://jsperf.com/jquery-next-loop-vs-nextall-first/11 But sadly, if even modern IE doesn't handle it... – T.J. Crowder Nov 10 '14 at 11:37
  • @ guest: At that point, we get into having to be worried whether it worked in all cases, and in any case `selector` was never reliable and [was deprecated in 1.7 and removed in 1.9](http://api.jquery.com/selector/) (even though it's still there, it's part of the internals, only there to support `live` for the migration plugin). – T.J. Crowder Nov 11 '14 at 08:12