Deferred object as callback

Posted:  October 10th, 2012 comments:  6

Callbacks are a nice way of Iversion of control, but are even more powerful with deferred objects.
Basically callbacks are a way to pass back the control from a called function, the deferred/promise just do the opposite: it returns the control the called function.

Consider this scenario: a table widget, with the classic delete/modify buttons next to each row. Since the table widget is a generic component, we want to handle the delete operation on the model outside the widget, and we don’t event want to extend the generic object to do that, we want to user a callback:

$.widget('my_table_widget',{
  onDelete: function(id_of_row) {
    var that = $(this);
    // call the backend and delete the record
    $.ajax({
      method: 'delete',
      url: '/api/records/'+id_of_row
      })
      .done(function() {
        // ok, record delete, now I should update the table
        that.widget('remove_row',id_of_row);
        });
      }
    });

In the example above: the callback first calls the backend to remove the record and then removes the row from the table (I mean the DOM). In order to do this: it needs to know something about the table widget (in this case, the method “remove_row”).
But what if we could pass the ball back to the widget? Let’s rewrite it with a deferred object:

$.widget('my_table_widget',{
  onDelete: function(id_of_row) {
    var deferred = $.Deferred();
    // call the backend and delete the record
    if (confirm('Delete?')) {
      $.ajax({
        method: 'delete',
        url: '/api/records/'+id_of_row
        })
        .done(function() {
          // ok, record delete, now I should update the table
          deferred.resolve();
          })
        .fail(function() {
          deferred.reject();
          })
      }
    else {
      deferred.reject();
      }
    return deferred.promise();
    }
  });

It’s more linear but, most important, the callback knows nothing about the remove_that_damn_row_from_the_dom method of the widget, it just passed back the control, it’s like saying “I’m done, it’s your turn”.
More separation, less documentation to read, easier to implement, less errors.

On the widget side, the callback should be treated this way

// somewhere inside the table widget, here is where we execute the callback
var callback_result = options.onDelete.call(widget,id);
// if the callback answer with a deferred/promise, this will
// handle it when it's resolved/rejected
if (isPromise(callback_result)) {
  callback_result
    .done(function() {
      // ok, asynch operation on the other side is over
      // remove the row from the dome
      widget.widget('remove_row',id);
      })
    .fail(function() {
      // do nothing
      })
  }
  • Categories

  • Tags

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 2 other subscribers

  • Follow me on Twitter

    • Kaspars

      There is a problem, callback_result promise will never fail if anything goes wrong. Instead you should
      a) also call deferred.reject() if request fails or
      b) in onDelete just return the same promise which was returned by $.ajax

    • Guidone

      Thanks Kaspars for your comment.
      You’re right I should have handled also the reject, but I just wanted to keep the example simple.
      And yes, in that example it’s possible to pass the same promise returned by $.ajax(), in a real case scenario should be a confirm dialog there, and in that case a new promise object is needed.
      I’m emending the post, just to make the example more clear.

    • NicoGranelli

      I really like this idea, one the most interesting things I’ve read in the last few weeks.

      Consider myself inspired :)

      • guidone

        Thanks Nico, glad to hear that

    • http://decodify.blogspot.co.uk/ Barry

      Nice article I find myself using this pattern more and more.

      A suggestion…

      Instead of using isPromise(result) you can use $.when(result).done(…

      From the docs:
      “If a single argument is passed to jQuery.when and it is not a Deferred, it will be treated as a resolved Deferred and any doneCallbacks attached will be executed immediately.”

    • guidone

      Thanks Barry ;-)