Search code examples
angularjsangularjs-scopeangular-promise

Angularjs - Cleanest way to add promises to scope, now that Angular deprecated auto unwrapping of promises


I really like the clean (and I think easy to follow) way that promises were autounwrapped:

$scope.myData = DataService.query({something:"etc"}); // done;

And I really don't care for what seems to be the standard way of doing it now, without the automatic unwrapping:

DataService.query({something:"etc"}).$promise.then(function (data){
    $scope.myData = data;
});

What I'd like to see is something like this:

$scope.pulseData = $scope.setPromise(CitsciAnalytics.pulse({
    projId:"yardmap"
}));

But I can't see how to make that happen. The closest I got was:

$scope.pulseData = $scope.setPromise("pulseData", CitsciAnalytics.pulse({
    projId:"yardmap"
}));

Using a function added to the root scope:

.run(["$rootScope", "$log", function ($rootScope, $log) {
    //parent method to avoid promise unwrapping boilerplate
    $rootScope.setPromise = function (scopeVar, promise) {
        if (arguments.length === 2 && promise && promise.$promise) {
            var scope = this;
            promise.$promise.then(function (data){
                scope[scopeVar] = data;
            });
        } else {
            $log.error("$rootScope.setPromise has invalid arguments");
        }
    };
}]);

but I don't like the unDRY requirement of having to pass the scope variable name as an additional string. Has anyone else tackled this, or see a way to do this more cleanly?


Solution

  • First of all you don't need to use

    DataService.query({something:"etc"}).$promise.then(function(data){
        $scope.myData = data;
    });
    

    which clearly refers to a $resource, because $resource will return an empty array or object and fill it with data as they arrive.

    So, with a $resource class, you can still use

    $scope.myData = DetaService.query(...);
    

    $resource's paradigm is also a good approach to follow in your own "data-fetching" services: Return and empty array and fill it with data as they arrive.

    E.g.:

    .factory('DataService', function ($http) {
        function query(params) {
            var data = [];
            $http.get('/some/url/with/params').then(function (response) {
                response.data.forEach(function (item) {
                    data.push(item);
                });
            });
            return data; 
        }
    
        return {
            query: query
        };
    });
    
    .controller('someCtrl', function ($scope, DataService) {
        $scope.data = DataService.query({...});
    

    If you have to use thrid-party services that return a promise, you can implement your generic function to offer similar functionality:

    .run(function ($rootScope) {
        $rootScope.promiseToArray = function (promise) {
            var arr = [];
            promise.then(function (data) {
                data.forEach(function (item) {
                    arr.push(item);
                });
            });
            return arr;
        };
    });
    
    .controller('someCtrl', function ($scope, ThirdPartyService) {
        $scope.data = $scope.promiseToArray(ThirdPartyService.fetchData());
    });
    

    See, also, this short demo.


    The above samples are just for illustration purposes and not production-ready code.
    I a real-world app, you would need to gracefully handle exceptions, rejections etc.