Duck Punched Filter

May 1, 2010 | Demo | Download | Github

Something that has always bothered me is the inability to pass in arguments to custom filter functions. With jQuery 1.4.x, this problem has been semi-fixed, in that you can pass a function into the filter method, and base selections on the current scope. But what about when you have a filter that is used across multiple scripts and scopes, you still have to copy and paste the function between the scopes. Before we can solve this problem, lets take a look at what goes on when using the filter method.

Background

// http://github.com/jquery/jquery/blob/1.4.2/src/traversing.js#L75
filter: function( selector ) {
	return this.pushStack( winnow(this, selector, true), "filter", selector );
},

This doesn't tell us much other than it utilizes the winnow function to create a new stack.

// http://github.com/jquery/jquery/blob/1.4.2/src/traversing.js#L8
var winnow = function( elements, qualifier, keep ) {
	if ( jQuery.isFunction( qualifier ) ) {
		return jQuery.grep(elements, function( elem, i ) {
			return !!qualifier.call( elem, i, elem ) === keep;
		});

	} else if ( qualifier.nodeType ) {
		return jQuery.grep(elements, function( elem, i ) {
			return (elem === qualifier) === keep;
		});

	} else if ( typeof qualifier === "string" ) {
		var filtered = jQuery.grep(elements, function( elem ) {
			return elem.nodeType === 1;
		});

		if ( isSimple.test( qualifier ) ) {
			return jQuery.filter(qualifier, filtered, !keep);
		} else {
			qualifier = jQuery.filter( qualifier, filtered );
		}
	}

	return jQuery.grep(elements, function( elem, i ) {
		return (jQuery.inArray( elem, qualifier ) >= 0) === keep;
	});
};

This is part of what we need. The winnow function is a proxy for both the filter and not methods. It runs the qualifier on the current stack, and then returns a new list of elements that passed the qualifier(or failed for not method). Lets break it down for a better understanding.

  1. If a function is passed, we loop through each of the current elements, and add those that pass the function into a new array and return it
  2. If and element is passed, we search through the current stack for the element that matches the one passed, and return an array containing only that element.
  3. If a string is passed, then Sizzle is used to filter based on the query and returns that array.
  4. The last catch-all handles instances where an array of elements is passed into the not method to check against and remove from the current stack. You can pass an array of elements for the filter method as well, but it would be faster to just use jQuery( [ elems ] ), since that's exactly what you need.
// http://github.com/jquery/jquery/blob/1.4.2/src/core.js#L198
pushStack: function( elems, name, selector ) {
	// Build a new jQuery matched element set
	var ret = jQuery();

	if ( jQuery.isArray( elems ) ) {
		push.apply( ret, elems );

	} else {
		jQuery.merge( ret, elems );
	}

	// Add the old object onto the stack (as a reference)
	ret.prevObject = this;

	ret.context = this.context;

	if ( name === "find" ) {
		ret.selector = this.selector + (this.selector ? " " : "") + selector;
	} else if ( name ) {
		ret.selector = this.selector + "." + name + "(" + selector + ")";
	}

	// Return the newly-formed element set
	return ret;
},

Alright, so after a new set of elements are configured, we move onto the pushStack method, which creates a blank jQuery instance, merge the new set into it, store a reference to the pre-filter instance for use in the end method, and set the context and selector properly.

The Duck Punched Filter

So how do we hack this to allow for dynamic arguments passed into the qualifier? We duck punch it.

/*
 * Duck Punched Filter
 * May 1, 2010
 * Corey Hart @ http://www.codenothing.com
 */
(function( $, undefined ) {

// Keep a copy of the old filter for reference
var old = $.fn.filter;

// Create space for holding filter custom filter functions
$.filters = {};

// Overload the filter method
$.fn.filter = function( pass, args, fn ) {
	// Make sure the qualifier was passed, otherwise use old filter
	if ( args === undefined ) {
		return old.apply( this, arguments );
	}

	// Reorganize
	if ( $.isArray( pass ) ) {
		fn = args;
		args = pass;
		pass = false;
	}
	else if ( $.isFunction( args ) ) {
		fn = args;
		if ( typeof pass == 'boolean' ) {
			args = [];
		} else {
			args = pass;
			pass = false;
		}
	}

	// Setup qualifier to allow a stored reference or anonymous function
	fn = typeof fn == 'string' ? $.filters[ fn ] : fn;
	var self = this, elems = [], i = -1, l = self.length;

	// Allow for full control of elems stack creation
	if ( pass === true ) {
		// Convert the current stack into array of elements, and spot
		// it at the first position
		args.unshift( self.toArray() );
		elems = fn.apply( self, args ) || [];
	}
	else {
		// Configure new set of elements based on qualifier and the 2 first 
		// spots in the arguments for index and current element
		args.unshift( null );
		args.unshift( null );
		for ( ; ++i < l; ) {
			args[ 0 ] = i;
			args[ 1 ] = self[ i ];
			if ( fn.apply( self[ i ], args ) === true ) {
				elems.push( self[ i ] );
			}
		}
	}

	// Make the new jquery object
	return self.pushStack( elems, 'filter', fn );
};

})( jQuery );

With the above patch, we can setup a basic filter in a different scope.

jQuery.filters.MyFilter = function( i, elem, key, value ) {
	// Check the elements data cache based on the dynamic arguments
	// Return true to push the element onto the new stack
	return jQuery.data( elem, key ) === value;
};

And then we can call that new filter from anywhere within our applications. Here's a basic call that will chop the stack into elements that only have a 'coolname' data key of 'kaeden'.

// Using the function reference directly
jQuery('div').filter( [ 'coolname', 'kaeden' ], jQuery.filters.MyFilter );

// Using the key mapping to jQuery.filters space created by this patch
jQuery('div').filter( [ 'coolname', 'kaeden' ], 'MyFilter' );

Loop Reduction

Even better, we can reduce the function calls on every iteration, and control the full loop from within the filter. Here's the filter setup.

jQuery.filters.MyBetterFilter = function( elems, key, value ) {
	var newStack = [], i = -1, l = elems.length;

	for ( ; ++i < l; ) {
		if ( jQuery.data( elems[ i ], key ) === value ) {
			newStack.push( elems[ i ] );
		}
	}

	return newStack;
};

And to call the better filter, we pass true as the first argument to force full control.

// Using the function reference directly
jQuery('div').filter( true, [ 'coolname', 'kaeden' ], jQuery.filters.MyBetterFilter );

// Using the key mapping to jQuery.filters space created by this patch
jQuery('div').filter( true, [ 'coolname', 'kaeden' ], 'MyBetterFilter' );

I must point out that this hacked filter has only been tested with jQuery-1.4.2, and is in no way future proof. In any case, you can both play with the demo, and download it below. Please, let me know if you find a bug.

Download

Latest: duck-punched-filter.zip
Released: May 1, 2010
-Initial Release
Have a question or comment? ask@codenothing.com.