changing a controller scope variable in a directive is not reflected in controller function

16,569

Solution 1

The reason why it does not show the updated value immediately is because the 2 way binding updates the parent (or the consumer scope of the directive) scope's bound value only during the digest cycle. Digest cycle happens after the ng-click is triggered. And hence $scope.page in the controller is not yet updated. You can get around this in many ways by using a timeout which will defer the action to run at the end of the digest cycle. You could also do it by setting an object which holds the value as 2-way bound property. Since 2-way bound property and parent scope share the same object reference you will see the change immediately.

Method 1 - using a timeout:

  scope.incrementPage = function() {
     scope.page += 1;
     $timeout(scope.alertPage)
  }  

Method 2 - Bind an object:

 //In your controller
 $scope.page2 = {value:1};

//In your directive 
scope.incrementPage = function() {
     scope.page.value += 1;
     scope.alertPage();
 }  

Method3 - Pass the value using function binding with argument:

//In your controller
$scope.alertPage = function(val) {
  alert(val);
}

and

<!--In the view-->
<div incrementer page="page" alert-page="alertPage(page)"></div>

and

//In the directive
scope.incrementPage = function() {
     scope.page += 1;
     scope.alertPage({page:scope.page});
 }  

app = angular.module('app', []);

app.controller('myCtrl', function($scope) {

  $scope.page = 1;
  $scope.page2 = {value:1};
  
  $scope.alertPage = function() {
    alert($scope.page);
  }
  
  $scope.alertPage2 = function() {
    alert($scope.page2.value);
  }

})

app.directive('incrementer', function($timeout) {
  return {
   
    scope: {
      page: '=',
      alertPage: '&',
      page2:"=",
      alertPage2: '&'
    },
    template: '<button ng-click="incrementPage()">increment page</button>',

    link: function(scope, elem, attrs) {
      scope.incrementPage = function() {
          scope.page += 1;
          scope.page2.value += 1;
          $timeout(function(){ scope.alertPage() });
          scope.alertPage2();
      }
    }
  }
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.5/angular.min.js"></script>
<div ng-app="app" ng-controller="myCtrl">
  <div incrementer page="page" alert-page="alertPage()" page2="page2" alert-page2="alertPage2()"></div>
  </div>

Solution 2

You can pass the variable by reference, then the update will be immediate (because you wont copy it, but simply pass its location in memory).

View:

<incrementer page="data" alert-page='alertPage()'></incrementer>

Directive:

link: function(scope, elem, attrs) {
  scope.incrementPage = function() {
      scope.page.page += 1;
      scope.alertPage();
  }

Solution 3

The problem is that the timeline is off.

  1. button clicked, incrementPage()
  2. directive scope value incremented (now 2)
  3. alertPage() parent scope value read (still 1)
  4. parent scope updated as part of digest (now 2)

To get around this, you need to either call the alert function after the digest cycle (e.g. $timeout) or you need to watch for changes in the parent scope.

// in controller
$scope.$watch('page', function (currentValue, previousValue) {
  // initially triggered with same value
  if (currentValue > previousValue) {
    alert(currentValue)
  }
})

Then change the value naturally.

// in directive html
<button ng-click="page = page + 1">
Share:
16,569

Related videos on Youtube

sq1020
Author by

sq1020

Updated on September 16, 2022

Comments

  • sq1020
    sq1020 over 1 year

    In my directive, I have a controller variable, page which gets incremented when you press the button in the directive. However, the next line, scope.alertPage() which calls the controller function does not reflect this change. Notice, when you click the button page is still alerted as 1!

    I know I can fix this by adding $scope.$apply in the controller but then I get the error that says a digest is already taking place.

    Plunker

    app = angular.module('app', []);
    
    app.controller('myCtrl', function($scope) {
    
      $scope.page = 1;
    
      $scope.alertPage = function() {
        alert($scope.page);
      }
    
    })
    
    app.directive('incrementer', function() {
      return {
        scope: {
          page: '=',
          alertPage: '&'
        },
        template: '<button ng-click="incrementPage()">increment page</button>',
    
        link: function(scope, elem, attrs) {
          scope.incrementPage = function() {
              scope.page += 1;
              scope.alertPage();
          }
        }
      }
    })
    

    html template:

      <body ng-app='app' ng-controller='myCtrl'>
        page is {{page}}
    
        <incrementer page='page' alert-page='alertPage()'></incrementer>
      </body>
    
    • PSL
      PSL over 9 years
      Place it in a $timeout or bind 2 way to an object with the property. Since it is 2 way binding, the original 2 way bound value will be updated only during the next digest cycle. i.e try $timeout(scope.alertPage) or another way you could try in your controller set $scope.page = {value : 1} and in the directive do $scope.page += 1; and rest whatever you are doing
  • PSL
    PSL over 9 years
    Really create a watch for this? The purpose of function binding is completely lost here IMHO. I am sure example pasted to increment a number is just a mere example, original code must be more complex than that.
  • PSL
    PSL over 9 years
    Yeah similar to method 2 in my example.. Thinking alike.. Cheers.. :)
  • AJcodez
    AJcodez over 9 years
    @PSL i think $watch is an elegant solution. Using $timeout triggers a whole-new digest cycle afaik.
  • PSL
    PSL over 9 years
    Not really in this case there will be a digest cycle for ngclick timeout will add it's own to the asyc queue which will run along with the same digest cycle.. also you shouldn't just be creating watches.. it is really unnecessary in this case and will add up to the digest cycle.