📜 ⬆️ ⬇️

Promises in AngularJS

One of the key components of almost any web application is interaction with the server. In large applications, this is not a single request. In this case, requests are often necessary to combine for sequential or parallel execution, and often combine both. In addition, large applications usually have a multi-layered architecture - a wrapper over the RESTFul API => business entities => more complex business logic (partitioning for example). And on each layer it is necessary to accept data in one format and transfer to the next layer in another.

All these tasks can be handled by promises.

For details, welcome under cat.

')
Promises provide an interface for interacting with objects containing the result of performing some operation, the end time of which is unknown. Initially, any promise is not resolved (unresolved) and will be resolved either with a specific value (resolved) or rejected with an error (rejected). As soon as a promise becomes resolved or rejected, its state can no longer change, which ensures that the state remains unchanged for any number of checks. That does not mean that at different stages of the checks you will get the same value.

In addition, promises can be combined for both sequential and parallel execution.

Further, the entire description will be built on the basis of AngularJS 1.1.5, and all the examples will be executed in the form of tests .

So what is a promise? This is an object with two methods:


What does AngularJS give you a promise?


Next, we will go through all the options for working with promises.

Simplest use



  var responseData; $http.get('http://api/user').then(function(response){ responseData = response.data; }); 


Nothing interesting - callback and all. But it is necessary to start from something in the presentation? .. :-)

Return values ​​from handlers



  var User = function(data){ return angular.extend(this, data); }; $httpBackend.expectGET('http://api/user').respond(200, { result: { data: [{name: 'Artem'}], page: 1, total: 10 } }); var data = {}; $http.get('http://api/user').then(function(response){ var usersInfo = {}; usersInfo.list = _.collect(response.data.result.data, function(u){ return new User(u); }); usersInfo.total = response.data.result.total; return usersInfo; }).then(function(usersInfo){ data.users = usersInfo; }); 


Thanks to this chain of then you can build a multi-layered application. ApiWrapper made a request, executed common error handlers, gave the data from the response unchanged to the next layer. There, the data was transformed as necessary and given to the next. Etc.

Any return value from then will come in success - callback following then . They did not return anything - in the success-callback will come undefined (see tests).

To do a reject , you need to return $q.reject(value) .
  $httpBackend.expectGET('http://api/user').respond(400, {error_code: 11}); var error; $http.get('http://api/user').then( null, function(response){ if (response.data && response.data.error_code == 10){ return { list: [], total: 0 }; } return $q.reject(response.data ? response.data.error_code : null); } ).then( null, function(errorCode){ error = errorCode; } ); 


Call chains


  $httpBackend.expectGET('http://api/user/10').respond(200, {id: 10, name: 'Artem', group_id: 1}); $httpBackend.expectGET('http://api/group/1').respond(200, {id: 1, name: 'Some group'}); var user; $http.get('http://api/user/10').then(function(response){ user = response.data; return $http.get('http://api/group/' + user.group_id); }).then(function(response){ user.group = response.data; }); 

This will avoid the pyramid code, make the code more linear, and therefore more readable and easier to maintain.

Parallel execution of requests with the expectation of all


$q.all(...) accepts an array or dictionary of promises, combines them into one that will be resolved when all promises are resolved, or rejected with an error, when at least one promise is rejected. The values ​​will come in success-callback either as an array or as a dictionary, depending on how the all method was called.

  $httpBackend.expectGET('http://api/obj1').respond(200, {type: 'obj1'}) var obj1, obj2; var request1 = $http.get('http://api/obj1'); var request2 = $timeout(function(){ return {type: 'obj2'}; }); $q.all([request1, request2]).then(function(values){ obj1 = values[0].data; obj2 = values[1]; }); expect(obj1).toBeUndefined(); expect(obj2).toBeUndefined(); $httpBackend.flush(); expect(obj1).toBeUndefined(); expect(obj2).toBeUndefined(); $timeout.flush(); expect(obj1).toEqual({type: 'obj1'}); expect(obj2).toEqual({type: 'obj2'}); 


  $q.all({ obj1: $http.get('http://api/obj1'), obj2: $timeout(function(){ return {type: 'obj2'}; }) }).then(function(values){ obj1 = values.obj1.data; obj2 = values.obj2; }); 


$ q.when


Wrap any object in a promise object. Most commonly needed for mocks in unit tests. Or when there is a chain of requests, one of which needs to be executed not always, but only in some cases, and in other cases there is a ready-made object.

  spyOn(UserApi, 'get').andReturn($q.when({id: 1, name: 'Artem'})); var res; UserApi.get(1).then(function(user){ res = user; }); $rootScope.$digest(); expect(res).toEqual({id: 1, name: 'Artem'}); 


Note that to resolve promises, at least one $digest loop must be executed.

Creating your own deferred objects


$q -service also allows you to wrap any asynchronous operation in your deferred-object with the corresponding promise-object.
  var postFile = function(name, file) { var deferred = $q.defer(); var form = new FormData(); form.append('file', file); var xhr = new XMLHttpRequest(); xhr.open('POST', apiUrl + name, true); xhr.onload = function(e) { if (e.target.status == 200) { deferred.resolve(); } else { deferred.reject(e.target.status); } if (!$rootScope.$$phase) $rootScope.$apply(); }; xhr.send(form); return deferred.promise; }; 


Key points here:


AngularJS Features


First, the $ q-service is implemented taking into account dirty-checking in AngularJS, which gives a faster resolve and reject, removing unnecessary redrawing by the browser.
Second, when interpolating and calculating angular expressions, promise objects are interpreted as the value obtained after resolve.
  $rootScope.resPromise = $timeout(function(){ return 10; }); var res = $rootScope.$eval('resPromise + 2'); expect(res).toBe(2); $timeout.flush(); res = $rootScope.$eval('resPromise + 2'); expect(res).toBe(12); 


This means that if you follow the change of a variable through a string watch , then exactly the resolved value will come as a value.
  var res; $rootScope.resPromise = $timeout(function(){ return 10; }); $rootScope.$watch('resPromise', function(newVal){ res = newVal; }); expect(res).toBeUndefined(); $timeout.flush(); expect(res).toBe(10); 

An exception is the use of the function. The value returned from it will be used as is, i.e. it will be a promise object.
  $rootScope.resPromise = function(){ return $timeout(function(){ return 10; }); }; var res = $rootScope.$eval('resPromise()'); expect(typeof res.then).toBe('function'); 


Understanding this is important when writing various loading widgets, loading directives for buttons, etc.

AngularJS 1.2.0 - what's new in promises




And finally, a screenshot of completed tests from WebStorm 7 EAP. Yet nice added integration with karma.

Source: https://habr.com/ru/post/189084/


All Articles