AngularJS ng-repeat with data from service

ansorensen picture ansorensen · Feb 19, 2013 · Viewed 19.4k times · Source

Originally in my app, I created controllers with very basic $http calls to get a resource by getting the ID of an object from the url ($routeParams). Ng-repeat display the results correctly.

However, I noticed refreshing in a later view (different controller) wiped out the data and broke the page. So, I created a function on the service to be used in multiple controllers, to check whether the data has is available and to react as follows:

1) If the resource is defined, return it (no API call) 2) If the resource is not defined, get the id from the url and get it from the API 3) If the resource is not defined & you can't get the ID, just return false.

However, this broke the code: the template rendered before the service returned the data, and ng-repeat did not update. The code looks like this:

angular.module('myApp', ['ngCookies'])
    .config(...)
    .service('myService', ['$cookies', '$http', function($cookies, $http) {
        myData = {};

        return {
            getData:function(dataID) {
                if(myData.name) {return myData);
                else if (dataID && dataID !== '') {
                    $http.get('/api/data/' + dataID)
                        .success(function(data) {
                            myData = data.object;
                            $cookies.dataID = data.object.id;
                            return myData;
                        }
                }
                else { return false; }
            }
        }
    }]);

function myCtrl($scope, $http, $routeParams, myService) {
    $scope.data = myService.getData($routeParams.dataID);

    ...
}

And here's the template. It's in jade, which means rather than angle brackets, you just list the element with parameters in parenthesis right after, and content after the parenthesis.

h2 My heading
ul
    li(ng-repeat='option in data')
        a(href="#", ng-click='someFuncInCtrl(option.name)')  {{ option.name }}

When the controller did the $http.get itself, the ng-repeat worked fine because the $scope was updated in the ".success" callback. Now that there's a service that returns the data after a slight delay, "$scope.data" is just undefined, the ng-repeat list is empty.

I used a console.log to check myData right before return "return myData", and the myData is working, it just isn't returned in time, and for whatever reason the list is not updating whenever $scope does get the data.

I looked a using $routeProvider's resolve... but that makes getting the ID from the url challenging, as the resolve object doesn't seem to have access to $routeParams. I know that $scope.$apply is supposed to help update the scope when it's altered by outside functions... but I have no clue where to put it. The most similar problem on SO didn't use a service.

I tried:

$scope.$apply($scope.data = myService.getData($routeParams.dataID));

And

$scope.$apply(function() {
    $scope.data = myService($routeParams.dataID);
});

Both times I only got Error: $digest already in progress.

Answer

bmleite picture bmleite · Feb 19, 2013

The problem is on the way you interact with the service. Since your getData function can return both synchronous and/or asynchronous information, you can't just use normal return(s).

$http.get('/api/data/' + dataID)
    .success(function(data) {
        myData = data.object;
        $cookies.dataID = data.object.id;
        return myData;
    });

The return on the above snippet will not return anything from getData because it will be executed on the context of the $http.get success callback (and not on the getData call stack).

The best approach for handling sync and async service requests is to use promises.

Your getData function should look something like this:

getData:function(dataID) {
    var deferred = $q.defer();
    if(myData.name) {
       deferred.resolve(myData);
    } else if (dataID && dataID !== '') {
        $http.get('/api/data/' + dataID)
            .success(function(data) {
                 myData = data.object;
                 $cookies.dataID = data.object.id;
                 deferred.resolve(myData);
                 // update angular's scopes
                 $rootScope.$$phase || $rootScope.$apply();
             });
    } else { 
       deferred.reject();
    }

    return deferred.promise;
}

Note: You need to inject the $rootScope on your service.

And on your controller:

function myCtrl($scope, $http, $routeParams, myService) {
    myService.getData($routeParams.dataID).then(function(data) {
        // request was successful
        $scope.data = data;        
    }, function() {
        // request failed (same as your 'return false')
        $scope.data = undefined;
    });
}