Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ngChange fires before value makes it out of isolate scope

//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
    $scope.loadResults = function (){
        console.log($scope.searchFilter);
    };
});

// directive
angular.module('myApp')
.directive('customSearch', function () {
    return {
        scope: {
            searchModel: '=ngModel',
            searchChange: '&ngChange',
        },
        require: 'ngModel',
        template: '<input type="text" ng-model="searchModel" ng-change="searchChange()"/>',
        restrict: 'E'
    };
});

// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>

Here is a simplified directive to illustrate. When I type into the input, I expect the console.log in loadResults to log out exactly what I have already typed. It actually logs one character behind because loadResults is running just before the searchFilter var in the main controller is receiving the new value from the directive. Logging inside the directive however, everything works as expected. Why is this happening?

My Solution

After getting an understanding of what was happening with ngChange in my simple example, I realized my actual problem was complicated a bit more by the fact that the ngModel I am actually passing in is an object, whose properties i am changing, and also that I am using form validation with this directive as one of the inputs. I found that using $timeout and $eval inside the directive solved all of my problems:

//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
    $scope.loadResults = function (){
        console.log($scope.searchFilter);
    };
});

// directive
angular.module('myApp')
.directive('customSearch', function ($timeout) {
    return {
        scope: {
            searchModel: '=ngModel'
        },
        require: 'ngModel',
        template: '<input type="text" ng-model="searchModel.subProp" ng-change="valueChange()"/>',
        restrict: 'E',
        link: function ($scope, $element, $attrs, ngModel)
        {
            $scope.valueChange = function()
            {
                $timeout(function()
                {
                    if ($attrs.ngChange) $scope.$parent.$eval($attrs.ngChange);
                }, 0);
            };
        }
    };
});

// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>
like image 404
Rob Allsopp Avatar asked Oct 16 '15 19:10

Rob Allsopp


People also ask

What is the correct way to create a new isolated scope?

You can create a new scope manually. You can create a new scope from $rootScope if you inject it, or just from your controller scope - this shouldn't matter as you'll be making it isolated. var alertScope = $scope. $new(true); alertScope.

What is ngChange?

Ng-change is a directive in AngularJS which is meant for performing operations when a component value or event is changed. In other words, ng-change directive tells AngularJS what to do when the value of an HTML element changes. An ng-model directive is required by the ng-change directive.


2 Answers

The reason for the behavior, as rightly pointed out in another answer, is because the two-way binding hasn't had a chance to change the outer searchFilter by the time searchChange(), and consequently, loadResults() was invoked.

The solution, however, is very hacky for two reasons.

One, the caller (the user of the directive), should not need to know about these workarounds with $timeout. If nothing else, the $timeout should have been done in the directive rather than in the View controller.

And two - a mistake also made by the OP - is that using ng-model comes with other "expectations" by users of such directives. Having ng-model means that other directives, like validators, parsers, formatters and view-change-listeners (like ng-change) could be used alongside it. To support it properly, one needs to require: "ngModel", rather than bind to its expression via scope: {}. Otherwise, things would not work as expected.

Here's how it's done - for another example, see the official documentation for creating a custom input control.

scope: true, // could also be {}, but I would avoid scope: false here
template: '<input ng-model="innerModel" ng-change="onChange()">',
require: "ngModel",
link: function(scope, element, attrs, ctrls){
  var ngModel = ctrls; // ngModelController

  // from model -> view
  ngModel.$render = function(){
    scope.innerModel = ngModel.$viewValue;
  }

  // from view -> model
  scope.onChange = function(){
    ngModel.$setViewValue(scope.innerModel);
  }
}

Then, ng-change just automatically works, and so do other directives that support ngModel, like ng-required.

like image 194
New Dev Avatar answered Oct 21 '22 03:10

New Dev


You answered your own question in the title! '=' is watched while '&' is not

  • Somewhere outside angular:

    input view value changes

  • next digest cycle:

    ng-model value changes and fires ng-change()

    ng-change adds a $viewChangeListener and is called this same cycle. See: ngModel.js#L714 and ngChange.js implementation.

    At that time $scope.searchFilter hasn't been updated. Console.log's old value

  • next digest cycle: searchFilter is updated by data binding.

UPDATE: Only as a POC that you need 1 extra cycle for the value to propagate you can do the following. See the other anwser (@NewDev for a cleaner approach).

.controller('mainCtrl', function ($scope, $timeout){
    $scope.loadResults = function (){
        $timeout(function(){
           console.log($scope.searchFilter);
        });
    };
});
like image 23
aacotroneo Avatar answered Oct 21 '22 03:10

aacotroneo