3

As far as I can tell, ng-change is called before ng-model is actually changed in a select element. Here's some code to reproduce the issue:

angular.module('demo', [])
  .controller('DemoController', function($scope) {
    'use strict';

    $scope.value = 'a';
    $scope.displayValue = $scope.value;

    $scope.onValueChange = function() {
      $scope.displayValue = $scope.value;
    };
  })
  .directive("selector", [
    function() {
      return {
        controller: function() {
          "use strict";
          this.availableValues = ['a', 'b', 'c'];
        },
        controllerAs: 'ctrl',
        scope: {
          ngModel: '=',
          ngChange: '='
        },
        template: '<select ng-model="ngModel" ng-change="ngChange()" ng-options="v for v in ctrl.availableValues"> </select>'
      };
    }
  ]);
<html lang="en">

<head>
  <meta charset="utf-8">
</head>

<body>
  <div ng-app="demo" ng-controller="DemoController">
    <div selector ng-model="value" ng-change="onValueChange">
    </div>
    <div>
      <span>Value when ng-change called:</span>
      <span>{{ displayValue }}</span>
    </div>
  </div>

  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
  <script src="demo.js"></script>
</body>

</html>

If you run this, you should change the combobox 2 times (e.g. 'b' (must be different than the default), then 'c').

  • The first time, nothing happens (but the value displayed in the text should have changed to match the selection).
  • The second time, the value should change to the previous selection (but should have been set to the current selection).

This sounds really similar to a couple previous posts: AngularJS scope updated after ng-change and AngularJS - Why is ng-change called before the model is updated?. Unfortunately, I can't reproduce the first issue, and the second was solved with a different scope binding, which I was already using.

Am I missing something? Is there a workaround?

Community
  • 1
  • 1
Tony S Yu
  • 3,003
  • 30
  • 40
  • try ng-change="ngChange" instead of ng-change="ngChange()"? I think the ngChange() is making a call. – NPToita Dec 02 '15 at 07:20
  • why would you want to wrap a ` – Callum Linington Dec 02 '15 at 08:08
  • I suspect that It can be somehow related to this http://stackoverflow.com/a/31013435/2435473 – Pankaj Parkar Dec 02 '15 at 20:10
  • @CallumLinington: This is just a simplified example to highlight the issue. A real example might be a directive with a ` – Tony S Yu Dec 03 '15 at 04:12
  • @tsyu80 if it gets any bigger then i would try and bind controller values to it, i would use the event aggregator pattern – Callum Linington Dec 03 '15 at 08:42

2 Answers2

3

It's good idea not to use the same model value in the directive. Create another innerModel which should be used inside directive and update the "parent" model when needed with provided NgModelController.

With this solution, you don't hack the ng-change behavior - just use what Angular already provides.

angular.module('demo', [])
  .controller('DemoController', function($scope) {
    $scope.value = 'a';
    $scope.displayValue = $scope.value;
    $scope.onValueChange = function() {
      $scope.displayValue = $scope.value;
    };
  })
  .directive("selector", [
    function() {
      return {
        controller: function() {
          this.availableValues = ['a', 'b', 'c'];
        },
        require: 'ngModel',
        controllerAs: 'ctrl',
        scope: {
          'ngModel': '='
        },
        template: '<select ng-model="innerModel" ng-change="updateInnerModel()" ng-options="v for v in ctrl.availableValues"> </select>',
        link: function(scope, elem, attrs, ngModelController) {
          scope.innerModel = scope.ngModel;
          scope.updateInnerModel = function() {
            ngModelController.$setViewValue(scope.innerModel);
          };
        }
      };
    }
  ]);
<div ng-app="demo" ng-controller="DemoController">
  <div selector ng-model="value" ng-change="onValueChange()">
  </div>
  <div>
    <span>Value when ng-change called:</span>
    <span>{{ displayValue }}</span>
  </div>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>

Inspired by this answer.

Community
  • 1
  • 1
fracz
  • 20,536
  • 18
  • 103
  • 149
  • Thanks! This works and makes a lot of sense. Also, thanks for editing my code blocks to be executable; I haven't played around with that yet. – Tony S Yu Dec 03 '15 at 04:17
1

It's a digest issue.. Just wrap the onValueChange operations with $timeout:

$scope.onValueChange = function() {
   $timeout(function(){
       $scope.displayValue = $scope.value;        
   });
};
  • Don't forget to inject $timeout in your controller.

... or you can check this link on how to implement ng-change for a custom directive

Community
  • 1
  • 1
Tanase Butcaru
  • 962
  • 1
  • 10
  • 23
  • This seems like a viable solution, but @fracz's solution seemed more correct to me. (I assume his approach is what you were suggesting in your last line.) – Tony S Yu Dec 03 '15 at 04:13