AngularJS : $scope.$watch is not updating value fetched from $resource on custom directive
Asked Answered
D

4

23

I'm having an issue with custom directives that's driving me crazy. I'm trying to create the following custom (attribute) directive:

angular.module('componentes', [])
    .directive("seatMap", function (){
        return {
            restrict: 'A',
            link: function(scope, element, attrs, controller){

                function updateSeatInfo(scope, element){
                    var txt = "";
                    for (var i in scope.seats)
                        txt = txt + scope.seats[i].id + " ";
                    $(element).text("seat ids: "+txt);
                }

                /* 
                    // This is working, but it's kind of dirty...
                    $timeout(function(){updateSeatInfo(scope,element);}, 1000);
                */

                scope.$watch('seats', function(newval, oldval){
                    console.log(newval, oldval);
                    updateSeatInfo(scope,element);
                });
            }
        }
    });

This "attribute-type" directive (called seatMap) is trying to show a list of seat ids (e.g, for a theatre) which I'll fetch from the server via $resource service (see code below) into a div (element).

I'm using it with this simple partial html:

<div>
    <!-- This is actually working -->
    <ul>
        <li ng-repeat="seat in seats">{{seat.id}}</li>
    </ul>

    <!-- This is not... -->
    <div style="border: 1px dotted black" seat-map></div>
</div>

And this is the controller which is loading the scope:

function SeatsCtrl($scope, Seats) {
    $scope.sessionId = "12345";
    $scope.zoneId = "A";
    $scope.seats = Seats.query({sessionId: $scope.sessionId, zoneId: $scope.zoneId});
    $scope.max_seats = 4;
}

Where "Seats" is a simple service using $resources to fetch a JSON from the server

angular.module('myApp.services', ['ngResource'])
    .factory('Seats', function($resource){
        return $resource('json/seats-:sessionId-:zoneId.json', {}, {});
    })
;

app.js (asientos_libres.html is the partial I've been using):

angular.module('myApp', ['myApp.filters', 'myApp.services', 'myApp.directives', 'componentes']).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.when('/view1', {templateUrl: 'partials/asientos_libres.html', controller: SeatsCtrl});
    $routeProvider.otherwise({redirectTo: '/view1'});
  }]);

The problem is, even though I set up a "scope.$watch" in the link function of the directive so that the scope can check whether "seats" attribute has changed to update the list of ids, it isn't working at the moment $scope.seats is changing in the controller (when we call "query").

As you might see in the code, I made a try using $timeout to delay the launch of "updateSeatInfo", but I'm afraid it's not the smartest solution by far...

I also tried to not to make a JSON request, but use a hard-coded dictionary in $scope.seats and it works, so it seems it's a matter of synchrony.

Note: The updateSeatInfo is just a test function, the actual function I'll use is a bit more complex.

Any idea about how to cope with it?

Thank you a lot beforehand!

Edit 1: Added app.js, where I'm using router to call SeatsCtrl, thanks to Supr for the advice. However, I'm still having the same issue.

Edit 2: Solved!(?) Ok! It seems I found a solution, which may not be the best, but it's working properly! :) As far as I could see here http://docs.angularjs.org/api/ng.$timeout, we can use $timeout (a wrapper over setTimeout) with no delay! This is great because we aren't artificially delaying the execution of our code inside $timeout, but we're making the directive not to run it until the asynchronous request has finished.

Hope it will work for long-wait requests, too...

If someone knows a better way to fix it, please tell!

Daubery answered 21/6, 2012 at 10:4 Comment(0)
G
56

The issue is that watch compares the reference instead of the object by default. Add ,true to the end to have it compare the value instead.

scope.$watch('seats', function(newval, oldval){
                console.log(newval, oldval);
                updateSeatInfo(scope,element);
            }, true);
Gluteus answered 19/10, 2012 at 0:25 Comment(1)
A word of caution: deep comparisons like this have a performance cost. It's mostly a problem when deep watching a collection of objects rather than a single object, but it's worth to keep in mind.Haberdasher
O
5

I had this problem too. It was due to the fact that my variable was at first not defined 'undefined' in the scope. Watch seems to not work on undefined variables. Seems obvious after-all.

I was first trying to use watch to trigger when my variable would be effectively set by the controller. Example:

myApp.controller('Tree', function($scope, Tree) {

Tree.get({},
function(data) { // SUCCESS
    console.log("call api Tree.get succeed");
    $scope.treeData = data;
},

function(data) { // FAILURE
    console.log("call api Tree.get failed");
    $scope.treeData = {};
});
});

I solved it by initializing my variable with an empty object before calling the service:

myApp.controller('Tree', function($scope, Tree) {

$scope.treeData = {};      // HERE

Tree.get({},
function(data) { // SUCCESS
    console.log("call api Tree.get succeed");
    $scope.treeData = data;
},

function(data) { // FAILURE
    console.log("call api Tree.get failed");
    $scope.treeData = {};
});
});

In that case watch was able to detect the change in the variable.

Obeah answered 29/12, 2012 at 13:47 Comment(0)
V
0

I can't see that you're using the SeatsCtrl-controller anywhere? How is it being used? And have you verified that it is activated, and that the query is actually performed?

The quickest way to check if SeatsCtrl is in use is to simply add a console.log('SeatsCtrl actived!'); inside it. If it is not, then add ng-controller="SeatsCtrl" to the div.

You can also put a watch-and-log on the seats directly inside the controller just to make sure it is not an issue with scoping.

Vaccaro answered 21/6, 2012 at 11:19 Comment(3)
Well, I actually use SeatsCtrl via router in app.js, sorry to not to tell it before. I've edited the post to make it clearer.Daubery
I see. None the less, did you verify that the controller runs, and that the query is returning seats as expected? Like I said, adding a watch inside SeatsCtrl will at least reveal whether or not the query runs as it should, in which case it is probably a problem with scoping.Vaccaro
Checked. The query is running but only if I use a timeout. It's the same issue as it's happening into the directive. It seems it's completely a matter of synchrony for getting data via Ajax (via $response)... The scope is all right, by the way.Daubery
R
0

I have this problem too. I think it's a problem in Angular. There is a ticket on GitHub to "beef up $resource futures" which would probably address this issue, as what you really need is access to the promise object for the resource you have.

Or, the watchers could wait to fire until the promise is resolved.

In the meantime, a slightly more elegant way to fix this which doesn't require a timeout is to reassign the scope property that is being watched from the success callback on the $resource. This will cause the watcher to fire again at the appropriate time.

A timeout with no delay is just putting the evaluation of the deferred function on the end of the current stack - in your case, your resource happened to be resolved by then, but this could be a problem if the server is having a case of the Mondays.

Example:

$scope.myAttr = MyResource.fetch(
  function (resource) { $scope.myAttr = new MyResource(angular.copy(resource)); }
);
Roxyroy answered 31/8, 2012 at 17:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.