Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Kris Jones

How Much Should jQuery Event Bindings Handle?

By Ben Nadel on

Last week at the jQuery NYC meetup, I posed the following question: How much should jQuery event bindings actually handle? When I write spaghetti code, the answer to this is fairly straightforward - whatever gets the job done, refactor as necessary. But, as I move into a more object-oriented, Component-based approach, the answer to this question becomes somewhat less clear in my mind. Unfortunately, no one at the jQuery meetup seemed interested in having a philosophical debate as to where code should be placed. But, this type of question is still very important to me; if I can't truly explain why I do something in a certain way, then I feel like I don't truly understand what I'm doing.

To explore this topic, I'm going to set up a very simple demonstration in which a Javascript component handles the event delegation on an HTML table. In the following code, the Javascript class, TableController, manages the click events on its UI counterpart, removing table rows from the given table as necessary:

  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>How Much Should jQuery Event Bindings Handle?</title>
  • <style type="text/css">
  •  
  • table a {
  • color: #CC0000 ;
  • }
  •  
  • </style>
  • <script type="text/javascript" src="../jquery-1.4.2.js"></script>
  • <script type="text/javascript">
  •  
  • // I manage the states and event handlers of the table.
  • function TableController( target ){
  • var self = this;
  •  
  • // Store the target as the UI aspect of this component.
  • this.ui = target;
  •  
  • // Delegate all clicking on the table.
  • this.ui.click(
  • function( event ){
  •  
  • // Prevent the event default action. Since we
  • // delegating events, we don't want any click
  • // to have an inherent meaning until we say
  • // it should.
  • event.preventDefault();
  •  
  • // Get the target of the event.
  • var target = $( event.target );
  •  
  • // Get the closest link to see if the click came
  • // from a link.
  • if (target.is( "a" )){
  •  
  • // Confirm with the user that they really
  • // intended to remove the selected row.
  • if (confirm( "Are you sure?" )){
  •  
  • // Remove the selected row.
  • target.closest( "tr" ).remove();
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  • };
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // When the DOM is ready, initialize the script.
  • $(function( $ ){
  •  
  • // Create an instance of the table controller and
  • // give it domain over the table.
  • new TableController( $( "table" ) );
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • How Much Should jQuery Event Bindings Handle?
  • </h1>
  •  
  • <table border="1" cellpadding="5" cellspacing="1">
  • <thead>
  • <tr>
  • <th>
  • Name
  • </th>
  • <th>
  • <br />
  • </th>
  • </tr>
  • </thead>
  • <tbody>
  • <tr>
  • <td>
  • Vicky
  • </td>
  • <td>
  • <a href="#">Remove</a>
  • </td>
  • </tr>
  • <tr>
  • <td>
  • Tricia
  • </td>
  • <td>
  • <a href="#">Remove</a>
  • </td>
  • </tr>
  • <tr>
  • <td>
  • Erika
  • </td>
  • <td>
  • <a href="#">Remove</a>
  • </td>
  • </tr>
  • </tbody>
  • </table>
  •  
  • </body>
  • </html>

In this version of the code, you can see that the jQuery event binding (this.ui.click) is handling all of the necessary logic:

  • Cancelling the default behavior of the event.
  • Checking to see if a "Remove" link triggered the event.
  • Prompting the user for a removal confirmation.
  • Removing the ancestral TR from the table.

Like I said before, in a spaghetti code situation, this would be just fine with me; but, as this UI element and event binding are being managed by an Object in a componentized approach, this strategy doesn't feel quite right. Some of these actions don't feel completely related to the event, and therefore, probably shouldn't be contained completely within the event handler.

Cancelling the event's default behavior and checking to see if the target of the event is a "Remove" link both feel very much part of the event binding; after all, they are directly tied to the existence of the jQuery Event object. As such, I think they should only be part of the event binding and not factored out - outside of an event binding, they simply wouldn't make sense.

Removing the TR from the table, on the other hand, feels like a very generic action. There's nothing about it that makes me think it couldn't exist outside of this event delegation, or that it even has to be exclusive to this particular instance of event delegation. As such, let's try a version of this code where "removing the TR" has been factored out into a class method of the TableController class.

NOTE: In the remaining demos, I'm only going to show the Javascript as none of the HTML has changed.

  • // I manage the states and event handlers of the table.
  • function TableController( target ){
  • var self = this;
  •  
  • // Store the target as the UI aspect of this component.
  • this.ui = target;
  •  
  • // Delegate all clicking on the table.
  • this.ui.click(
  • function( event ){
  •  
  • // Prevent the event default action. Since we
  • // delegating events, we don't want any click
  • // to have an inherent meaning until we say
  • // it should.
  • event.preventDefault();
  •  
  • // Get the target of the event.
  • var target = $( event.target );
  •  
  • // Get the closest link to see if the click came
  • // from a link.
  • if (target.is( "a" )){
  •  
  • // Confirm with the user that they really
  • // intended to remove the selected row.
  • if (confirm( "Are you sure?" )){
  •  
  • // Remove the selected row.
  • self.removeRow(
  • target.closest( "tr" )
  • );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  • };
  •  
  • // Define the class methods for this controller.
  • TableController.prototype = {
  •  
  • // I remove the given table row.
  • removeRow: function( tr ){
  • tr.remove();
  • }
  •  
  • };

As you can see this time, we've created a component class method, removeRow(), which encapsulates the implementation of the TR element removal. Then, rather than having the jQuery event binding remove the TR directly, it invokes this class method to perform that action. This is a very minor change, but emotionally, I like the direction in which we're going.

Something still doesn't feel right. Perhaps it's the user prompt in the event delegation? In this next version, I'm going to try factoring more of the event handling into class methods:

  • // I manage the states and event handlers of the table.
  • function TableController( target ){
  • var self = this;
  •  
  • // Store the target as the UI aspect of this component.
  • this.ui = target;
  •  
  • // Delegate all clicking on the table.
  • this.ui.click(
  • function( event ){
  •  
  • // Prevent the event default action. Since we
  • // delegating events, we don't want any click
  • // to have an inherent meaning until we say
  • // it should.
  • event.preventDefault();
  •  
  • // Get the target of the event.
  • var target = $( event.target );
  •  
  • // Get the closest link to see if the click came
  • // from a link.
  • if (target.is( "a" )){
  •  
  • // Handle the row removal request.
  • self.handleRowRemoveRequest(
  • target.closest( "tr" )
  • );
  •  
  • }
  •  
  • }
  • );
  • };
  •  
  • // Define the class methods for this controller.
  • TableController.prototype = {
  •  
  • // I handle the row removal request made by the user.
  • handleRowRemoveRequest: function( tr ){
  •  
  • // Confirm with the user that they really
  • // intended to remove the selected row.
  • if (confirm( "Are you sure?" )){
  •  
  • // Remove the selected row.
  • this.removeRow( tr );
  •  
  • }
  •  
  • },
  •  
  •  
  • // I remove the given table row.
  • removeRow: function( tr ){
  • tr.remove();
  • }
  •  
  • };

This time, I created a component class method, handleRowRemoveRequest(), which further encapsulates the implementation of the event delegation. Unfortunately, this approach makes me feel a bit nauseous. I think anytime we have methods that contain terms like "handle" or "on" within the method name (ex. onClick, handleClick, onClickHandler), we've taken a seriously wrong turn.

Something about this last approach feels very clearly wrong; at least, much more wrong that my second approach. So, let me think a bit more about why my second approach feels wrong. Upon reflection, I don't think I care that the user prompt (confirm) is in event handler. In fact, I can start to see the user prompt as being directly related to the event, and not so much the final gesture. Think about it this way - if I wanted to programmatically delete each row in the table, would I prompt the user for each row? Or, would I just prompt them for the overall delete gesture? Clearly, the prompt would be for the overall gesture - "Are you sure you want to delete ALL rows?" - and not for the individual rows.

So, if the user prompt should be in the event binding (for argument's sake), then is there anything in the second demo that I should be reacting to in a negative manor?

Perhaps, what I am reacting to here is that the event binding it translating the UI event into a UI-only gesture. Meaning, clicking on the "Remove" link is being treated as if it has UI consequences only. If this TableController is meant to contain both the business logic and the UI logic of the table markup, then shouldn't the event binding translate into a broader gesture more related to the application and less specifically to the UI?

  • // I manage the states and event handlers of the table.
  • function TableController( target ){
  • var self = this;
  •  
  • // Store the target as the UI aspect of this component.
  • this.ui = target;
  •  
  • // Delegate all clicking on the table.
  • this.ui.click(
  • function( event ){
  •  
  • // Prevent the event default action. Since we
  • // delegating events, we don't want any click
  • // to have an inherent meaning until we say
  • // it should.
  • event.preventDefault();
  •  
  • // Get the target of the event.
  • var target = $( event.target );
  •  
  • // Get the closest link to see if the click came
  • // from a link.
  • if (target.is( "a" )){
  •  
  • // Confirm with the user that they really
  • // intended to remove the selected row.
  • if (confirm( "Are you sure?" )){
  •  
  • // Remove the Girl associated with the
  • // selected row.
  • self.removeGirl(
  • target.closest( "tr" )
  • );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  • };
  •  
  • // Define the class methods for this controller.
  • TableController.prototype = {
  •  
  • // I remove the Girl associated with the given row.
  • removeGirl: function( tr ){
  • // Here, I would perform any business logic that
  • // might be part of what it means to remove a girl
  • // from this component. Example - and AJAX call
  • // or a request to a service component.
  • //
  • // GirlService.removeGirl( tr.data( "id" ) );
  • //
  •  
  • // Now that our business logic has been executed,
  • // let's take care of the UI fallout.
  • this.removeRow( tr );
  • },
  •  
  •  
  • // I remove the given table row.
  • removeRow: function( tr ){
  • tr.remove();
  • }
  •  
  • };

In this approach, the jQuery event binding calls a new method, removeGirl(). While this might seem like a really tiny change - going from removeRow() to removeGirl() - the difference is rather large. The former only implies a physical change to the table; the later implies a change to the collection of girls as a conceptual entity within our application. Part of that change may include a physical (UI) change, or, it may not; the point here is that I do not think it is the event binding's place to make that decision? Rather, that decision should be handed off to a core part of the component where that gesture may be easily invoked by any number of starting points.

Most of this was thinking out loud, so I am sure it is riddled with flaws; but, the last demo seems to sit the best in my head. I think a lot of the problems I was having with event bindings that I've been programming is that they are too much based on UI changes and not enough based on "user gestures." When I start to think in terms of what the intent of the user is, I think it starts to become more clear where code should be placed.

As a final note, you might think that I am over thinking the situation. This is definitely possible! Sometimes, this kind of analysis just helps me move past mental blocks.




Reader Comments

Nice!

". Unfortunately, no one at the jQuery meetup seemed interested in having a philosophical debate as to where code should be placed. But, this type of question is still very important to me; if I can't truly explain why I do something in a certain way, then I feel like I don't truly understand what I'm doing."

I agree. It's too bad it didn't catch anyone's interest. Maybe it was just the night or maybe how it was presented. Then again, there are a lot of people who are happy to work on code without giving it a lot thought. Just as you can find a lot of mechanics who couldn't care less about addressing the question of "why is the engine in the front of the car? Why not the back? Or the middle?"

So it goes.

Reply to this Comment

@Allen,

I like to think it was just the night. Our meeting was kind of unorganized and I didn't even have a laptop with me to show a demo.

Reply to this Comment

Hey! I'm pretty sure we *did* talk about this for at least a couple of minutes. :)

As I mentioned at the meetup, I've been more partial to an approach like this, which encapsulates the idea of the data it's representing into a module and doesn't really use anonymous functions, which helps with organisation a great deal, imo. It also allows for access to the Girl module via jQuery.data()

function Girl(row) {
var removelink = row.find("a.remover").click(remove),
girl = {
elem:row,
name:row.find("td.name").text(),
remove:remove
};


function remove(e) {
if (confirm("R U sure?")) {
delete TableController.girls[girl.name];
row.remove();
}
return false;
}

TableController.girls[girl.name] = girl;
return girl;
}

TableController.ui.find("tr.girl").each(function() {
var $this = $(this);
$this.data("girl",new Girl($this));
});

Reply to this Comment

@Adam,

Sorry if I misunderstood; I thought that stuff was part of a totally different discussion - something about the Module pattern?

That said, I am not sure that I follow all of your decisions. In this, you have the remove() method both prompting and returning false. This would seem to tightly couple the concept of remove() to the event itself.

How would this deal with something like a deleteAll() method on the table? I would assume the event on the table could trigger something like:

$.each( TableController.girls,
function( name, girl ){
girl.remove();
});

... But this would prompt for each record being removed - clearly something that we don't want to have the user content with.

This is why I was saying that it would make sense to factor out some of the UI-only methods such that the more "business" related methods could invoke them from various starting points.

Plus, are there concerns here about memory? The beauty of an object Prototype is that all instances of a class share the same function reference. By defining methods internally, it seems that every single instance has it's own set of functions. Is there concern here about memory usage?

What is the selling point about anonymous functions over centralized definitions? To be honest, I am not sure that I understand the down-side of anonymous methods to begin with. Perhaps you could talk a little on that?

This is a really fascinating conversation to me - I hope that I don't come off as condescending; I am truly interested.

Reply to this Comment

@Adam,

It just occurred to me that we are both creating per-instance methods; you do it with your internal functions - I do it with my anonymous functions. That said, if you respond, no need to respond that part of my previous comment :)

Reply to this Comment

Yah, you're right, if I was taking more time, I would have uncoupled the confirmation from the remove a bit more, or had a flag on the remove function that allowed for unconfirmed removal.

The reason I don't like anonymous functions is that they're very hard to talk about and refer to. A named function has semantic value, whereas anonymous functions are just "the anonymous function I bound to X element." They tend to be harder to locate and harder to describe to another person, compared to an object method.

I hashed that example together pretty quickly, it isn't *exactly* how I'd do it, especially because using something nicer than confirm() requires a more complex, evented logic for handling removals.

Reply to this Comment

@Adam,

Good point re: the ability to talk about them. I think I tend to just refer to them as "event handlers". Of course, if you don't know what code I am referring to, it doesn't give you anything to search for in the markup.

My biggest concern with feeling the "pressure" to give my event handlers names is that I don't want to fall into having methods with "bad smelling" names like "handleClick" and "onRemoveClick" and very implementation-based naming. When you have to name your event handler, you have to come up with meaningful names for both the event handler and the methods that the event handler might turn around and invoke.

I'll be honest with you - I'm not all that great at naming methods, which is part of why I find explorations like this very helpful; it really helps me to get a handle on the "why" behind the "why", which will help my terminology.

As far as an extended logic for the "prompt", I totally agree. I was thinking of bringing it up in the original blog post, but I couldn't even think clearly about it. A Javascript confirm() is so nice because it actually halts the work flow. If I wanted to switch over to something like a jQuery Modal confirmation window, clearly I'd have to fall back on a whole callback route, which totally changes the playing field in terms of what kinds of methods I would need.

... but perhaps that's a whole other conversation.

Reply to this Comment

Copy from twitter reply about @fcocabezas What are you waiting for? Is there something you want to see more of? Let me know., here we go!

"Just the clear discussion about event bindings handle. By time limits, always I got a spaghetti code somewhere. Sorry by delay."

Greetings

Francisco

Reply to this Comment

maybe i'm missing the whole point here, and maybe it is specific to jquery bindings, but why not just write a helper function or class
and include it directly in the class or prototype the method to the class, and use jquery or plain old javascript to callt he method?

the options above all seem unessesarily long. personally i'd follow adam's advice and use handleRowRemoveRequest which seems like a pretty reasonably named function for what it actually does instead of using an anonymous function, and it can easilt be re-used on another project.

i'm new to OOPJS, but what am i missing here?

what i tend to do is make my classes as small as possible, and link to the methods, which i write outside of the class, so my methods are usually not included within the class itself this way i can link the same method to more than one class if need be, or just sub-class it and it would be inherited from the parent class automatically, so i use them more like a function library and link them into the classes as need be.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.