Skip to content

Instantly share code, notes, and snippets.

@max-mapper
Created August 19, 2012 05:18
Show Gist options
  • Save max-mapper/3392235 to your computer and use it in GitHub Desktop.
Save max-mapper/3392235 to your computer and use it in GitHub Desktop.
put-a-closure-on-it

Put a closure on it

Sometimes you'll have objects that manage state along with event handling. This happens frequently in MVC apps. Let's start with a messy example:

var Launcher = function(rocket) {
  this.rocket = rocket
}

Launcher.prototype.isReady = function() {
  return rocket.isFueled() && rocket.isManned()
}

Launcher.prototype.launch = function(e) {
  if(this.isReady()) {
    $.ajax('/launch', { rocket_id: this.rocket.id, type: this.type })
  } else {
    console.log("can't launch yet")
    e.preventDefault()
  }
}

var pad = new Launcher(rocket)

$('#launch').click($.proxy(pad.launch, pad))

The anti-pattern above, in which an event is bound to an instance function on an object is common in many MVC frameworks and client-side code. The launch function here has to be bound to the Launcher instance in order for the call to this.isReady() to work within that function. this must be bound to the Launcher instance rather than the event. jQuery offers the $.proxy function to accomplish this, while CoffeeScript provides a built-in workaround with the => operator. This example also illustrates the problem where an object begins to take on too many responsibilities - state and event management.

A less messy solution is to create a prototype-specific handler object that wraps each event in a closure:

var Launcher = function(rocket) {
  this.rocket = rocket
  this.type = 'heavy'
}

Launcher.prototype.isReady = function() {
  return rocket.isFueled() && rocket.isManned()
}

Launcher.prototype.launch = function() {
  if(this.isReady()) {
    $.ajax('/launch', { rocket_id: this.rocket.id, type: this.type })
    return true
  } else {
    return false
  }
}

Launcher.handlers = {
  launchClick: function(launcher) {
    return function(e) {
      if(!launcher.launch()) {
        e.preventDefault()
        console.log("can't launch yet")
      }
    }
  }
}

var pad = new Launcher(rocket)

$('#launch').click(Launcher.handlers.launchClick(pad))

This drives more of the DOM-related logic into another object and helps code clarity by specifically enumerating which objects interact with a given event. You also retain access to the this that points to the button being clicked.

A more detailed discussion of this particular closure pattern is here. Additionally, see Hootroot.com's source for a real-world example.

@sylvainpolletvillard
Copy link

ES6 solved the problem with arrow functions:

$('#launch').click(e => pad.launch(e))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment