0

How can I get the nearest next element that sattisfies some condition/function? Like:

nextEl = someEl.next(function() {... return true to return this from next() });

I'm not aware that the .next() method in jQuery API supports a function as an argument. Is there some trick to write it like this without doing a while loop?

Also I don't want to select all elements first and then use filter() -- this suboptimal from performance point of view.

tillda
  • 18,150
  • 16
  • 51
  • 70
  • Are you completely certain that it's a condition that could not be met by another selector? – zincorp Nov 26 '10 at 19:35
  • Yes. Also I don't want to select all elements first and then use filter() -- this suboptimal from performance point of view. – tillda Nov 26 '10 at 19:45
  • I updated my answer to turn the native code version into a plugin called `nextMatch()`, so the condition doesn't run more times than it has to, and you don't fetch all the next siblings with `nextAll()`. – user113716 Nov 26 '10 at 23:19

2 Answers2

2

Get all the next elements using .nextAll(), then use .filter() and return the result of the condition. When the condition returns true, the element is kept.

Then narrow it down to the .first() match.

nextEl = someEl.nextAll().filter(function() {
    return ( someCondition );
}).first();

Or if there will be many elements tested, and you don't want to run the condition all those extra times, use .each(), then return false; when the condition is met. This halts the loop.

var nextEl;

someEl.nextAll().each(function() {
    if( someCondition ) {
        nextEl = this;  // reference the matched element
        return false;   // break the loop
    }
});

EDIT: If you don't want to select all the next elements, I'd go native, and use a while loop, something like this:

var nextEl = someEl[0];

while( nextEl = nextEl.nextSibling ) {
    if( someCondition ) {
        break;
    }
}

The loop will break as soon as the condition is met, and the most recent assignment to nextEl will be your element.

If the condition is never met, the loop will end when it runs out of elements, and nextEl will be null or undefined ( I don't remember which ).

This should be a very quick way to do it.


EDIT:

Here's a function version of it. It accepts the starting element and a function that runs the test. Then it returns the match found, or undefined.

function nextMatch( el, func ) {
    while( el = el.nextSibling ) {
        if( func() ) {
            return el;
        }
    }
}

  // nextEl will contain the match, or "undefined"
var nextEl = nextMatch( someEl, function() {
    return (someTest);
});

var someOtherEl = nextMatch( someEl, function() {
    return (someOtherTest);
});

LAST EDIT:

I guess I might as well make it into a plugin:

(function( $ ) {
    $.fn.nextMatch = function( func ) {
        return this.map(function() {
            var el = this;
            while( el = el.nextSibling ) {
                if( el.nodeType === 1 && func.call( el ) ) {
                    return el;
                }
            }
        });
    }
})( jQuery );

var nextEl = someEl.nextMatch( function() {
    return (someTest);
});

So now it's more of a jQuery type solution. You still should have better performance since it doesn't fetch all the next siblings, and the while loop still breaks once a match is found.

user113716
  • 318,772
  • 63
  • 451
  • 440
  • This works, but there is some performace penalty for selecting all the elements So I wanted to not use that. – tillda Nov 26 '10 at 19:44
  • @tillda - Is the penalty in the condition test, or just in selecting the elements. I added an answer that halts the conditions after the first time it is satisfied. If you didn't want the `.nextAll()`, you could use a `while` loop. I'll give an update. – user113716 Nov 26 '10 at 19:47
  • Actually the `return false;` inside the `filter` doesn't break the loop. It just discards that element from the results of `filter`. So, in fact, this selects the last matching sibling (look [here](http://jsfiddle.net/cambraca/vQPC4/1/)) – cambraca Nov 26 '10 at 19:52
  • @cambraca - That was a `copy/paste` error. In the description you'll see I should have `.each()` there instead. I also missed the closing `}` for the `if()`. Thanks for the heads up. Fixed. :o) – user113716 Nov 26 '10 at 19:52
  • But if performance is really a concern, I like your native js idea. – cambraca Nov 26 '10 at 19:56
  • @cambraca - Agreed. Native should always be quicker. [Here's your test](http://jsfiddle.net/patrick_dw/vQPC4/4/) using the `while` loop. – user113716 Nov 26 '10 at 19:59
  • I have about 20-30 of these similar but different loops in my application, so that's why I'm asking if there is some simple API way. Thanks for answering anyway! – tillda Nov 26 '10 at 20:00
  • @tillda - Do you mean that they are essentially the same except for the condition test? If so, you could just make a function out of it that receives the test as a function parameter that is called. – user113716 Nov 26 '10 at 20:02
1

Try this:

nextEl = someEl.nextAll('.a').first().css('color', 'red');

The condition is in this case that it has the class a, and it turns the color red (merely as an indicator).

Look at it working here.

cambraca
  • 27,014
  • 16
  • 68
  • 99