javascriptangularjsangularjs-directiveangularjs-ng-change

AngularJS Directive Not Firing On-Change Callback


I've created a numeric stepper for use with CSS styles, but am having issues getting it to fire the ng-change when you type in it manually.

I created a log on the plunker to illustrate when the callback is being fired. As you can see from playing with it, it works fine when you click on the stepper arrows, but not when you type in the box directly.

Current Code Example: Plunker

HTML:

<div class="stepper-container">
    <input type="text" ng-model="ngModel">
    <button class="stepper-up fa fa-chevron-up" ng-click="increment()"></button>
    <button class="stepper-down fa fa-chevron-down" ng-click="decrement()"></button>
</div>

JavaScript:

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

app.controller('MainCtrl', function($scope) {
  $scope.myModel = null;
  $scope.log = [];

  $scope.someMethod = function () {
    $scope.log.push('Change event on ' + $scope.myModel);
  }
});

app.directive('numericStepper', function () {
  return {
    restrict: 'EA',
    require: 'ngModel',
    scope: {
      ngModel: '='
    },
    replace: true,
    templateUrl: 'numeric-stepper.html',
    link: function (scope, element, attrs, ngModelCtrl) {
      console.log('NumericStepper::link', ngModelCtrl.$viewValue);

      var sizingUnit = null;
      var css3Lengths = [
        // Percentage
        '%',
        // Font Relative
        'em', 'ex', 'ch', 'rem',
        // Viewport Relative
        'vw', 'vh', 'vmin', 'vmax',
        // Absolute
        'cm', 'mm', 'in', 'px', 'pt', 'pc'
      ];

      scope.$watch(function () {
        return ngModelCtrl.$modelValue;
      }, function (newValue, oldValue) {
        updateValue();
      });

      ngModelCtrl.$formatters.unshift(function (value) {
        value = isNaN(parseInt(value)) ? 0 : value;
        return value;
      });

      scope.increment = function () {
        updateValue(1)
      };

      scope.decrement = function () {
        updateValue(-1);
      };

      function updateValue(amount) {
        var matches = ngModelCtrl.$viewValue.toString().split(/(-?\d+)/);
        var value = (parseInt(matches[1]) || 0) + (amount || 0);
        sizingUnit = matches[2].trim();

        ngModelCtrl.$setViewValue(value + sizingUnit);
        sanityCheck();
      }

      function sanityCheck() {
        var validity = css3Lengths.indexOf(sizingUnit) != -1;
        ngModelCtrl.$setValidity('invalidUnits', validity);
      }
    }

} });


Solution

  • Change your template text box to include an isolate scope call for ngChange. In that function, use timeout to allow the model update/digest to happen before calling parent controllers change function...

    So change your template textbox:

    <input type="text" ng-model="ngModel" ng-change="textChanged()">
    

    Then change your directive:

    // $timeout works better here than watch
    app.directive('numericStepper', function ($timeout) { 
    
      return {
        restrict: 'EA',
        require: 'ngModel',
        scope: {
          ngModel: '=',
          ngChange: '&' // add me!
        },
        replace: true,
        templateUrl: 'numeric-stepper.html',
        link: function (scope, element, attrs, ngModelCtrl) {
          console.log('NumericStepper::link', ngModelCtrl.$viewValue);
    
          var sizingUnit = null;
          var css3Lengths = [
            // Percentage
            '%',
            // Font Relative
            'em', 'ex', 'ch', 'rem',
            // Viewport Relative
            'vw', 'vh', 'vmin', 'vmax',
            // Absolute
            'cm', 'mm', 'in', 'px', 'pt', 'pc'
          ];
          /********** DONT NEED THIS
          // scope.$watch(function () {
          //   return ngModelCtrl.$modelValue;
          // }, function (newValue, oldValue) {
          //   updateValue();
          // });
          ******************/
    
          // Add this function
          scope.textChanged = function() { 
            $timeout(function(){ 
              updateValue();
              scope.ngChange(); }, 500); // could be lower
          }
          ngModelCtrl.$formatters.unshift(function (value) {
            value = isNaN(parseInt(value)) ? 0 : value;
            return value;
          });
    
          scope.increment = function () {
            updateValue(1)
          };
    
          scope.decrement = function () {
            updateValue(-1);
          };
    
          function updateValue(amount) {
            var matches = ngModelCtrl.$viewValue.toString().split(/(-?\d+)/);
            var value = (parseInt(matches[1]) || 0) + (amount || 0);
            sizingUnit = matches[2].trim();
    
            ngModelCtrl.$setViewValue(value + sizingUnit);
            sanityCheck();
          }
    
          function sanityCheck() {
            var validity = css3Lengths.indexOf(sizingUnit) != -1;
            ngModelCtrl.$setValidity('invalidUnits', validity);
          }
        }
      }
    });
    

    And a working plunker