Shorthand methods for unbind

read 12 comments

Someone sent me an email the other day, asking that I add shorthand methods for .unbind(eventType) to the jQuery core file. He argued that since jQuery provides shorthands such as .click() for .bind('click'), it should also include .unclick() for .unbind('click') for consistency. But he didn't consider two things:

  1. I can't change jQuery's API.
  2. Those shorthand methods used to be part of jQuery core, but with the release of 1.0, John Resig cleaned up the API quite a bit, removing all of the .unEvent() and .oneEvent() methods.

While I understand the desire for simplicity in developers' code and consistency in jQuery's API, I think Mr. Resig made the right decision removing the shorthand methods. Keeping both "un" and "one" shorthands in there would have meant an additional 44 methods, with very little gain for most users of the library. Nevertheless, it's fairly trivial to create a plugin for this sort of thing.

If we want the shorthand methods for a particular project, we can simply iterate through an array of event types and add the "un" or "one" method to the jQuery prototype (jQuery.fn). Here is what it might look like:

Continue Reading Below
JavaScript:
  1. (function($) {
  2.   var eventTypes = ['blur','focus','resize','scroll','click','dblclick',
  3.                   'mousedown','mouseup','mousemove','mouseover',
  4.                   'mouseout','mouseenter','mouseleave','change','select',
  5.                   'submit','keydown','keypress','keyup','error'];
  6.  
  7.   $.each(eventTypes, function(index, eventType) {
  8.     jQuery.fn[ 'un' + eventType ] = function( fn ) {
  9.       return this.unbind( eventType, fn ) ;
  10.     };
  11.   });
  12. })(jQuery);

To add the "one" shorthand methods, we can repeat lines 8– 9, using one in place of both un in line 8 and unbind in line 9.

Update

My apologies to everyone who saw this article as it was originally written. I messed things up when I replaced jQuery's $.each() method with a native for loop and failed to test the revision. I've modified it once more, putting the $.each() method back in there. If you look at the jQuery core file where it creates .click() and friends, you'll see where I got the idea.

Many thanks to Collin, Ralf, and especially Matthew, who were very kind to post comments pointing out the problems with the original script. I put all three versions — the original, the modified one above, and Matthew's version — in a single file for your downloading pleasure.


comment feed

12 comments

  1. That's pretty clever!

    One question: Would it make better sense to use 'var i' inside the 'for' loop, to keep 'i' local instead of global (where it may be overwritten by some other code)?

    • You're absolutely right, Collin. That should be var i. Fixing it now.

      • You actually need capture the current value of i or the corresponding eventType in each iteration of the loop. The way it is now, all the unwhatever functions would call this.unbind(undefined, fn) because each function keeps a reference to the single i variable, which is equal to etlength at the end of the loop. The loop should look like this:

        for (var i=0; i < etLength ; i++) {
            $.fn[ 'un' + eventTypes[i] ] = (function(type) {
                return function(fn) {
                    return this.unbind( type, fn );
                };
            })(eventTypes[i]);
        }

        Of course, you could also factor the outer anonymous function into a makeUnbind function or something like that since it doesn't need to close over any local variables.

        • Ugh. What a mess. Thanks, Matthew, for pointing this out. I shouldn't have rewritten the script without testing it right before I published the entry.

  2. Hi,

    The eventType array contais 'load' twice. In fact, 'load' should be completely removed from that list. The problem is the unload event (the one which is triggered when you leave a page). Your code will make this event inaccessible via jQuery.

  3. Really... I think we should expose that array of events in the core. I've used it in plugins like 2 or 3 times at least.
    Great post.

  4. I like the idea of keeping the jQuery API clean, and provides these functionalities in a plugin as you do. I'm considering using it. Thanks.

  5. Keegan Watkins

    Nice work, Karl! I tend to agree that this sort of functionality shouldn't be in the core, especially considering how easy it is to implement on one's own. I recently developed a plugin for binding multiple events (both native and custom) in a single call. What I liked about this approach is that a.) only one short-hand method is needed, and b.) the plugin is not coupled to event names at all, allowing future support for new/custom events.

    $.fn.on = function(eventMap /* Object */) {
        return this.each(function() {
            var elem = $(this);
            // Iterate through the map, binding each event to each handler
            $.each(eventMap, function(type, handler) {
                elem.bind(type, handler);
            });
        });
    };
    

    Usage is as follows:

    $("#someID").on({
        click : function(e) {
            // Handle click 
        },
        mouseover : function(e) {
            // Handle mouseover
        },
        mouseout : function(e) {
            // Handle mouseout
        }
    });
    

    Or, with named function references:

    $("#someID").on({
        click : handleClick,
        mouseover : handleMouseover,
        mouseout : handleMouseout
    });
    
    function handleClick(e) {
    	// Handle Click
    }
    
    function handleMouseover(e) {
    	// Handle Mouseover
    }
    
    function handleMouseout(e) {
    	// Handle Mouseout
    }
    


    Implementing an unbinding method is equally trivial:

    $.fn.un = function(eventMap /* Object */) {
        return this.each(function() {
            var elem = $(this);
            // Iterate through the map, unbinding each handler
            $.each(eventMap, function(type, handler) {
                elem.unbind(type, handler);
            });
        });
    };
    

    Which enables unbinding in the same manner:

    $("#someID").un({
        click : handleClick,
        mouseover : handleMouseover,
        mouseout : handleMouseout
    });
    

    NOTE: When using the "on" method, the functions corresponding to the event names can be either anonymous functions or references to named functions. In the case of the "un" method, however, the functions MUST be named function references.

    The full plugin:

    (function($) {
       
        $.fn.on = function(eventMap /* Object */) {
            return this.each(function() {
                var elem = $(this);
                // Iterate through the map, binding each event to each handler
                $.each(eventMap, function(type, handler) {
                    elem.bind(type, handler);
                });
            });
        };
       
        $.fn.un = function(eventMap /* Object */) {
            return this.each(function() {
                var elem = $(this);
                // Iterate through the map, unbinding each handler
                $.each(eventMap, function(type, handler) {
                    elem.unbind(type, handler);
                });
            });
        };
       
    })(jQuery)
    

    Keep up the good work!

  6. Keegan Watkins

    Thank you, Karl!

    Unfortunately, I've changed my mind about this topic... again! (I'm a bit obsessive, it's fine though). After commenting above, I now feel that this functionality does in fact belong in the core. My initial objections to adding these features to the Core were that:

    a.) adding frivolous methods sucks. The jQuery namespace is fairly clean and small, and I like it that way. And, ...

    b.) I personally don't like the idea of iterating through an array to produce dynamic method names in a core library. I use the same technique often, but never in the context of a framework; in my experience it leads to maintenance issues over time (but that's just a matter of preference, of course :). And finally, ...

    c.) adding short-hand methods for all events could grow to be unwieldy, especially when you consider that an "add/bind" and "remove/unbind" method would be needed for each event.

    Then after some thought, it dawned on me: What I was really looking for in the comment above was to minimize the number of methods added. The "Holy Grail" of event bindings (for me) would be to be able to create as many (or few) bindings as I need to on a given DOM collection, and to be able to do so easily (using a single method would be ideal). If only jQuery could determine whether:

    a.) a key/value pair were passed as String/Function arguments, or

    b.) a map of type/handler values was passed

    This logic is already in place elsewhere in jQuery, for example in the $().css() and $().attr() methods. Both are able to apply individual key and value arguments, and operate once using them. But they're also capable of applying a map of key/value pairs, iterating over them and applying each pair. So, really, I'm just dreaming of sexier $().bind() and $().unbind() implementations. And so, down the rabbit hole I went...

    ...and here's what I came up with:

    (function($) {
    	
    // Keep a copy of the old methods
    $.fn._bind = $.fn.bind;
    $.fn._unbind = $.fn.unbind;
    	
    // Redefine $().bind()
    $.fn.bind = function( type, data, fn ) {
    	// If only a map of handlers was passed...
    	return (arguments.length === 1) ? 
    		
    	this.each(function(key, node) {
    		// Iterate over the map...
    		$.each(type, function(event, handler) {
    			event == "unload" ?
    				// ... using $.fn.one() for "unload" events...
    				$(this).one(event, handler) :
    				// ... and $.event.add() for others
    				jQuery.event.add( this, event, handler );
    		});	
    	}) :
    
    	// Otherwise, use the existing implementation as of 1.3.2,
    	// with slight syntactic modifications
    	this.each(function(key, node) {
    		type == "unload" ?
    			// Use $.fn.one() for "unload" events...
    			$(this).one(type, fn) :
    			// ... and $.event.add() for others
    			jQuery.event.add( this, type, fn || data, fn && data );
    	});	
    };
    	
    // Redefine $().unbind()
    $.fn.unbind = function(type, fn) {
    	// If only a map of handlers was passed...
    	return (arguments.length === 1) 
    
    	this.each(function(){
    		// Iterate over the map...
    		$.each(type, function(event, handler) {
    			// ... and unbind each using event.remove()
    			jQuery.event.remove( this, event, handler );
    		});
    	}) :
    
    	// Otherwise, use the existing implementation as of 1.3.2,
    	// copied verbatim
    	this.each(function(){
    		jQuery.event.remove( this, type, fn );
    	});
    };
    
    })(jQuery);
    

    Usage would be similar to the existing $().bind() method, but allow for multiple event/handler pairs:

    $("#myID").bind({
    	click : function(e) {
    		// handle click
    	},
    	mouseover : function(e) {
    		// handle mouseover
    	},
    	mouseout : function(e) {
    		// handle mouseout
    	}
    });
    

    Or, with named functions (ideal for later use of $().unbind()):

    $("#myID").bind({
    	click : clickHandler,
    	mouseover : mouseoverHandler,
    	mouseout : mouseoutHandler
    });
    		
    function clickHandler(e) {
    	// handle click
    }
    		
    function mouseoverHandler(e) {
    	// handle mouseover
    }
    		
    function mouseoutHandler(e) {
    	// handle mouseout
    }
    

    With named function references, unbinding multiple events would be a breeze:

    $("#myID").unbind({
    	click : clickHandler,
    	mouseover : mouseoverHandler,
    	mouseout : mouseoutHandler
    });
    

    Now, of course if this were to be absorbed by the Core there would definitely be performance/syntactic/readibility improvements to be made, but it works beautifully as a prototype. The additional check against the number of arguments adds some extra overhead, but for me the ease-of-use justifies the extra processing. Imagine, two simple methods to encapsulate all binding/unbinding behavior... Furthermore, existing scripts which rely on binding events individually would not break, allowing backwards compatibility. All existing logic, such as using $().one() for "unload" events, is still intact. All that has changed is to loop through a map in the case that one is passed. Now, some known issues:

    a.) The ability to pass custom data to $().bind() is lost when an object of event/handler pairs is passed. I personally don't usually use it anyhow, and binding events individually will still support custom data (as the implementation is the same).

    b.) Like the solution in my comment above, passing a map of event/handler pairs to $().unbind() requires that the handlers be references to named functions. Passing identical anonymous functions to $().unbind(), (as they were originally passed to $().bind()), will not work, even if the signatures and/or function bodies are the same.

    What do you think? Would more versatile implementations of $().bind() and $().unbind() render this entire discussion moot? Thoughts? Are you reading this, John Resig :) ?

2 Pings

  1. Shorthand methods for unbind in jQuery « Steele

    [...] or "one" method to the jQuery prototype (jQuery.fn). Here is what it might look like: PLAIN TEXT [...]

Sorry, but comments for this entry are now closed.