Instantly share code, notes, and snippets.

Embed
What would you like to do?
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.

@chuyik

This comment has been minimized.

Show comment
Hide comment
@chuyik

chuyik Feb 4, 2015

I think the handlers style is not so consistent with the prototype style, maybe we could use Function's built-in method, bind.

 $('#launch').click(pad.launch.bind(pad))

chuyik commented Feb 4, 2015

I think the handlers style is not so consistent with the prototype style, maybe we could use Function's built-in method, bind.

 $('#launch').click(pad.launch.bind(pad))
@sylvainpolletvillard

This comment has been minimized.

Show comment
Hide comment
@sylvainpolletvillard

sylvainpolletvillard Nov 12, 2015

ES6 solved the problem with arrow functions:

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

sylvainpolletvillard commented Nov 12, 2015

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