16

I am having trouble updating my scope on the front-end while making a request to an API. On the backend I can see that the value of my $scope variable is changing but this is not being reflected in the views.

Here is my controller.

Controllers.controller('searchCtrl', 
 function($scope, $http, $timeout) {
   $scope.$watch('search', function() {
      fetch();
   });

 $scope.search = "Sherlock Holmes";

 function fetch(){
   var query = "http://api.com/v2/search?q=" + $scope.search + "&key=[API KEY]&format=json";
    $timeout(function(){
      $http.get(query)
      .then(function(response){ 
        $scope.beers = response.data; 
        console.log($scope.beers);
      });
    });  
 }
});

Here is a snippet of my html

<div ng-if="!beers">
  Loading results...
</div>
<p>Beers: {{beers}}</p>
<div ng-if="beers.status==='success'">

  <div class='row'>
    <div class='col-xs-8 .col-lg-8' ng-repeat="beer in beers.data track by $index" ng-if="beer.style">
    <h2>{{beer.name}}</h2>          
    <p>{{beer.style.description}}</p>
    <hr>
    </div>
  </div>
</div>

<div ng-if="beers.status==='failure'">
  <p>No results found.</p>
</div>

I've tried several solutions including using $scope.$apply(); but this just creates the common error

Error: $digest already in progress

The following post suggested to use $timeout or $asyncDefault AngularJS : Prevent error $digest already in progress when calling $scope.$apply()

The code I have above uses $timeout and I have no errors but still the view is not updating.

Help appreciated

Community
  • 1
  • 1
peterpod
  • 246
  • 1
  • 3
  • 11
  • 2
    You shouldn't need the `timeout` - `$http` will trigger a `$digest` cycle - are you sure the data is coming back? – tymeJV Jan 20 '16 at 18:41
  • add an error handler to the request. Also check dev tools network to make sure request is succeeding and returning what is expected. Sounds like ajax problem – charlietfl Jan 20 '16 at 18:41
  • Agreed that no timeout is needed for this code, and that $http should trigger a digest cycle. Does your console.log inside the $http response show $scope.beers updated? – Daniel Nalbach Jan 20 '16 at 18:44
  • Why a `timeout`? When the answer from the server is ready, `then` give you the data. I suggest to use a `service ` and call it from the controller, saving the result in `$scope.beers`. – robe007 Jan 20 '16 at 18:56
  • To clarify the reason I was using $timeout is because other posts suggested to use $scope.$apply(). The caused the digest error and timeout is one way to sidestep this. – peterpod Jan 20 '16 at 19:19
  • 2
    @peterpod - then that indicates that somehow the digest cycle is not being properly triggered, even though an apply shows one in progress. I'm curious to see jusopi's proposal makes a difference. As a side note, best practice is to make API calls from services rather than controllers. I'd put the fetch method in a service, and have the controller call the service method when the expected event occurs. Setting a watch for a variable you are going to staticly declare is not right though. Either call fetch directly, at controller instantiation, or wire it up to UI like a button click. – Daniel Nalbach Jan 20 '16 at 19:26
  • are you sure that your repeat is right? you have `ng-repeat="beer in beers.data`, and in your controller you have `$scope.beers = response.data;`. That would mean that your ng-repeat is iterating over the equivalent of `response.data.data;`. Is that really what your response looks like, or are you doubling your `data`s? – Andrew Cavanagh Jan 20 '16 at 20:06
  • @AndrewCavanagh thanks for looking closely at the code but believe it or not the response is structured that way. – peterpod Jan 20 '16 at 20:11

3 Answers3

13

I you are using AngularJS 1.3+, you can try $scope.$applyAsync() right after $scope.beers = response.data; statement.

This is what Angular documentation says about $applyAsync()

Schedule the invocation of $apply to occur at a later time. The actual time difference varies across browsers, but is typically around ~10 milliseconds. Source

Update

As others have pointed out, you should not (usually) need to trigger the digest cycle manually. Most of the times it just points to a bad design (or at least not an AngularJS-friendly design) of your application.

Currently in the OP the fetch method is triggered on $watch. If instead that method was to be triggered by ngChange, the digest cycle should be triggered automatically.

Here is an example what such a code might look like:

HTML

// please note the "controller as" syntax would be preferred, but that is out of the scope of this question/answer
<input ng-model="search" ng-change="fetchBeers()">

JavaScript

function SearchController($scope, $http) {

    $scope.search = "Sherlock Holmes";

    $scope.fetchBeers = function () {
        const query = `http://api.com/v2/search?q=${$scope.search}&key=[API KEY]&format=json`;
        $http.get(query).then(response => $scope.beers = response.data);
    };

}
Vladimir Zdenek
  • 2,270
  • 1
  • 18
  • 23
0

As the comments suggest, you shouldn't need to use $timeout to trigger a digest cycle. As long as the UX that elicits the change is within the confines of an angular construct (e.g. controller function, service, etc.) then it should manifest within the digest cycle.

Based on what I can infer from your post, you are probably using a search input to hit an API with results. I'd recommend changing the logic up such that you are triggering your search on an explicit event rather than the $watcher.

<input ng-model="search" ng-change="fetch()">

Remove the $watch logic and the $timeout wrapper.

function fetch(){
    var query = "http://api.com/v2/search?q=" + $scope.search + "&key=[API KEY]&format=json";
$http.get(query)
.then(function(response){ 
    $scope.beers = response.data; 
    console.log($scope.beers);

    //it's a good habit to return your data in the promise APIs
    return $scope.beers;  
});
}

The reasons I make this recommendation is:

  • You have finer control of how the ng-change callback is triggered using ng-model-options. This means you can put a delay on it, you can trigger for various UX events, etc.
  • You've maintained a clearer sequence of how fetch is called.
  • You have possibly avoided performance and $digest issues.
jusopi
  • 6,791
  • 2
  • 33
  • 44
  • Hey. Let me try this out! Perhaps I'm being too fancy with the watcher – peterpod Jan 20 '16 at 19:18
  • I hope it works out. Generally I use `$watch` for things that aren't directly triggered by some UX (in other words, not triggered by an existing directive or other ng construct) and in directives where I need to tap into DOM events where I don't want to wire up an isolate scope, etc. – jusopi Jan 20 '16 at 19:26
  • I tried you suggestion with ng-change but decided I will just be simple here and do ng-submit. Regardless even with this method the view is not updating. My consolelog prints out a new json response on each form submission but the scope variable doesn't update. I updated my post above to show my html – peterpod Jan 20 '16 at 19:51
  • you need to inspect the response from your service call. I'm guessing you're getting everything coming back as normal, you're just inspecting the wrong data? – jusopi Jan 20 '16 at 20:01
  • Do you mean I may be parsing it incorrectly? The thing is disregarding whether the status is successful I put

    Beers: {{beers}}

    to check if the variable is even changing. However, its just undefined the entire time as a result the top div displays with "Loading results"
    – peterpod Jan 20 '16 at 20:09
  • I'd recommend making a plunkr/jsfiddle so we can see it in context. Sorry we're not much help thus far. – jusopi Jan 20 '16 at 20:13
0

Hey guys I solved the issue but I'm not sure exactly why this changed anything. Rearranging my code on JS Fiddle I just put all my partials into the index.html file like so and the requests and scope variables updated smoothly. Is was there perhaps a controller conflict with my html above?

<body ng-app="beerify" ng-controller='searchCtrl'>

<nav class="navbar navbar-inverse navbar-fixed-top">
  <div class="container"><!-- nav bar code -->
  </div>
</nav>

<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
  <div class="container">
    <h1>Title</h1>

    <form ng-submit="fetch()">
      <div class="input-group">
          <input type="text" ng-model="search" 
                 class="form-control" placeholder="Search the name of a beer" name="srch-term" id="srch-term">
          <div class="input-group-btn">
              <button class="btn btn-default" type="submit"><i class="glyphicon glyphicon-search"></i></button>
          </div>
      </div>
    </form>

  </div>
</div>

<div class="container">

  <div ng-if="!beers">
    Loading results...
  </div>
  <div ng-if="beers.status==='success'">
   <div class='row'>
      <div class='col-xs-8 .col-lg-8' ng-repeat="beer in beers.data track by $index" ng-if="beer.style">
        <!-- ng-if will make sure there is some information being displayed
           for each beer -->
        <h2>{{beer.name}}</h2>
        <h3>{{beer.style.name}}</h3>
        <p>AbvMin: {{beer.abv}}</p>
        <p>AbvMax: {{beer.ibu}}</p>      
        <p>{{beer.style.description}}</p>
        <hr>
      </div>
    </div>
  </div>

  <div ng-if="beers.status==='failure'">
    <p>No results found.</p>
  </div>
</body>
peterpod
  • 246
  • 1
  • 3
  • 11