tags:

views:

241

answers:

6

In jQuery, filter() reduces your result to those elements that fulfill a certain condition.

This splits the list in two parts. Working with the "good half" of the elements is easy:

$("some selector").filter(function() {
  // determine result...
  return result;
}).each( /* do something */ );

But how can I work with the "other half" of my elements, too - but without doing the equivalent of this:

$("some selector").filter(function() {
  // determine result...
  return !result;
}).each( /* do something else */ );

Basically, I'd like to feed two separate /* do something */ parts to a single filter. One for those that match, and one for the others - without having to filter twice. Am I missing a jQuery function that does this?


P.S.: I guess I could do:

$("some selector").each(function() {
  // determine result...
  if (result)
    /* do something */
  else
    /* do something else */
});

But I was hoping for something nicer.

A: 

Interesting question. I see you are leaning toward what I was going to suggest:

$("some selector").each(function() { 
  if ($(this).is(SOMEFILTER)) { 
    // do something
  } else {
    // do something  
  }
  // continue with things that apply to everything
}); 
BradBrening
I was hoping for a more jQuery-style, method chaining solution; but `each()` would work, technically. PS: If the filter result is not determinable by a jQuery selector, you can't use `is()`, unfortunately.
Tomalak
+1  A: 

You might try your hand at writing a jQuery plugin to do this. Check out the code of the filter function, and come up with something that does more precisely what you want. It could be something like:

$("some selector").processList(predicate, successCallback, failureCallback);

Then you would pass in three callbacks: one that evaluates an object to see if it matches the filter selection (you could also accept a selector string, or the like); one that handles objects that match the selection, and another that handles objects which don't match.

RMorrisey
Yeah, something like this is what I have in mind. :) Won't be too complicated, but I did not want to re-invent the wheel.
Tomalak
Yeah, sorry. I'm not sure if any such thing exists, already. If you need help figuring out how to write the plugin, let us know =)
RMorrisey
Thanks. ^^ I think the plugin that would be able to do this is a no-brainer. I'm just trying to make sure I'm not missing something clever that is built into jQuery already.
Tomalak
+1  A: 

I don't know if this is any nicer, but using filter() you could do something like:

var $others = $();

var $filtered = $('div').filter(function() {
    if(! your filter test) {
        $others.push(this);
    } else {
        return true; 
    }
});

alert($others.length);
alert($filtered.length);

EDIT:

At first I tried it starting with an empty jQuery set $(), and then using add() to populate it with the non-filter results, but couldn't make it work.

EDIT:

Updated to use push directly on an empty jQuery object as suggested by Tomalak.

patrick dw
Hm, that would be one way to do it. Not exactly smooth, but workable. :) +1 (PS: jQuery is an array, you can use `push()` on it directly)
Tomalak
Thanks, yeah, it would be a little nicer if I knew a better way to populate the `others` jQuery set within the filter.
patrick dw
@Tomalak - just noticed your comment about using `push()` directly on the jQuery object. Makes sense, so I updated my answer. Thanks.
patrick dw
Small issue with `$()` - it contains the document object, so it is not empty. `$(null)` would be a *really* blank jQuery object.
Tomalak
@Tomalak - It did prior to version 1.4. From the docs: *As of jQuery 1.4, calling the jQuery() method with no arguments returns an empty jQuery set.* Still, the `not()` solution above ( and the plugin version ) is looking pretty slick.
patrick dw
Oh I see. I've tested with 1.3.2.
Tomalak
+4  A: 

I usually use not for this - it can take an array of elements and remove them from your selection, leaving you with the complement:

var all = $("some selector");
var filtered = all.filter(function() {
  // determine result...
  return result;
});
var others = all.not(filtered);
Kobi
Nice one. I was not aware that `not()` works this way, too. +1
Tomalak
+1 - Better than mine. Thanks for the tip.
patrick dw
+1  A: 
$.fn.if = function(cond, ontrue, onfalse) {
  this.each(function() {
    if (cond.apply(this)) ontrue.apply(this);
    else onfalse.apply(this);
  });
};

$('some selector').if(function() {
  // determine result
}, function() {
  // do something
}, function() {
  // do something else
});

I'm not sure it is much more readable than putting an if inside an each manually, though.

Tgr
You'd have to add the index as an argument to `cond.apply()` since jQuery itself feeds the current index to filter functions. And you'd have to add type checks for the `cond, ontrue, onfalse` arguments, obviously. Other than that, this is what I had in mind. Once you chain 5 methods in jQuery, readability suffers anyway. ;)
Tomalak
+6  A: 

The method recommended by Kobi in plugin form:

$.fn.invert = function() {
  return this.end().not(this);
};

$('.foo').filter(':visible').hide().invert().show();

Note that invert() will not add a new element to the jQuery stack but replace the last one:

$('.foo').filter(':visible').invert().end(); // this will yield $('.foo'), not $('.foo:visible')

Edit: changed prevObject to end() at Tomalak's suggestion.

Tgr
Sweet. :-) I really like that one.
Tomalak
My code cannot be chained, and this seems to get around that very nicely. +1
Kobi
Wouldn't `end()` be better than `prevObject`? `prevObject` means that I must filter in the immediately preceding step, while `end()` refers to the "most recent filtering operation" according to the docs, which means it can be several steps back.
Tomalak
`prevObject` is the last jQuery object in the stack, which is the result of the last traversal-type operation. (Actually, `end()` is defined as `function() {return this.prevObject || jQuery(null);}`.)
Tgr
`end()` might be more future-proof though, as `prevObject` is an internal feature, while `end()` is part of the API.
Tgr
Thanks for clarifying. The API argument a good one, I'd go for `end()`, then.
Tomalak
I think it does not get much better than this. Nice solution, thank you.
Tomalak