3

I have a click handler that needs to make several async calls, one after another. I've chosen to structure these calls using promises (RSVP, to be precise).

Below, you can see the clickA handler, inside the controller (it's an Ember app, but the problem is more general, I think):

App.SomeController = Ember.Controller.extend({

  actions: {

    clickA: function() {
      var self = this;

      function startProcess() {
        return makeAjaxCall(url, {
          'foo': self.get('foo')
        });
      }

      function continueProcess(response) {
        return makeAjaxCall(url, {
          'bar': self.get('bar')
        });
      }

      function finishProcess(response) {
        return new Ember.RSVP.Promise(...);
      }

      ...

      startProcess()
        .then(continueProcess)
        .then(finishProcess)
        .catch(errorHandler);
    }
  }
});

It looks great, but now I have to add a second action that reuses some of the steps.

Since each of the inner functions needs to access properties from the controller, one solution would be to make them methods of the controller:

App.SomeController = Ember.Controller.extend({

  startProcess: function() {
    return makeAjaxCall(url, {
      'foo': this.get('foo')
    });
  },

  continueProcess: function(response) {
    return makeAjaxCall(url, {
      'bar': this.get('bar')
    });
  },

  finishProcess: function(response) {
    return new Ember.RSVP.Promise(...);
  },

  actions: {
    clickA: function() {
      this.startProcess()
        .then(jQuery.proxy(this, 'continueProcess'))
        .then(jQuery.proxy(this, 'finishProcess'))
        .catch(jQuery.proxy(this, 'errorHandler'));
    },

    clickB: function() {
      this.startProcess()
        .then(jQuery.proxy(this, 'doSomethingElse'))
        .catch(jQuery.proxy(this, 'errorHandler'));
    }
  }
});

So, my question is: is there a better way? Can I get rid of all those jQuery.proxy() calls somehow?

scribu
  • 2,958
  • 4
  • 34
  • 44
  • see also: [Object method with ES6 / Bluebird promises](http://stackoverflow.com/q/27149034/1048572) – Bergi Jun 14 '15 at 22:21

3 Answers3

4

A solution would be to use a better promise library.

Bluebird has a bind function which lets you bind a context to the whole promise chain (all functions you pass to then or catch or finally are called with this context ).

Here's an article (that I wrote) about bound promises used like you want to keep a controller/resource : Using bound promises to ease database querying in node.js

I build my promise like this :

// returns a promise bound to a connection, available to issue queries
//  The connection must be released using off
exports.on = function(val){
    var con = new Con(), resolver = Promise.defer();
    pool.connect(function(err, client, done){
        if (err) {
            resolver.reject(err);
        } else {
            // the instance of Con embeds the connection
            //  and the releasing function
            con.client = client;
            con.done = done;
            // val is passed as value in the resolution so that it's available
            //  in the next step of the promise chain
            resolver.resolve(val);
        }
    });
    // the promise is bound to the Con instance and returned
    return resolver.promise.bind(con);
}

which allows me to do this :

db.on(userId)          // get a connection from the pool
.then(db.getUser)      // use it to issue an asynchronous query
.then(function(user){  // then, with the result of the query
    ui.showUser(user); // do something
}).finally(db.off);    // and return the connection to the pool 
Denys Séguret
  • 372,613
  • 87
  • 782
  • 758
  • Interesting, but doesn't that actually violate the `Promises/A+` spec? – Bergi Jan 23 '14 at 18:11
  • @Bergi I don't think so. It's merely an extension. Pekta's library is also listed on the official A+ page : http://promisesaplus.com/implementations – Denys Séguret Jan 23 '14 at 18:14
  • I guess this is what I was looking for. Too bad rsvp.js doesn't support it. – scribu Jan 23 '14 at 18:17
  • @scribu There are probably other reasons to choose Bluebird. It's well known for its performances for example. – Denys Séguret Jan 23 '14 at 18:19
  • @dystroy: Yeah, because they did only test "normal" promises :-) A bound one would violate [requirement §2.2.5](http://promisesaplus.com/#point-41) - but I never liked that spec point anyway – Bergi Jan 23 '14 at 18:25
  • You're right. In fact, as I see it, Bluebird starts from the spec but is used a little differently. `catch` and `finally` are huge additions too in my opinion. – Denys Séguret Jan 23 '14 at 18:37
  • 1
    Yep, RSVP also has `catch` and `finally`. This terminology really helps to drive home the point about why promises are good, IMO. – scribu Jan 23 '14 at 19:55
3

I may be missing something, but would this solve your issue?

actions: (function() {
    var self = this;
    function startProcess() { /* ... */ }
    function continueProcess(response) { /* ... */ }
    function finishProcess(response) { /* ... */ }
    function doSomethingElse(response) { /* ... */ }
    /*  ... */
    return {
        clickA: function() {
            startProcess()
                .then(continueProcess)
                .then(finishProcess)
                .catch(errorHandler);
        },
        clickB: function() {
            startProcess()
                .then(doSomethingElse)
                .catch(errorHandler));      
        }
    };
}());

Just wrap the actions in an IIFE, and store the common functions there, exposing only the final functions you need. But I don't know Ember at all, and maybe I'm missing something fundamental...

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
2

Browsers have a "bind" method on all functions. It's also easy to create a pollyfill for Function#bind.

this.startProcess()
    .then(this.continueProcess.bind(this))
    .then(this.finishProcess.bind(this))
    .catch(this.errorHandler.bind(this));

The jQuery.proxy method essentially does the same thing.

Greg Burghardt
  • 17,900
  • 9
  • 49
  • 92