Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rejecting promises with multiple arguments (like $http) in AngularJS

Callbacks for $http promises have multiple arguments: body, status, headers, config.

I'd like to create similar promise by hand but don't know how to do this. What I'd like to do is more or less:

myservice.action().then(function(status, message, config) {
    // ...
});

I know I could pass object with keys to callback but would like to have similar convention as in $http. I look at the angular sources, but either don't understand it fully or just can't do that right.

Do you know how to create promises that are able to pass multiple arguments to callback/errbacks?

like image 874
grafthez Avatar asked Jul 16 '13 20:07

grafthez


1 Answers

As suggested in the comments, have a look at the $q implementation of AngularJS. The docs are notorious for being... well hard to understand sometimes.

But anyway, let's try a short example. I do this in CoffeeScript, because I prefer it, but you should be able to compile the example at coffeescript.org if you want to.

Let's implement a service:

app = angular.module 'my.custom.services', ['your.other.modules']

app.factory 'Service', ['$http' , '$q', (http, q) ->

  # this is your deferred result
  deferred = q.defer()

  get: ->
    deferred.promise
]

This one is easy. It's just a service, which will make use of $q and $http, because a) we want some of this sweet promise-based stuff we're talking about and b) '$http' is a nice example in itself for something that can call asynchronous and which's result is not available right away.

The interesting part here is the get part of the object that is returned here. Take note, that the service is implemented as a factory, not as a service. For the differences, see here. I usually think of it, as a "fancy" version for a service, where I just have some extra space for my own logic before exposing an API of the service (it really means something different, but thats for another story.

Anyway, get will return the promise of the deferred object when called. The promise is an object, which exposes a then method. When using this service, you'll probably inject it like:

app = angular.module 'my.custom.application', ['my.custom.services']

app.controller 'AppController', ['Service', (service)->

  service.get() # then what?

]

As I mentioned, get will just return a promise. Promises, as in real life, have to be resolved somewhere. So let's do that in the service - our promise will get resolved, whenever we're finished with the task we promised. This can be something like calling a URL via AJAX or a big calculation (anyone knows what the 117th Fibonacci number is?).

For our example, we use an http-call, as we do not now, whether or not and even when it will return to us:

app.factory 'Service', ['$http' , '$q', (http, q) ->

  # this is your deferred result
  deferred = q.defer()

  # this is where http is used, this is started immediately, but takes a while
  http.get('some-resource.json').then (response) ->
    # now 'response' is the whole successful response, it has a data object with the payload
    if !someCondition
      deferred.resolve response.data #we have what we wanted
    else
      deferred.reject {error: "Failed", additional: "foo", number: 2} #we ran into some error

  get: ->
    deferred.promise
]

Based on someCondition we can let the request fail on purpose if we want. If you want to try it your self, you can also use a timeout like in the docs.

What happens now? Well, we still have that controller around:

app.controller 'AppController', ['Service', (service)->

  service.get().then(successCallback, errCallback)

]

As I explained, the promise exposes a then method with the signature then(success, error), wherein success and error are functions that take whatever we resolved as arguments, e.g.:

app.controller 'AppController', ['Service', (service)->
  successCallback = (data) ->
    # we can work with the data from the earlier resolve here
    scope.data = data

  errCallback = (err) ->
    # the error object, we got from the rejection
    console.log err.error # => "Failed"

  service.get().then(successCallback, errCallback)

]

if you want multiple values to be passed to the callbacks, I'd suggest you pass an object when resolving/rejecting the promise. You can also do named callbacks for the promise, like angular does in it's $http implementation:

app.factory 'Service', ['$http' , '$q', (http, q) ->

  # this is your deferred result
  deferred = q.defer()

  # [...]

  get: ->
    promise = deferred.promise

    promise.success = (fn) ->
      promise.then (data) ->
       fn(data.payload, data.status, {additional: 42})
      return promise

    promise.error = (fn) ->
      promise.then null, (err) ->
        fn(err)
      return promise

    return promise 
]

You essentially extend the promise given back by a method success, which takes a single method as a callback, waits for the promise to be resolved and then uses the callback. You can do the same for an any other method if you want to (for hints, see the angular implementation)

In your controller, you can then use it like this:

service.get().success (arg1, arg2, arg3) ->
  # => arg1 is data.payload, arg2 is data.status, arg3 is the additional object
service.get().error (err) ->
  # => err

This is the basics, you can experiment if you want, I suggest you try the following:

  • Multiple promises in a service with multiple deferred promises
  • Try another method of deferring a result, like a long calculation

And, as a bonus:

  • Try isolating the success/error callbacks into named methods of the service

This example uses the $q implementation in Angular, you can of course use another implementation for promises, like this one, which is the basis for $q.

like image 77
Florian Avatar answered Sep 20 '22 17:09

Florian