Calling $setViewValue in a directive doesn't fire $watch in the controller - javascript

I am using a directive with a ngModelController like this
var directive = {
link: link,
restrict: 'E',
require: '?ngModel'
}
My link function does something like this (I omitted the long directive stuff)
function link(scope, element, attrs, ctrl) {
scope.updateModel = function(){
var newModel = {};
newModel.aValue = 'foo';
ctrl.$setViewValue(newModel);
}
}
I use the directive in my controller
View: this is correctly updated
<my-directive ng-model="aModel">
{{aModel.aValue}} // This is changed correctly when I update the value in the directive
</my-directive>
Controller: here is where I have problems
$scope.aModel = {};
$scope.aModel.aValue = 'bar';
$scope.$watch('aModel', function(newValue, oldValue){
console.log('aModel changed'); // This is never fired
});
$scope.$watch('aModel.aValue', function(newValue, oldValue){
console.log('aModel changed'); // This is neither fired
});
The documentation of $setViewValue says
"Note that calling this function does not trigger a $digest."
so I tried to fire it manually with $render, $digest or $apply, but every time run into trouble since the console says that the $digest is already in progress.
So what's the problem with this?
UPDATE
I just realized something really interesting.
Adding a $timeout to check the value in the controller, I realized that it is not actually changed and that's probably why the $watch function is not called.
But since the view is updated, I don't really get the point.
This is the situation:
Controller
$timeout(function(){
console.log('Check aModel value in the controller');
console.log($scope.aModel.aValue); // This is always 'bar' even if the view display 'foo'
},10000);
UPDATE 2
I think I found the problem: ngModel cannot be an object but it must be a value.
I leave the question open to see if anyone comes with a different solution, but I guess that's the point.
UPDATE 3
I was wrong, you can update the ngModel as object, but there is no way to fire the $watch method.
I create a plunker to show the problem.
I put a large timeout to avoid any $digest in progress problem.
Any hint would be really appreciated.

Add third parameter true to $watch function.

Use $timeout with no time parameter in order to call $apply and trigger the digest loop. This will cause it to execute a fraction of a second later and not run into the digest loop in a digest loop error. Clearly what your doing is not 'angular aware' and so you need to trigger the digest loop using $apply but your in a digest loop so you need to use the 'hack' mentioned above.

Related

Is AngularJs digest cycle triggered after any function call?

I have a directive that defines a function innerBarStyle() at the link stage and binds it to the scope:
restrict : 'EA',
scope: {
values: '='
},
link: function(scope, elements, attributes){
scope.innerBarStyle = function(value){
console.count("innerBarStyleCounter");
return {
width: 10px;
};
}),
templateUrl: 'template.html'
};
The function does nothing but counting the number of times it gets executed and returning an object.
Now, in a template directive's template I'm calling this function by means of an expression. Something like <div ... ng-style=innerBarStyle(someValueInCurrentScope)><div>
What I get in practice is an infinite loop that causes the aforementioned function to be called repeatedly.
After some research, I've found that this usually occurs when the called function implicitly or explicitly triggers the digest cycle (e.g. if it makes use of the $http service). But in this case the function is really doing nothing. Is it possible that the digest cycle is triggered somewhere else or am I missing something?
BTW, I know that there would be better ways to achieve the same result, I'm just curious about how things works here.
Without seeing the actual code (I understand you can't post the exact code since it's for your work) I can only guess. But I think what's happening is that you are adjusting the style of the element via the return of the $scope. innerBarStyle which triggers the ng-style directive which calls a digest cycle, which triggers the scope function again. Ergo the continuous execution of this logic.
In order to fix this you should probably use the angular.element APIs on the elem of the directive to adjust the CSS.

Angular scope watch doesn't work in my app

I've got the following code where I'm trying to show/hide a text box based on if a key was pressed in the global window scope. However, every time a key is pressed, it does not seem to fire the watch service. Why is this?
Plnkr here http://plnkr.co/edit/qL9ShNKegqJfnyMvichk
app.controller('MainCtrl', function($scope, $window) {
$scope.name = '';
angular.element($window).on('keypress', function(e) {
//this changes the name variable
$scope.name = String.fromCharCode(e.which);
console.log($scope.name)
})
$scope.$watch('name', function() {
console.log('hey, name has changed!');
});
});
It is because you are handling the keypress event outside of the digest cycle. I would strongly encourage you to let angular do its thing with databinding or using ngKeypress
Otherwise, in your handler, call $scope.$digest().
angular.element($window).on('keypress', function(e) {
//this changes the name variable
$scope.name = String.fromCharCode(e.which);
console.log($scope.name);
$scope.$digest();
})
On a high level view, watching a value on a scope needs two parts:
First: the watcher - like you created one. Every watcher has two parts, the watch function (or like here the value) and the listener function. The watch function returns the watched object, the listener function is called when the object has changed.
Second: the $digest cycle. The $digest loops over all watchers on a scope, calls the watch function, compares the returned newValue with the oldValue and calls the corresponding listener function if these two do not match. This is called dirty-checking.
But someone has to kick the $digest. Angular does it inside its directives for you, so you don't care. Also all build-in services start the digest. But if you change the object outside of angular's control you have to call $digest yourself, or the preferred way, use $apply.
$scope.$apply(function(newName) {
$scope.name = newName;
});
$apply first evaluates the function and then starts the $digest.
In your special case, I would suggest to use ngKeypress to do it the angular way.

$animate.removeClass not working without $evalAsync inside directive?

I have created a directive that fade out and fade in the view on model changes.
app.controller('myCtrl', function($scope, $interval) {
$scope.number = 0;
$interval(function() {
$scope.number++;
}, 1000);
});
app.directive('fadeOnChange', function($timeout, $animate) {
return {
restrict: 'E',
//We output the value which will be set between fades
template: '{{theFadedValue.nv}}',
link: function(scope, elem, attr) {
//The scope variable we will watch
var vtw = attr.valueToWatch;
//We add the anim class to set transitions
$animate.addClass(elem, 'anim');
//We watch the value
scope.$watch(vtw, function(nv) {
//we fade out
var promise = $animate.addClass(elem, 'fade-it-out');
//when fade out is done, we set the new value
promise.then(function() {
scope.$evalAsync(function() {
scope.theFadedValue = {"nv": nv};
//we fade it back in
$animate.removeClass(elem, 'fade-it-out');
});
});
})
}
};
});
And here's the view
<div ng-controller="myCtrl">
<h1><fade-on-change value-to-watch="number"></fade-on-change></h1>
</div>
It works perfectly but I'd like to understand why I need to use $apply, $digest, $timeout or $evalAsync to wrap my call to $animate.removeClass for it to work? If I don't, the class simply does not get removed (which caused me a lot of headaches this afternoon).
I red about those four methods and understand how they are different, but the need to use one of those in this case mystifies me.
plunker
Basically async methods are don't run $digest cycle directly.(exceptional case for $http because it internally wraps inside $scope.$apply())
In your case you are waiting for complete promise which async. That's why you could use $timeout(function(){ }) or $evalAsync, This angular service wrap inside $scope.$apply() & $scope.$apply() internally run the digest cycle and all scope variables gets updated.
$evalAsync run after the DOM has been manipulated by Angular, but
before the browser renders
Link here for more information on when to use $scope.$apply()
Hope this will clear your doubts. Thanks.

Using $compile in a directive triggers AngularJS infinite digest error

Any thoughts on why this directive is triggering an infinite digest error?
http://jsfiddle.net/smithkl42/cwrgLd0L/13/
var App = angular.module('prettifyTest', []);
App.controller('myCtrl', function ($scope) {
$scope.message = 'Hello, world!';
})
App.directive('prettify', ['$compile', function ($compile) {
var template;
return {
restrict: 'E',
link: function (scope, element, attrs) {
if (!template) {
template = element.html();
}
scope.$watch(function () {
var compiled = $compile(template)(scope);
element.html('');
element.append(compiled);
var html = element.html();
var prettified = prettyPrintOne(html);
element.html(prettified);
});
}
};
}]);
It seems to be the very first line in the scope.$watch() function that's triggering the model update, as when I remove that line, it doesn't trigger the error.
var compiled = $compile(template)(scope);
I'm a little confused as to why that line is triggering another $digest - it doesn't seem to be updating anything directly in the scope.
Is there a better way to accomplish what I'm trying to do, e.g., some other way to check to see if the key values in the scope have actually changed so I can recompile the template? (And is there a better way of grabbing the template?)
When you use scope.$watch() with just a function and no watch expression, it registers a watcher that gets triggered on every digest cycle. Since you're calling $compile within that watcher, that's effectively triggering another digest cycle each time since it needs to process the watchers created by your template. This effectively creates your infinite digest cycle.
To use the same code, you should probably just be compiling once in your postLink function, but I don't think you even need to do that - you should be able just use the template property. Then your $watch() statement should include an expression targeting the property you want to watch for changes - in this case, just 'message', and update the HTML accordingly.

Can anyone explain this small piece of code?

I was looking at one of the custom implementations of ng-blur (I know it's already available in the standard AngularJS now). The last line is what I don't understand.
.controller('formController', function($scope){
$scope.formData = {};
$scope.myFunc = function(){
alert('mew');
console.log(arguments.length);
}
})
.directive('mew', function($parse){
return function(scope, element, attr){
var fn = $parse(attr['mew']);
element.bind('blur', function(event){
scope.$apply(function(){
fn(scope);
});
});
}
});
In the view there's a simple mew="myFunc()" applied to inputs.
My question is why are we passing the scope to the function in the very last line of the directive. I tried to make it work without that but it doesn't. What's actually happening?
Also this too works scope.$apply(attr.mew). Same reason or something different?
$parse only does just that, it parses the string passed in, you need to call the resulting function with the current scope because otherwise how else would it know which function to call?
scope.$apply works in the following manner:
The expression is executed using the $eval() method.
Any exceptions from the execution of the expression are forwarded to the $exceptionHandler service.
The watch listeners are fired immediately after the expression was executed using the $digest() method.
The reason scope.$apply(attr.mew) is due to the fact that it's doing all of the above. It is parsing, and then applying the result of the parse to the scope.
Another option is to use an isolate scope to bind your directive to the mew attr.
return {
scope: {
mew: '&'
},
link: function (scope, element, attr) {
var fn = scope.mew;
element.bind('blur', function (event) {
scope.$apply(function () {
fn();
});
});
}
}
Example
For this specific example it will work, but as you said, the blur is out of the digest loop. In most of the use cases the function will change data on one scope or another, and the digest loop should run and catch those changes.

Categories

Resources