Model inside a template of a custom directive never updated - angularjs

I have a simple custom directive myElement with a templateUrl that print a simple message:
<p>Message: {{message}}</p>
Here's the definition of my directive:
testapp.directive('myElement', function() {
return {
restrict: 'E',
template: '<p>Message: {{message}}</p>',
link: function(scope, elem, attrs) {
scope.message = 'This message is never updated... :(';
setTimeout(function() {
scope.message = "Why this message is never shown?";
}, 1000);
}
};
});
After 1 second, I expect an update of the message to "Why this message is never shown?". Unfortunately, the message is never updated.
Here is the jsFiddle: http://jsfiddle.net/seyz/SNMfc
Could you explain me why?

Due to how Angular dirty checking works, when you execute code outside of angular scope (e.g. using setTimeout, setInterval, or from within some third-party plugin callback) the changes produced by that code invocation won't be 'recognized' immediately by Angular scope.
For such scenarios you need to wrap your code inside the scope.$apply() method.
In this particular case you may simply use $timeout function to replace your setTimeout(fn, 1000)call with $timeout(fn, 1000)and your code will be wrapped with scope.$apply() call (Plunker).

You will need to use scope.$apply();
setTimeout(function() {
scope.message = "Why this message is never shown?";
scope.$apply();
}, 1000);
From the documentation:
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries).

Related

Angular class change directive doesn't work

I want to add a directive that will do something when an element class is changed.
My directive is:
(function (angular) {
angular.module('myApp')
.directive('myClassWatch', myClassWatch);
function myClassWatch() {
return {
restrict: 'A',
link: function (scope, element, attrs, controller) {
scope.$watch(function () {
return element.attr('class');
}, function (newValue, oldValue) {
debugger;
if(oldValue !== newValue)
console.log('class changed from ' + oldValue + ' to ' + newValue);
});
}
}
}
})(angular);
The html is:
<div class="top-icons-item popup-container popup-link-container" my-class-watch>
</div>
I do some actions elsewhere that toggles the class "open" in my div element - it is visible in the html - yet the debugger is never called (also no console logs of course). I can see that the link function is called on page load and the debugger also stops, but thats only on page load and not afterwards when I actually do actions that adds another class.
I have read several issued here before including Directive : $observe, class attribute change caught only once but I can't understand why I don't get the same result. What can I do to try check why this occures?
Update: The class change is made using jQuery not in a controller but in an old jquery watches code. May this be cause? could angular be unaware of class change when its not done from an angular code?
Wrap your jQuery code into $apply.
It similar to you are making changes to $scope out of angular context(jquery ajax, setTimeout, etc). Use $apply to make angular know about the changes done.
angular.element(document.getElementById('app')).injector().invoke(['$compile', '$rootScope', function($compile, $rootScope) {
$rootScope.$apply(function() {
//your jquery code goes here...
var a = document.getElementById('abc');
angular.element(a).addClass('hello');
});
}]);

Update Scope With SetInterval / SetTimeout

I'm trying to create a "Clock" directive as a way to learn Angular. How would I go about making sure it "ticks?"
Here's what I tried fiddle Link:
angular.module('app')
.directive('clock' , function(){
return {
restrict: 'E',
template: '<div>{{date}}</div>',
link: function(scope, elem, attr){
scope.date = getDate();
setInterval(function(){
scope.date = getDate();
}, 100);
}
}
})
Using setInterval to upate the scope variable isn't working. I'm assuming because the directive isn't watching for it to be changes anymore?
It is updating the scope variable, but Angular isn't aware of the change.
It is recommended to use Angular $interval.
Here is your fiddle with $interval: https://jsfiddle.net/oafcde6g/11/
In other words, Angular doesn't do any magic by "listening" to scope properties, it simply does dirty-checking. But to do it, it needs to be notified that something changed. So if you really wanted to use setInterval instead of $interval, you could notify it manually that something changed:
setInterval(function () {
scope.$apply(function () {
scope.date = getDate();
});
});
This way, you're notifying it to run your anonymous function inside $apply and then digest the scope, checking if anything changed. If it did, it will update the view.
"Magic" of ng-* events (ng-click, ng-model etc)
Even though it looks like angular automatically knows to rerender the view when you click/change something by using it's built-in directives, it really isn't so.
Some simple implementation of ng-click would look something like:
angular.module('app')
.directive('ngClick' , function(){
return {
restrict: 'A',
link: function(scope, elem, attr){
elem.on('click', function () {
scope.$apply(function () {
scope.$eval(attr.ngClick);
});
});
}
}
})
You can see this for yourself if you try to bind click event normally, outside of angular context and make it change a scope variable. It won't update until you explicitly tell it to (like we did in setInterval example).

When does Angulars link function run?

From what I understand it only runs once before the page is rendered. But is there ever a case where it runs after the page has been rendered?
I tried testing a few things with this plnkr:
angular
.module('app', [])
.directive('test', function($http) {
return {
restrict: 'E',
template: '<input />',
link: function(scope, el, attrs) {
var input = angular.element(el[0].children[0]);
input.on('change', function() {
console.log('change event');
scope.$apply(function() {
console.log('digest cycle');
});
});
input.on('keyup', function() {
console.log('keyup event');
var root = 'http://jsonplaceholder.typicode.com';
$http.get(root+'/users')
.success(function() {
console.log('http request successful');
})
.error(function() {
console.log('http request error');
});
});
console.log('link function run');
}
};
});
Does typing in the input field cause the link function to run?
Do event listeners cause the link function to run?
Do http requests (made with $http?) cause the link function to run?
Do digest cycles cause the link function to run?
The answer to all of these questions seem to be "no".
The link function runs when an instance of the directive is compiled, in this case, when a <test></test> element is created. That can be when angular's bootstrapping compiles the page, when it comes into being from a ng-if, when a ng-repeat makes it, when it's made with $compile, etc.
link will never fire twice for the same instance of the directive. Notably, it fires right after the template has been compiled in the directive's lifecycle.
1 - No, it causes to change the only ng-model if you have it binded.
2 - No, it will only launch the code inside the event binds.
3 - Again no, the event bind will launch the $http.get(). And please don't put an $http directly on your directive. Use a factory or something like that.
4 - Dunno
As Dylan Watt said, the directive link runs only when the directive is compiled (only once) per element/attr.... You can compile it in different ways. Plain http, $compile, ng-repeat....
You can create a $watch inside your directive to "relaunch" some code on a binded element change.
This maybe can help you: How to call a method defined in an AngularJS directive?

Firing angular js animation within a directive

I have a simple directive along these lines:
angular.module('application').directive( 'sdTitle', sdTitleDirective );
sdTitleDirective.$inject = ['$animate'];
function sdTitleDirective($animate) {
var directive = {
template: '<div class="title-container">some content</div>',
link: link,
replace: true
};
return directive;
function link(scope, element, attrs) {
element.bind("click", function() {
$animate.leave(element);
});
}
}
And an animation similar to this:
angular.module('application').animation('.title-container', titleAnimation);
function titleAnimation() {
return {
leave: leaveAnimation
};
function leaveAnimation(element, done) {
console.log('animate leave', element);
element.hide().fadeOut(800, done);
}
}
I can't seem to get the leaveAnimation to actually fire when the directive's element is clicked. I must be missing something with how $animate works or how javascript animations are called, but I'm at a loss.
How do I correctly use the $animate service's animation methods within a directive and with javascript rather than CSS3 animations?
DOM event handlers attached by for example addEventListener() or the jqLite/jQuery methods bind and on are executed outside of the current Angular context and will not trigger the digest loop.
You will have to do it manually by using for example $apply:
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life cycle of
exception handling, executing watches.
Example:
element.bind("click", function() {
scope.$apply(function() {
$animate.leave(element);
});
});
Demo: http://plnkr.co/edit/OepqBsCW25Lf2qCenbtK?p=preview
It's not apparent from your posted code if you aren't, but you need to use the ngAnimate module.
Note that in the demo I also changed:
element.hide().fadeOut(800, done);
To:
element.fadeOut(800, done);

Is there a way to detect directive destruction in AngularJS?

I want to use a $timeout in a directive in AngularJS. But I can't find a way in the directive documentation to detect when it is destroyed, in case it happens before my timeout finishes and I need to cleanup the timeout.
Is there an event I can bind to or some built in function (similar to $destroy for controllers) that I can use to detect when my directive will be destroyed? Or am I missing a fundamental concept about directives?
The $destroy event you mentioned can also be used in a directive:
app.directive('myDirective', function() {
return {
link: function(scope) {
scope.$on('$destroy', function() {
// Clean up
});
}
};
});

Resources