AfterWorkGuinness
AfterWorkGuinness

Reputation: 1850

Making services asynchronous in AngularJS

Forgive my ignorance, I'm new to Angular and JavaScript. I've read that promises make it more elegant to call asynchronous services, but I've not been able to find any documentation on how to make my service asynchronous.

Upvotes: 2

Views: 66

Answers (1)

Josh
Josh

Reputation: 44906

If you are simply wrapping a call to $http then there is no additional work needed on your part. All the $http calls return a promise, so you can just pass that along and your API will be async, and promise based.

function MyService($http){
   this.$http = $http;
}

MyService.prototype = {
   getContacts: function(){
      //$http returns a promise already,
      // so just pass it along
      return $http.get('/api/contacts');
   }
};

angular.service('myService', MyService);

//Later on in some controller

function MyController(myService){
   var _this = this;

   //getContacts() returns the promise
   // from $http
   myService.getContacts()
      .success(function(contacts){
         _this.contacts = contacts;
      });
}

However...

If you want to create a normal API that executes code asynchronously, then you can wrap that up in a promise just like the example in the docs.

JavaScript is basically* single threaded, so that means that it uses an execution queue for asynchronous code. Execution will go from top to bottom, and whenever it stops, the queue will be emptied out in order.

When you call something like setTimeout or setInterval it is simply placing that code on the queue to be executed whenever the current thread gets around to it, but it isn't happening in the background.

You can test this for yourself, by opening up a browser console and typing this code into it:

setTimeout(function(){ console.log("I'm first!"); }, 0);
console.log("Nope, I am!");

Even though I have set the timeout to be 0 milliseconds, that doesn't mean it will execute immediately. It means it will be placed on the queue to be run immediately once all the other code finishes.

Finally

Try not to think of promises as strictly for managing asynchronous calls. They are a pattern for executing code once some precondition has been met. It just so happens that the most popular precondition is some asynchronous I/O via AJAX.

But the pattern is a great way of managing any operation that must wait for some number of preconditions.

Just to really drive this point home, check out this little snippet that uses a promise to determine when the user has clicked the button more than 5 times in a row.

(function() {

  var app = angular.module('promise-example', []);

  function PromiseBasedCounter($q, $timeout) {
    this.$q = $q;
    this.$timeout = $timeout;
    this.counter = 0;
    this.counterDef = $q.defer();
  }
  PromiseBasedCounter.$inject = ['$q', '$timeout'];
  
  PromiseBasedCounter.prototype = {
    increment: function() {
      var _this = this;

      //$timeout returns a promise, so we can
      // just pass that along. Whatever is returned
      // from the inner function will be the value of the
      // resolved promise
      return _this.$timeout(function() {

        _this.counter += 1;

        //Here we resolve the promise we created in the 
        // constructor only if the count is above 5
        if (_this.counter > 5) {
          _this.counterDef.resolve("Counter is above 5!");
        }

        return _this.counter;
      });
    },
    countReached: function() {
      //All defered objects have a 'promise' property
      // that is an immutable version of the defered
      // returning this means we can attach callbacks
      // using the promise syntax to be executed (or not)
      // at some point in the future.
      return this.counterDef.promise;
    }
  };

  app.service('promiseBasedCounter', PromiseBasedCounter);

  function ClickCtrl(promiseBasedCounter) {
    var _this = this;
    _this.promiseBasedCounter = promiseBasedCounter;
    _this.count = 0;

    //Here we set up our callback. Notice this
    // really has nothing to do with asyncronous I/O
    // but we don't know when, or if, this code will be
    // run in the future. However, it does provide an elegant
    // way to handle some precondition without having to manually
    // litter your code with the checks
    _this.promiseBasedCounter.countReached()
      .then(function(msg) {
        _this.msg = msg;
      });
  }
  ClickCtrl.$inject = ['promiseBasedCounter'];
  ClickCtrl.prototype = {
    incrementCounter: function() {
      var _this = this;

      //Whenever we increment, our service executes
      // that code using $timeout and returns the promise.
      // The promise is resolved with the current value of the counter.
      _this.promiseBasedCounter.increment()
        .then(function(count) {
          _this.count = count;
        });
    }
  };

  app.controller('clickCtrl', ClickCtrl);

}());
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<div ng-app="promise-example" ng-controller="clickCtrl as ctrl">
  <div class="container">
    <h1>The current count is: <small>{{ctrl.count}}</small></h1>
    <button type="button" class="btn btn-lg btn-primary" ng-click="ctrl.incrementCounter()">Click Me!</button>
  </div>
  <div class="container">
    <h1>{{ctrl.msg}}</h1>
  </div>
</div>

Upvotes: 2

Related Questions