Angularjs $watchCollection on $rootScope not triggered when values updated from directive

1.4k views Asked by At

I'm running into a bit of an issue solving a problem with some Angularjs functionality I'm working on.

The basic idea is that I have a system where certain criteria must be met before the user is allowed to advance to the next section of the app. One example of this is that a user must both add a comment, and click a link (in the real app, this is a file download) in order for them to advance.

You can take a look at the full example here: https://jsfiddle.net/d81xxweu/10/

I'll assume that the HTML is pretty self explanatory and move on to what I'm doing with my Angular module. My app declaration and initialization are as follows:

var myApp = angular.module('myApp', ['ngRoute']);

myApp.run(function ($rootScope) {
    // Both of these must be met in order for the user to proceed with 'special-button'
    $rootScope.criteria = {
        criteria1: false,
        criteria2: false
    };
});

This is pretty simple. I'm attaching an object called criteria to the root scope of the application in order to make it accessible to my directives and controllers. I have a directive that renders the link which allows the user to advance once the criteria are met. In this example the text of the link changes from "Waiting..." to "Click to proceed" to indicate we may advance.

myApp.directive('specialButton', function ($rootScope) {
    return {
        scope: true,
        template: "<a href='#'>{{ linkText }}</a>",
        replace: true,
        link: function (scope, el, attrs) {
            scope.linkText = 'Waiting...';

            var setLinkState = function(currentCriteria) {
                var criteriaMet = true;

                for(var k in $rootScope.criteria) {
                    if($rootScope.criteria[k] == false) {
                        criteriaMet = false;
                    }
                }

                if(criteriaMet) {
                    scope.linkText = 'Click to proceed';
                }
            };

            // Watch for changes to this object at the root scope level
            $rootScope.$watchCollection('criteria', function(newValues) {
                setLinkState(newValues);
            });
        }
    };
});

So in order to trigger the watch statement we've set on this directive I can add a comment as allowed by this controller:

myApp.controller('comments', function ($scope, $rootScope) {
    $scope.commentText = '';
    $scope.comments = [];

    $scope.addComment = function () {
        $scope.comments.push({ commentText: $scope.commentText });
        $scope.commentText = ''

        // When the user adds a comment they have met the first criteria
        $rootScope.criteria.criteria1 = true;
    };
});

The previous is my controller for displaying/adding comments. I set criteria1 to true here to indicate the user has added a comment. This actually works fine, and the $watchCollection in the specialButton directive is called as expected.

The problem arises when I try to perform the same action from the link that must be clicked in order to advance. This is rendered with a directive as it is my understanding that in a case such as this a directive makes more sense than a controller, unlike the comment list/form.

myApp.directive('requiredLink', function($rootScope) {
    return {
        scope: true,
        template: "<a href='#'>Click me!</a>",
        replace: true,
        link: function(scope, el, attrs) {
            el.bind('click', function(evt) {
                evt.preventDefault();

                // When the user clicks this link they have met the second criteria
                $rootScope.criteria.criteria2 = true;
            });
        }
    };
});

As you can see here I pass in $rootScope just as in the controller. However when I set criteria2 to true the $watchCollection is not triggered.

So what ends up happening is if I add a comment first, then click the other button, I do not see specialButton update its text because the second change never triggers the watch. If, however, I click the link first, then add a comment, specialButton updates as expected. The click of requiredLink IS updating the data, but not triggering the watch. So when I then add a comment and the $watch is triggered it sees that BOTH have been set to true.

Thanks in advance for any help you can offer in resolving this issue; I appreciate your time.

1

There are 1 answers

1
Pankaj Parkar On BEST ANSWER

Your actual problem is you are update $rootScope from the event which is outside the angular context, so its obivious that angular binding will not update because digest cycle doesn't get fired in that case. You need to fire it manually by using $apply() method of $rootScope

el.bind('click', function(evt) {
    evt.preventDefault();
    // When the user clicks this link they have met the second criteria
    $rootScope.criteria.criteria2 = true;
    $rootScope.$apply(); //this will run digest cycle & will fire `watchCollection` `$watcher`
});

Demo Plunkr

Though this solution work but I'll suggest you to use service instead of using $rootScope

For implementation using service you need to follow below things that would help you.

Your service should be using criteria variable in object form, should follow the dot rule so that the respective reference will update using JavaScript prototypal

Sevice

app.service('dataService', function(){
    this.criteria = {
        criteria1: false,
        criteria2: false
    };
    //...here would be other sharable data.
})

Whenever you want to use it any where you need to inject it in function of controller, directive, filter wherever you want.

And while putting watch on service variable from directive you need to do something like below.

Directive

myApp.directive('specialButton', function (dataService) {
    return {
        scope: true,
        template: "<a href='#'>{{ linkText }}</a>",
        replace: true,
        link: function (scope, el, attrs) {
            //.. other code

            // deep watch will watch on whole object making last param true
            scope.$watch(function(){ 
                return dataService.criteria //this will get get evaluated on criteria change
            }, function(newValues) {
                setLinkState(newValues);
            }, true);
        }
    };
});