Using $compile in a directive triggers AngularJS infinite digest error - javascript

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.

Related

ng-click does not fire with a function

I am new with Angular and now I have encountered some problems...
So let's say I have a controller called ViewModelController and I use controlleras when I define the routes as following: .
And in my template I have just difened two div which seperate the container in two parts:
<div id='viewleft' class="divleft col-md-5"></div>
<div id='viewright' class="col-md-7 divright"></div>
And in ViewModelController, I have some code to render the template when the controller is loaded. The question is that the ng-click I put in all the elements just don't fire and I don't really know where is the problem.
I have tried thing like below but it just does not work.
var content1 = '<ul><li><button id ="b1" ng-click="vmCtrl.cprint($event.target)">123</button></li><ul>';
$("#viewleft").html(content1);
Can someone helps me on that? Thank you in advance, best wishes.
You have to compile this html so that angular code will work
$compile($("#viewleft").contents())(scope);
or better to use a directive that compile html when its value changes.
app.directive('compile', ['$compile', function ($compile) {
return function (scope, element, attrs) {
scope.$watch(
function (scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function (value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
}]);
in controller you can asssign html to scope variable
$scope.template= '<ul><li><button id ="b1" ng-click="vmCtrl.cprint($event.target)">123</button></li><ul>';
and on view you can add that
<div id='viewleft' class="divleft col-md-5" compile="template"></div>
You can refer this docs for $compile.

Modifying scope not working without $scope.$apply

I have a directive for a button, which when clicked displays a loading screen.
angular.module('randomButton', ['Data'])
.directive('randomButton', function(Data) {
return {
scope: '=',
restrict: 'A',
templateUrl: 'templates/components/randomButton.html',
link: function(scope, element, attrs){
element.click(function(){
scope._loading();
});
}
}
});
It does this by calling another function on the scope contained within a different directive:
angular.module('quoteContainer', [])
.directive('quoteContainer', function(Data) {
function quoteController($scope){
$scope._loading = function(){
console.log('loading');
$scope.loadMessage = Data.getRandomText();
$scope.loading = true;
};
}
return {
scope: '=',
restrict: 'A',
templateUrl: 'templates/components/quoteContainer.html',
controller: ['$scope', quoteController],
link: function(scope,element,attrs){
}
}
});
My problem is, for this change to occur, I'm having to call $scope.$apply within the ._loading() function. For example:
$scope._loading = function(){
console.log('loading');
$scope.$apply(function(){
$scope.loadMessage = Data.getRandomText();
$scope.loading = true;
});
};
I understand this is bad practice, and should only be used when interfacing with other frameworks/ajax calls etc. So why does my code refuse to work without $scope.$apply, and how can I make it work without it?
Basically you need to let angular know some way of the change happening, because since it's in an asynchronous event handler, angular's usual mechanisms for noticing changes are not applying to it.
Hence the need to wrap it in $apply, which triggers a digest cycle after your code has run, giving angular a chance to make changes according to the new data. However, the preferred way of doing this is using angular's built-in $timeout service, which effectively does the same (i.e. wrapping your code in an $apply block), but doesn't have problems with the possibility that an other digest cycle might be ongoing when it's triggered.
You can use it the same way as you're currently using $apply:
$timeout(function(){
$scope.loadMessage = Data.getRandomText();
$scope.loading = true;
});
(It can take a second parameter if you actually want to delay the application of the values, but it's not necessary in your case.)

$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.

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

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.

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