Update $scope when $resource promise resolves

3.6k views Asked by At

I've been trying to wrap my head around how to use angular $scope correctly. My app updates a view based on url links in json data it received from the server when loading the main page.

The controller fetches the next data file when clicking a button but I am unable to inject this data into my scope variable and update the view. The network transmits the data successfully so my service is working.

I don't understand how to make $scope resolve the $resource promise and update the view when i retrieve data, after the app has initialised. Greatly appreciate any help on this matter.

My ListController :

//View updates correctly when promise resolves
$scope.data = DataElements.get({}); 

$scope.nextPage = function (){ //Called when clicking 'next' button
var url = $scope.data.pager.nextPage; 

var pageNumber = url.split("page=").pop(); //Ugly but it is temporary, I promise..

//View doesnt update
DataElements.get({'page': pageNumber}, function(data){ 
    $scope.data = data; 
});

//Error: Digest already in progress
DataElements.get({'page': pageNumber }, function(data){ 
    $scope.$apply(function(){
      $scope.data = data; 
    });
});

//View doesnt update
$scope.data = DataElements.get({'page': pageNumber)};  

};

Service :

services.factory('DataElements', ['$rootScope','$resource', function($rootScope, $resource){
    return $resource($rootScope.baseUrl+'api/:endPointAdr.jsonp', 
        {'endPointAdr': 'dataElements', 'page': '@page'}, 
        { get : {'method' : 'JSONP', 'params' : {'callback' : 'JSON_CALLBACK'}}     
    });
}]);

View :

<section>
  <div>
    <h1>Search</h1>
    <input type="text" placeholder="Type something" autofocus>
    <input type="button" name="filter" value="Filter">
    <input type="button" name="clear" value="Clear">
    <div class="btn-toolbar" role="toolbar" aria-label="...">
      <a class="btn btn-default" href="#" aria-label="Previous Page">
        <span class="glyphicon glyphicon-chevron-left"></span>
      </a>
      <a class="btn btn-default" href="#" ng-click="nextPage()" aria-label="Next Page">
        <span class="glyphicon glyphicon-chevron-right"></span>
      </a>
    </div>
  </div>
  <div ng-controller="ListController">
    <ul class="" ng-show="data">
      <li class="" ng-repeat="element in data.dataElements ">
        <a href="#">
          <div class="info">
            <h2>{{element.name}}</h2>
          </div>
        </a>
      </li>
    </ul>
  </div>
</section>

Server response:

DataElements.get({}) == >
angular.callbacks._1({"pager": {"page":1,"pageCount":15,"total":706,"nextPage":"http// ...});(*)

DataElements.get({'page': 2})  == > 
angular.callbacks._2({"pager": {"page":2,"pageCount":15,"total":706,"nextPage":"http:// ...});(*)

(*) $resource strips the jsonp and only returns json.

Update :

If I only assign $scope.data in the nextPage function scope it works perfectly. I know the ng-clickdirective wraps this function in $scope.apply(). It then makes sense that explicitly calling apply causes an error.

Solution :

<div ng-controller="ListController"> created a duplicate controller with different $scope. Removing it fixed the problem. See my answer for details.

3

There are 3 answers

1
Natalie On BEST ANSWER

I finally solved the problem.

It boiled down to this <div ng-controller="ListController">directive. It created a second ListController and attached it to the $rootScope. There was nothing wrong with the binding, but nextPage() was called from a different scope.

During the init phase of the dynamic ListController, both controllers would execute DataElements.get.., this is why it appeared to work, but calling the ng-click : nextPage() did not run in the context of the controller assigned to the list.htmlview.

2
Sirar Salih On

As you mentioned, your data on $scope is getting updated but the change is not propagated to the view. Something is breaking the two-way binding.

See what kind of error you get in the console (F12 in Chrome).

Remove this code block:

//Error: Digest already in progress
DataElements.get({'page': pageNumber }, function(data){ 
    $scope.$apply(function(){
      $scope.data = data; 
    });
});

//View doesnt update
$scope.data = DataElements.get({'page': pageNumber)};  

Avoid using $apply explicitly, it's rarely a good idea.

Edit:

Although the OP solved his problem by removing a binding to ListController to avoid $scope confusion, the HTML code looks a bit strange as it is now. The optimal solution is to create a directive that binds to ListController, and then reuse this directive in the view. That way the OP will avoid such $scope issues in the future.

3
Wawy On

If DataElements.get returns a promise object you need to call the then method:

DataElements.get({'page': pageNumber}).then(function(data) {
   $scope.data = data;
})

or if it's using $resource usually returns an object with a $promise property on it:

DataElements.get({'page': pageNumber}).$promise.then(function(data) {
   $scope.data = data;
})