Efficient, concise way to find next matching sibling?

JqueryJquery Selectors

Jquery Problem Overview


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.

Jquery Solutions


Solution 1 - Jquery

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.

Solution 2 - Jquery

How about using the first method:

jQuery.fn.nextMatching = function(selector) {
    return this.nextAll(selector).first();
}

Solution 3 - Jquery

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>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionT.J. CrowderView Question on Stackoverflow
Solution 1 - JqueryNick CraverView Answer on Stackoverflow
Solution 2 - JquerylonesomedayView Answer on Stackoverflow
Solution 3 - Jqueryguest271314View Answer on Stackoverflow