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

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.

Related

Angularjs watch is not triggering

I have three elements to this program. First, in the service, I have:
$scope.loaded = MySvc.loaded;
$scope.loaded = false;
Then, in the controller which imports this service, I have a series of introductory things. I need to trigger events in the directives once the controller is done with its asycnhronous work, so in the controller, when that trigger is ready, I do
MySvc.loaded = true;
And then in the directive, which also imports the service, I have,
$scope.loaded = MySvc.loaded;
$scope.$watch('loaded', function (newValue, oldValue) {
The directive triggers when loaded is initialized to 'false', but when I change the value to 'true', nothing happens. The watch simply does not trigger. Why not? How do I fix this?
This is a fine way to do things, and I view it as orthogonal to promises (which are indeed extremely useful). Your problem comes from breaking the linkage you've created with the assignment. Instead try:
$scope.data = MySvc.data;
And append to that, e.g. MySvc.data.loaded = false. This way the data variable is never reassigned, so your linkage between controller and service stays intact.
Then you can either watch data.loaded or watch data as a collection by passing true as the 3rd option to $watch.
As I said in comment, you may have problems with yours scopes overridding the loaded property.
Try using the angular events to solve your problem.
angular.module('test', [])
.controller 'MyController', ($scope, $timeout) ->
$timeout ->
$scope.$broadcast('READY')
, 2000
.directive 'myDirective', ->
scope: {}
template: '<span>{{ value }}</span>'
link: ($scope) ->
$scope.value = "I'm waiting to be ready..."
$scope.$on 'READY', ->
$scope.value = "I'm ready!!"
See this in action (CoffeeScript as it's faster for prototyping but should be clear enough).
You're making this complicated. Use promises instead!!! They're sweet. Promises keep state of their resolution.
.factory('hello_world', function ($q, $timeout) {
var promise = $q.defer;
$timeout(function () {
promise.resolve('hello world');
}, 1000)
return promise.promise;
})
.controller('ctrl', function ($scope, hello_world) {
hello_world.then(function (response) {
console.log("I will only be ran when the async event resolves!");
console.log(response);
});
});
As you can see promises are much better, no watches. No weird gates. Simpler, and sexy. ;)

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

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.

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.

How to write directive that hooks into ng-click handler

I'm facing a situation in Angular where I sense I'm doing something wrong but I can't find the Angular way to solve it.
I'm developing a mobile app. I would like to create a directive let's call it cc-tap-highlight that would be used in conjunction with ng-click. Meaning that I could use it like this:
<a ng-click="doSomething()" cc-tap-highlight>Click me</a>
What this would do is to add a class to the clicked element and remove it after some seconds.
Now one could say, let's just manually bind to the elements click event in the directive. That would work for a desktop app. However, on mobile devices Angular does a lot of magic for us to have fast tap/clicks:
https://github.com/angular/angular.js/blob/master/src/ngMobile/directive/ngClick.js
For sure, I don't want to reimplement all of it's magic!
So, currently, instead of having my cc-tap-highlight directive I use this rather hackish approach:
In the view
<a ng-click="doSomething($event)" cc-tap-highlight>Click me</a>
In the controller:
$scope.doSomething = function($event){
//do your things with $event.currentTarget
}
There are two major problems with this approach:
the controller should not manipulate the DOM
We need to repeat the patter over and over through our entire code base violating DRY
However, I can't for the life of me, figure out how to write a directive that hooks into the ng-click handler and does it's things.
You can try to make your directive generate a ng-click directive with wrapper function.
Here's a quick example. It's by far not thoroughly tested but I think the principle is sound. What you want is your custom code to run before/after the click event regardless of how that's triggered(tap,click, whatever).
This does have the drawback that it creates a new scope so interaction with other directives that may need isolate scope was not tested.
DIRECTIVE
app.directive('myClick', ['$parse','$compile', function($parse, $compile) {
return {
restrict: 'A',
compile : function(tElement, tAttrs, transclude) {
//you can call the wrapper function whatever you want.
//_myClick might be more appropriate to indicate it's not really public
tElement.attr('ng-click', 'myClick($event)');
tElement.removeAttr('my-click');
var fn = $parse(tAttrs['myClick']);
return {
pre : function(scope, iElement, iAttrs, controller) {
console.log(scope, controller);
scope.myClick = function(event) {
console.log('myClick.before');
fn(scope, {$event:event});
console.log('myClick.after');
};
$compile(iElement)(scope);
},
post : function postLink(scope, iElement, iAttrs, controller) {
}
};
},
scope : true
};
}]);
CONTROLLER
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.test = function($event) {
console.log('test', $event);
};
//this is to show that even if you have a function with the same name,
//the wrapper function is still the one bound thanks to the new scope
$scope.myClick = function() {
console.log('dummy my click');
};
});
HTML
<button ng-click="test($event)">NG-CLICK</button>
<button my-click="test($event)">MY-CLICK</button>
<button ng-click="myClick($event)">MY-CLICK-DUPLICATE-FN</button>

Categories

Resources