Search code examples
javascriptangularjsangular-promiseangular-factory

How do I return a collection from $q.defer().promise in Angular?


Angular v.1.6.1

I am attempting to return a collection using $q.defer().promise, but I am only getting a single record instead of both records that are returned from my service.

The Model:

angular.module('app').factory('User',
function() {

    /**
     * Constructor, with class name
     */
    function User(id, firstName, lastName, startDate) {
        // Public properties, assigned to the instance ('this')
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.startDate = startDate;
    }


    /**
     * Public method, assigned to prototype
     */
    User.prototype.getFullName = function() {
        return this.firstName + ' ' + this.lastName;
    };

    User.apiResponseTransformer = function (responseData) {
        if (angular.isArray(responseData)) {
            return responseData
                .map(User.build)
                .filter(Boolean);
        }
        return User.build(responseData);
    }

    /**
     * Static method, assigned to class
     * Instance ('this') is not available in static context
     */
    User.build = function(data) {
        return new User(
            data.Id,
            data.FirstName,
            data.LastName,
            data.StartDate
        );
    };


    /**
     * Return the constructor function
     */
    return User;
});

The Service

(function () {
'use strict';

var serviceId = 'userService';
angular.module('app').factory(serviceId, ['common', '$http', 'config', 'User', userService]);

function userService(common, $http, config, User) {
    var $q = common.$q;
    var defer = $q.defer();

    var service = {
        getUsers: getUsers,
        getUser: getUser
    };

    return service;

    function getUser(id) {
        $http({
            method: 'get',
            url: config.remoteServiceName + 'users/' + id
        }).then(function (response) {
            defer.resolve(User.apiResponseTransformer(response.data));
        }).then(function (response) {
            defer.reject(response);
        });

        return defer.promise;
    }

    function getUsers(startDate) {
        $http({
            method: 'get',
            url: config.remoteServiceName +'users/',
            params: {
                startDate: startDate
            }
        }).then(function (response) {
            var users = [];
            angular.forEach(response.data, function (value, key) {             
                users.push(User.apiResponseTransformer(value));
            });
            defer.resolve(users);
        }).then(function(response) {
            defer.reject(response);
        });

        return defer.promise;
    }  
}
})();

The View Methods

function getUser() {
        userService.getUser(1).then(function successCallback(data) {
            vm.user = data;
        }).catch(function () {
            log('An error occured trying to get user...');
        });
    }

    function getUsers() {
        userService.getUsers(new Date().toUTCString()).then(function successCallback(data) {
            vm.users = data;
        }).catch(function () {
            log('An error occured trying to get user...');
        });
    }

Inside the view, the getUser call functions as expteced but getUsers receives only the first item in the collection from the service. I have verified that response.data does contain the entire collection of objects and those objects are pushed into the users array.

Even calling defer.resolve(response.data) only sends the first item in the collection.

Any assistance is appreciated!


Solution

  • There is no need to manufacture a promise with $q.defer from a promise-based API. (If the object has a .then method, it is a promise.)

    //ERRONEOUS
    function getUser(id) {
          $http({
             method: 'get',
             url: config.remoteServiceName + 'users/' + id
          }).then(function (response) {
             defer.resolve(User.apiResponseTransformer(response.data));
          }).then(function (response) {
             defer.reject(response);
          });
    
          return defer.promise;
    }
    

    Since the invoking $http(config) can be chained with a .then method, it is a returning a promise. Any API call that can be chained with a .then method is a promise-based API. By paying attention to this detail, one can determine the nature of an unknown API.

    //CORRECT
    function getUser(id) {
        var promise = $http({
            method: 'get',
            url: config.remoteServiceName + 'users/' + id
        })
    
        var nextPromise = promise.then(function (response) {
            var value = User.apiResponseTransformer(response.data);
            //RETURN value to chain
            return value;
        }).catch(function (errorResponse) {
            //THROW to chain rejection
            throw errorResponse;
        });
    
        return nextPromise;
    };
    

    The chain has been broken into its part for clarity. The $http service call returns a promise. The .then method call returns a new promise that resolves to the value returned to its response handler. The new promise is then returned to the enclosing function.

    It is important that there be a return (or throw) statement at every level of nesting.

    Usage

    getUser(id).then(function(transformedValue) {
        console.log(transformedValue);
    });
    

    Because calling the .then method of a promise returns a new derived promise, it is easily possible to create a chain of promises.

    It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs.

    --AngularJS $q Service API Reference - Chaining Promises