Search code examples
angularjsangularjs-scope

How to update a field binding using a $watch when using ControllerAs


Within my angular app I have set view using ui-router:

.state('search',
                {
                    url: '/',
                views: {
                    'navigation': {
                        templateUrl: '/app/views/navbartemplate.html',
                        controller: 'navigationController',
                        controllerAs: 'nav'
                    },
                    'content': {
                        templateUrl: '/app/views/search.html',
                        controller: 'searchController',
                        controllerAs: 'search'

                    }
                }

            })

I'm using claims from Identity Server 3 and passing user information to the client using the access token and identity claim. Since this is an angular app the claim is retrieved using a callback. It receives the access token then that token is passed back to get the identity which is an async callback. This is all managed using the OIDC token manager. Within the navigation controller I have tried to map the values of the user (first name last name) to a field in the navigation view from this callback.

    //get user information
    // no id token or expired => redirect to get one
    if (vm.mgr.expired) {
        vm.mgr.redirectForToken();
    } else {
        vm.mgr.oidcClient.loadUserProfile(vm.mgr.access_token)
            .then(function(userInfoValues) {
                dataService.setItem(userInfoValues);

            }).then(function() {
                vm.profile = dataService.getItem();
                checkAdmin(vm.profile.sys_admin);
                vm.message = "Welcome, " + vm.profile.given_name + " " + vm.profile.family_name;
                $log.info(vm.profile.sys_admin);
                $log.info(vm.message);

            });
    }

I can see through the logs that the properties are set, but since the DOM has already rendered the bound fields in the UI are never updated. I know this is a candidate to inject into the controller $scope and then apply a watch. However whenever I do this I get the angular error a digest is already in place. I have tried to nest the watch inside of the second .then promise as well as placing it outside of the functions. When nested inside it is ignored and when placed outside of the promise I get the error.

The watch was previously written as (note nav is the ControllerAs in the view):

$scope.$watch('nav.message', function(){
 checkAdmin(vm.profile.sys_admin);
                vm.message = "Welcome, " + vm.profile.given_name + " " + vm.profile.family_name;

});

Update: After some additional testing I moved the function out of the second promise to look more like this:

   var loadData = function() {

        checkAdmin(vm.profile.sys_admin);
        vm.message = "Welcome, " + vm.profile.given_name + " " + vm.profile.family_name;
        $log.info(vm.profile.sys_admin);
        $log.info(vm.message);
    }

    //get user information
    // no id token or expired => redirect to get one
    if (vm.mgr.expired) {
        vm.mgr.redirectForToken();
    } else {
        vm.mgr.oidcClient.loadUserProfile(vm.mgr.access_token)
            .then(function(userInfoValues) {
                dataService.setIqItem(userInfoValues);
                vm.profile = dataService.getIqItem();
            }).then(loadData);
    }   

I realize the functions did the same thing but what I found was clicking on other buttons in the screen to change the CSS theme (anything that caused a postback/re-render of the page) would update the bindings and I would see them appear in the values in the UI. This certainly seems as if it is related to the DOM rendering prior to the angular code completing and the scope is not updating. I'll try the Deep watch approach tomorrow to see if that corrects it.

I'm still curious as to How do I go about getting the message field to update once the Oidc manager returns the identity of the user? Watch Deepwatch or some other approach. I have seen SO posts where users use a timeout but that seems wrong or non reliable. Should this be a service instead of something in the view controller? Any ideas appreciated.

thanks in advance


Solution

  • Working through this problem I could see that the error was related to a digest cycle and the scope not being able to update the binding. Looking through similar posts I saw possible answers indicating the use of $timeout I was not a fan of this due to the fact this was an async call and it felt hokey to use timeout as it may not be 100% reliable. I then stumbled upon a blog post by Ben Nadal where he explained the use of $scope.$evalAsync I won't go into the full blog but in essence: "$scope.$evalAsync() expressions are placed in an "async queue" that is flushed at the start of each $digest iteration."

    Given that I updated my code to now look like:

     if (vm.mgr.expired) {
                vm.mgr.redirectForToken();
            } else {
                vm.mgr.oidcClient.loadUserProfile(vm.mgr.access_token)
                    .then(function(userInfoValues) {
                        $scope.$evalAsync(function() {
                            dataService.setItem(userInfoValues);
                            vm.profile = dataService.getItem();
                            checkAdmin(vm.profile.sys_admin);
                            vm.message = "Welcome, " + vm.profile.given_name + " " + vm.profile.family_name;
                        });
                    });
            }
    

    And now the evaluation is elevated so that the bindings in the UI are updated.