1

I'm trying to use a service call to update a results array that then gets used in an ng-repeat. On a form submit I call the necessary service and hookup my callbacks via .then() on the promise object. Unfortunately the view only gets updated once I start deleting the characters from the text input. It then displays the correct results.

Here is my view:

<div id="main" ng-controller="SearchController as searchCtrl">
        <div class="header" >
            <h1>Search and Add Tracks</h1>
        </div>
        <!--Search Bar-->
        <form class="pure-form pure-g" novalidate ng-submit="searchCtrl.search()">
            <div class="pure-u-1">
                <input class="pure-input-1" type="search" placeholder="Search for tracks" ng-model="searchCtrl.query">
            </div> 
        </form>
        <!--Search Results Table-->
        <div class="pure-u-1" >
            {{searchCtrl.results.length}}
            <div ng-repeat="track in searchCtrl.results" ng-include src="'templates/single-track-view.html'" >
            </div>
        </div>
    </div>

And my controller code:

app.controller('SearchController',function(){
    var searchCtrl = this;
    searchCtrl.results = [];
    searchCtrl.query = '';

    this.search = function(query){
        console.log(searchCtrl.query);
        var processTracks = function(results){
            console.log(results);
            searchCtrl.results = results[0].tracks;
            searchCtrl.results.push(results[1].tracks);
            searchCtrl.query = '';
            return results;
        }
        //search takes a DICTIONARY not a pure string
        mopidy.library.search({"any": searchCtrl.query}).then(processTracks,console.error.bind(console));
    }

});

When using the AngularJS inspector I can definitely see searchCtrl.results being updated within the scope with the correct results. The view simply will not update until I start removing characters.

EDIT: The result back from the promise is actually an array of responses. I'm calling an api from Mopidy (a music player), the array is the different responses from different music providers.

mrkaiser
  • 149
  • 7

3 Answers3

1

Calling $scope.$apply is risky but the digest loop is indeed the problem.

The correct way to proceed would instead be to assimilate the call into Angular by calling $q.when on it:

app.controller('SearchController',function($q){ // note the $q here for promises
    var searchCtrl = this;
    searchCtrl.results = [];
    searchCtrl.query = '';

    this.search = function(query){
        console.log(searchCtrl.query);
        var processTracks = function(results){
            console.log(results);
            searchCtrl.results = results[0].tracks;
            searchCtrl.results.push(results[1].tracks);
            searchCtrl.query = '';
            return results;
        }
        //$q.when assimilates a third party promise into Angular
        $q.when(mopidy.library.search({"any": searchCtrl.query}))
        .then(processTracks,console.error.bind(console));
    }

});

But why does it work? What's $q?

If we take a closer look at the code we notice .then.

That .then is how a promise - an abstraction over concurrency is signalling the value of the API call is now ready.

Promise libraries, at least decent ones run on a specification called "Promises/A+" which tells them how to interact with eachother - so Angular's promise library - $q - can consume the mopidy.library promise seamlessly.

Angular's $q promises are hooked directly into the digest loop, so casting that third party promise into an Angular one makes it run in sync with the digest loop, rather forcing a second digest yourself.

Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Oh it works! Why does it work though? Why is this better than calling .then on the original call? What does $q by us here? – mrkaiser Jun 29 '14 at 20:24
  • Good questions. `then` is by a promise library signalling the value is ready. Promise libraries, at least decent ones run on a specification called ["Promises/A+"](http://promisesaplus.com/) which tells them how to interact with eachother - so Angular's promise library - `$q` - can consume the `mopidy.library` promise seamlessly. Angular's $q promises are hooked directly into the digest loop (see http://stackoverflow.com/questions/23363014/), so casting that third party promise into an Angular one makes it run in sync with the digest loop, rather forcing a second digest yourself. – Benjamin Gruenbaum Jun 29 '14 at 20:27
  • Oh makes sense. So is it always good practice to wrap promises in an Angular Promise? – mrkaiser Jun 29 '14 at 20:46
  • Yes. If it repeats a lot - then you can make it a method, it's also safe to assimilate them inside your own `.then` calls (for example, returning that promise directly from a `then` chained to an `$http` call and then putting a `.then` on that would have worked. Personally, I avoid $q and use Bluebird as it's a much faster promise implementation and it's easier to debug - http://stackoverflow.com/q/23984471/1348195 – Benjamin Gruenbaum Jun 29 '14 at 20:50
0

unless your track property is an array this makes no sense

      searchCtrl.results = results[0].tracks;
      searchCtrl.results.push(results[1].tracks);

and if it is an array you are breaking the link between the ng-repeat and the observed variable by redefining it.and if mopidy is an external library you weill need to $pply your changes after updating the results variable

Dayan Moreno Leon
  • 5,357
  • 2
  • 22
  • 24
0

The solution here, as other have pointed out, is to make a call to $scope.$apply();

The reason: When you make an XHR it is outside of angular's regular digest cycle, i.e. when it updates all the listeners for two-way data binding. Calling $apply, forces a digest so that all listening models will be updated.

Per angular docs on $apply

lucky7id
  • 385
  • 1
  • 9