It’s fairly common practice in jQuery to bind events to a quick anonymous function that performs the action you desire. But how do you bind an event to a non-anonymous function – something that you’ve already extracted into a function so as to avoid reptition? I learned this one the hard way, and thought it was worth sharing.

I was working on some jQuery to apply to some legacy HTML at work, and was fiddling around to see if our goal was possible.

I was working on a “springy” archive menu. There are some headers and some lists; the lists are hidden on pageload, and when you click on a header, the subsequent list (ie the archive for that year) opens. Many lists can be open or closed at once. In addition, we toggle a class on the header (rather than the header link, which is there purely out of legacy code for now) to change an image, indicating that it’s open.

Here’s my proof of concept code – my first working version to demonstrate how this functionality would work.

$(document).ready(function() {
	var headlinks = $("div.container h3 a");

	for (var i=0; i < headlinks.length; i++) {
		$(headlinks[i]).bind("click", function() {
			$(this).parent().next().toggle();
			$(this).parent().toggleClass("open");
		});              
		$(headlinks[i]).parent().next().toggle();
		$(headlinks[i]).parent().toggleClass("open");
	};

	toggleArchiveLinks(headlinks[0]);
	$(headlinks[0]).parent().next().toggle();
	$(headlinks[0]).parent().toggleClass("open");
});

Now we've got the code working, it's time to refactor. There's a lot of repetition going on - three instances of that "toggle visibility and toggle class" behaviour, so let's pull that out into a function:

function toggleArchiveLinks(element) {
	$(element).parent().next().toggle();
	$(element).parent().toggleClass("open");
	return false;
}

Much better. Unfortunately, we can't replace our anonymous function within that bind directive with this. I initially thought you could bind "click" to toggleArchiveLinks(this). But that doesn't work, because in the context of events, what gets passed out to another function is the event object itself. (I think it works fine in the anonymous object due to the way things are scoped).

But it's a bit ugly to refactor some, but not all of the code. After looking at the jQuery docs for bind, it turns out that there's a third parameter you can pass in: a data object. This is made available to a handler function. So that means we can pass information about the element we want to toggle to a handler event. We write our new bind directive like this:

$(headlinks[i]).bind("click", {element: headlinks[i]}, handleToggleEvent);

That object in the middle will be made available to a new function handleToggleEvent. (We could, of course, pass as many key/value pairs as we wanted to to the function). We also need to write handleToggleEvent. That function looks like this:

function handleToggleEvent(event) {
	toggleArchiveLinks(event.data.element);
	event.preventDefault();
}

The function accepts an event as a parameter, and the object/hash from our bind statement is available as event.data. We're then free to call toggleArchiveLinks on the element of our choosing. Finally, we have to call event.preventDefault in order to stop the event propagating any further. If we don't do this, the bound behaviour will happen, and then the link will click through as normal. return false; won't work here, because we're actually dealing with the event itself, not just an anonymous function.

So we've now managed to refactor some repetitive code and call it from a bind statement. Our final jQuery script looks like this:

$(document).ready(function() {
	var headlinks = $("div.container h3 a");

	for (var i=0; i < headlinks.length; i++) {
		$(headlinks[i]).bind("click", {element: headlinks[i]}, handleToggleEvent);                
		toggleArchiveLinks(headlinks[i]);
	};
	toggleArchiveLinks(headlinks[0]);
});

function toggleArchiveLinks(element) {
	$(element).parent().next().toggle();
	$(element).parent().toggleClass("open");
	return false;
}

function handleToggleEvent(event) {
	toggleArchiveLinks(event.data.element);
	event.preventDefault();
}

Which is much better, I think.

4 comments on this entry.

  • malsup | 6 Aug 2007

    Tom,

    You make a great point about dealing with function arguments based on context. But it is possible to use a single fn for the toggling, although it’s somewhat less intuitive:

    
    $(document).ready(function() {
        var $links = $("div.container h3 a");
        $links.click(toggleArchiveLinks);
        toggleArchiveLinks($links[0]);
    });
    
    function toggleArchiveLinks(event) {
        var element = event.href ? event : this;
        var $p = $(element).parent();
        $p.toggleClass("open").next().toggle();
        return false;
    }
    
  • Vincent Robert | 10 Aug 2007

    You actually wanted to toggle the class for every link except the first one (which you toggle twice here).

    Why not just implement directly what you meant ? Here is a simpler version :

    $(document).ready(function() {
    	$("div.container h3 a")	
    		// Add the event handle "onclick"
    		.click(function() {
    			$(this).parent()
    				.next()
    					.toggle()
    				.end()
    				.toggleClass("open")
    				;
    		})
    		// Trigger it manually for every link exept the first one
    		.filter(":gt(0)")
    			.click()
    		.end()
    		;
    	};
    });
    

    I didn't tested but it should work. Hope this helps.

  • Sam | 6 Aug 2008

    I’m getting tired of seeing blog posts where the author doesn’t reply to good comments and questions.

    Great way to make sure whatever readers you do have, stop.

  • Tom | 7 Aug 2008

    Sorry you feel that way, Sam. I felt that Vincent’s code stood well on its own: it’s expressive, it’s better code than mine, and it works. I probably should have said “yes, that works”, but I didn’t actually have time to test it. You know, due to being very busy, and my code also working (even if it’s not as expressive).

    So, you know: Vincent’s code is good, it works, it’s better than mine; it feels like a nice footnote on the whole endeavour. Anyhow, now I’ve replied. Happy?

    If, of course, you’re a spambot, I’m wasting my time writing back to you. But it’s worth a pop.