It's not easy to frame this question, so I will try to explain what I want to know with an example:
Consider this simple angularjs app
: PLUNKER
angular.module('testApp', [])
.controller('mainCtrl', function($scope) {
$scope.isChecked = false;
})
.directive("testDirective", function () {
return {
restrict: 'E',
scope: {
isChecked: '='
},
template: '<label><input type="checkbox" ng-model="isChecked" /> Is it Checked?</label>'+
'<p>In the <b>directive\'s</b> scope <b>{{isChecked?"it\'s checked":"it isn\'t checked"}}</b>.</p>'
};
});
With this html:
<body ng-controller="mainCtrl">
<test-directive is-checked="isChecked"></test-directive>
<p>In the <b>controller's</b> scope <b>{{isChecked?"it\'s checked":"it isn\'t checked"}}</b>.</p>
</body>
The app:
So far, so good.
Now let's make a little change, like this: PLUNKER
angular.module('testApp', [])
.controller('mainCtrl', function($scope) {
$scope.isChecked = false;
$scope.doingSomething = function(){alert("In the controller's scope is " + ($scope.isChecked?"checked!":"not checked"))};
})
.directive("testDirective", function () {
return {
restrict: 'E',
scope: {
isChecked: '=',
doSomething: '&'
},
template: '<label><input type="checkbox" ng-change="doSomething()" ng-model="isChecked" /> Is it Checked?</label>'+
'<p>In the <b>directive\'s</b> scope <b>{{isChecked?"it\'s checked":"it isn\'t checked"}}</b>.</p>'
};
});
and this:
<!DOCTYPE html>
<html ng-app="testApp">
<head>
<script data-require="[email protected]" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.5/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-controller="mainCtrl">
<test-directive is-checked="isChecked" do-something="doingSomething()"></test-directive>
<p>In the <b>controller's</b> scope <b>{{isChecked?"it\'s checked":"it isn\'t checked"}}</b>.</p>
</body>
</html>
The only thing that we have done is:
window.alert
indicating if the 'isChecked' attribute of the controller's scope is checked or unchecked. (I'm doing a window.alert
on purpose, because I want the execution to stop)Now, when we check or uncheck the checkbox we get an alert, and in that alert we can see that the scope of the directive hasn't been updated yet. Ok, so one would think that the ng-change
gets triggered before the model gets updated, also while the alert is being displayed we can see that according to the text rendered in the browser "isChecked" has the same value in both scopes. All right, no big deal, if that's how the "ng-change" behave, so be it, we can always set a $watch
and run the function there... But lets do another experiment:
Like this: PLUNKER
.directive("testDirective", function () {
return {
restrict: 'E',
scope: {
isChecked: '=',
doSomething: '&'
},
controller: function($scope){
$scope.internalDoSomething = function(){alert("In the directive's scope is " + ($scope.isChecked?"checked!":"not checked"))};
},
template: '<label><input type="checkbox" ng-change="internalDoSomething()" ng-model="isChecked" /> Is it Checked?</label>'+
'<p>In the <b>directive\'s</b> scope <b>{{isChecked?"it\'s checked":"it isn\'t checked"}}</b>.</p>'
};
});
Now we are just using a function of the scope of the directive to do the same thing that the function of the scope of the controller was doing, but this time it turns out that the model has been updated, so it seems that at this point the scope of the directive is updated but the scope of the controller is not updated... Weird!
Let's make sure that that's the case: PLUNKER
angular.module('testApp', [])
.controller('mainCtrl', function($scope) {
$scope.isChecked = false;
$scope.doingSomething = function(directiveIsChecked){
alert("In the controller's scope is " + ($scope.isChecked?"checked!":"not checked") + "\n"
+ "In the directive's scope is " + (directiveIsChecked?"checked!":"not checked") );
};
})
.directive("testDirective", function () {
return {
restrict: 'E',
scope: {
isChecked: '=',
doSomething: '&'
},
controller: function($scope){
$scope.internalDoSomething = function(){ $scope.doSomething({directiveIsChecked:$scope.isChecked}) };
},
template: '<label><input type="checkbox" ng-change="internalDoSomething()" ng-model="isChecked" /> Is it Checked?</label>'+
'<p>In the <b>directive\'s</b> scope <b>{{isChecked?"it\'s checked":"it isn\'t checked"}}</b>.</p>'
};
});
This time we are using the function of the scope of the directive to trigger the bound function of the controller, and we are passing an argument to the controller's function with the value of the directive's scope. Now in the controller's function we can confirm what we already suspected in the previous step, which is: that the isolated scope gets updated first, then the ng-change
gets triggered, and it's not after that, that the bindings of the directive's scope get updated.
Now, finally, my question/s:
In other words: if the "ng-change" got triggered before the model gets updated, I could understand that, but I'm having a very hard time understanding that a function gets triggered after updating the model and before finishing to populate the changes of the bound properties.
If you read this far: congratulations and thanks for your patience!
Josep
To summarize the problem, ngModelController
has a process to go through before watches
will be fired. You're logging the outer $scope
property before ngModelController
has processed the change and caused a $digest cycle, which would in turn fire $watchers
. I wouldn't consider the model
updated until that point.
This is a complex system. I made this demo as a reference. I recommend changing the return
values, typing, and clicking - just messing around with it in all kinds of ways and checking the log. This makes it clear very quickly how everything works.
ngModelController
has it's own arrays of functions to run as responses to different changes.
ngModelController
has two kinds of "pipelines" for determining what to do with a kind of change. These allow the developer to control the flow of values.
If the scope property assigned as ngModel
changes, the $formatter
pipeline will run. This pipeline is used to determine how the value coming from $scope
should be displayed in the view, but leaves the model alone. So, ng-model="foo"
and $scope.foo = '123'
, would typically display 123
in the input, but the formatter could return 1-2-3
or any value. $scope.foo
is still 123, but it is displayed as whatever the formatter returned.
$parsers
deal with the same thing, but in reverse. When the user types something, the $parser pipeline is run. Whatever a $parser
returns is what will be set to ngModel.$modelValue
. So, if the user types abc
and the $parser
returns a-b-c
, then the view won't change, but $scope.foo
now is a-b-c
.
After either a $formatter
or $parser
runs, $validators
will be run. The validity of whatever property name is used for the validator will be set by the return value of the validation function (true
or false
).
$viewChangeListeners
are fired after view changes, not model changes. This one is especially confusing because we're referring to $scope.foo
and NOT ngModel.$modelValue
. A view will inevitably update ngModel.$modelValue
(unless prevented in the pipeline), but that is not the model change
we're referring to. Basically, $viewChangeListeners
are fired after $parsers
and NOT after $formatters
. So, when the view value changes (user types), $parsers, $validators, then $viewChangeListeners
. Fun times =D
All of this happens internally from ngModelController
. During the process, the ngModel
object is not updated like you might expect. The pipeline is passing around values that will affect that object. At the end of the process, the ngModel
object will be updated with the proper $viewValue
and $modelValue
.
Finally, the ngModelController
is done and a $digest
cycle will occur to allow the rest of the application to respond to the resulting changes.
Here's the code from the demo in case anything should happen to it:
<form name="form">
<input type="text" name="foo" ng-model="foo" my-directive>
</form>
<button ng-click="changeModel()">Change Model</button>
<p>$scope.foo = {{foo}}</p>
<p>Valid: {{!form.foo.$error.test}}</p>
JS:
angular.module('myApp', [])
.controller('myCtrl', function($scope) {
$scope.foo = '123';
console.log('------ MODEL CHANGED ($scope.foo = "123") ------');
$scope.changeModel = function() {
$scope.foo = 'abc';
console.log('------ MODEL CHANGED ($scope.foo = "abc") ------');
};
})
.directive('myDirective', function() {
var directive = {
require: 'ngModel',
link: function($scope, $elememt, $attrs, $ngModel) {
$ngModel.$formatters.unshift(function(modelVal) {
console.log('-- Formatter --', JSON.stringify({
modelVal:modelVal,
ngModel: {
viewVal: $ngModel.$viewValue,
modelVal: $ngModel.$modelValue
}
}, null, 2))
return modelVal;
});
$ngModel.$validators.test = function(modelVal, viewVal) {
console.log('-- Validator --', JSON.stringify({
modelVal:modelVal,
viewVal:viewVal,
ngModel: {
viewVal: $ngModel.$viewValue,
modelVal: $ngModel.$modelValue
}
}, null, 2))
return true;
};
$ngModel.$parsers.unshift(function(inputVal) {
console.log('------ VIEW VALUE CHANGED (user typed in input)------');
console.log('-- Parser --', JSON.stringify({
inputVal:inputVal,
ngModel: {
viewVal: $ngModel.$viewValue,
modelVal: $ngModel.$modelValue
}
}, null, 2))
return inputVal;
});
$ngModel.$viewChangeListeners.push(function() {
console.log('-- viewChangeListener --', JSON.stringify({
ngModel: {
viewVal: $ngModel.$viewValue,
modelVal: $ngModel.$modelValue
}
}, null, 2))
});
// same as $watch('foo')
$scope.$watch(function() {
return $ngModel.$viewValue;
}, function(newVal) {
console.log('-- $watch "foo" --', JSON.stringify({
newVal:newVal,
ngModel: {
viewVal: $ngModel.$viewValue,
modelVal: $ngModel.$modelValue
}
}, null, 2))
});
}
};
return directive;
})
;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With