Running code after an AngularJS animation has completed - angularjs

I have an element whose visibility is toggled by ng-show. I'm also using CSS animations - the automatic ones from ng-animate - on this element to animate its entry.
The element will either contain an image or a video.
In the case that the element contains a video, I want to play it, but I don't want to play the video until it's finished animating in.
As such, I was wondering if there's an easy way to bind a callback to the end of a CSS animation in AngularJS?
The docs reference a doneCallback, but I can't see a way to specify it...
One workaround(?) I have thought of is $watching element.hasClass("ng-hide-add-active") and waiting for it to fire with (true, false), implying it's just been removed..
Is there a nicer way?

#michael-charemza answer worked great for me. If you are using Angular 1.3 they changed the promise a little. I got stuck on this for a little bit but here is the change that got it to work:
if (show) {
$animate.removeClass(element, 'ng-hide').then(scope.afterShow);
}
if (!show) {
$animate.addClass(element, 'ng-hide').then(scope.afterHide);
}
Plunker: Code Example

As #zeroflagL has suggested, a custom directive to replace ngShow is probably the way to go. You can use & to pass callbacks into the directive, which can be called after the animations have finished. For consistency, the animations are done by adding and removing the ng-hide class, which is the same method used by the usual ngShow directive:
app.directive('myShow', function($animate) {
return {
scope: {
'myShow': '=',
'afterShow': '&',
'afterHide': '&'
},
link: function(scope, element) {
scope.$watch('myShow', function(show, oldShow) {
if (show) {
$animate.removeClass(element, 'ng-hide', scope.afterShow);
}
if (!show) {
$animate.addClass(element, 'ng-hide', scope.afterHide);
}
});
}
}
})
Example use of this listening to a scope variable show would be:
<div my-show="show" after-hide="afterHide()" after-show="afterShow()">...</div>
Because this is adding/removing the ng-hide class, the points about animating from the docs about ngShow are still valid, and you need to add display: block !important to the CSS.
You can see an example of this in action at this Plunker.

#michal-charemza solution works great, but the directive creates an isolated scope, so in some cases it cannot be a direct replacement for the default ng-show directive.
I have modified it a bit, so that it does not create any new scopes and can be used interchangeably with the ng-show directive.
app.directive('myShow', function($animate) {
return {
link: function(scope, element, attrs) {
scope.$watch(attrs['myShow'], function(show, oldShow) {
if (show) {
$animate.removeClass(element, 'ng-hide').then(function(){
scope.$apply(attrs['myAfterShow']);
});
} else {
$animate.addClass(element, 'ng-hide').then(function(){
scope.$apply(attrs['myAfterHide']);
});
}
});
}
}
})
Usage:
<div my-show="show" my-after-hide="afterHide()" my-after-show="afterShow()">...</div>
Plunker

Related

How to hook to "OnOpen" listener for uib-popover?

Context: I am using angular 1 and this UIB Popover control.
Since there is a text field in the popover template I called, my target is to focus on that text field whenever the popover is opened.
Unfortunately, there is no popover listener/event for "onOpen".
So I tried to do a
scope.$watch(()=>{return scope.isOpen}, (obj) ={
// where scope.isOpen is the local var in the popover-is-open
// expecting to write some code here to manipulate the element
// to realise the focus operation
// but there is no popover element yet when this is called
})
I was just wondering what other options I might have?
Thanks
I found nothing on the documentation talked about events and found this issue on the ui-bootstrap github stating that they do not support events nor do they ever plan to implement them. https://github.com/angular-ui/bootstrap/issues/5060
If you're looking for a different option that would give you access to the events would be to implement your own popover directive that simply wraps bootstrap popovers. In theory, they can function the same as the ui-bootstrap and allows you to tap directly into the events provided by bootstrap.
HTML
<div my-popover="Hello World" popover-title="Title" popover-shown="myCallback()">...</div>
JavaScript ('my-popover.directive.js')
angular
.module('myModule')
.directive('myPopover', myPopover);
function myPopover() {
return {
scope: {
popoverTitle: '#',
popoverShown: '&'
},
restrict: 'A',
link: function(scope, elem, attr) {
$(elem).popover({
title: scope.popoverTitle,
content: attr.myPopover
});
$(elem).on('shown.bs.popover', function () {
if(scope.popoverShown && typeof scope.popoverShown === 'function'){
scope.popoverShown();
}
});
}
};
}
Similar to uib-popover, you can add support for additional configurations by adding additional scoped properties.

Angular ng-if like directive

I want to have a directive that checks the tag name of component and based on some condition shows/hides the component. I want showing hiding to behave like ng-if (not initialising controller of component). Example:
<my-component custom-if></my-component>
Inside the directive custom-if:
return {
compile: function($element) {
if($element[0].tagName === 'some condition'){
//Element is my-component
$element.remove();
}
}
};
The problem I have is that even if I remove element it still calls controller of the my-component. Same happens if I remove element inside compile or preLink function of directive. I also tried to inherit ng-if but I can't get the tag name of component inside custom-if directive because the element is a comment (probably it is ng-if specific behaviour to wrap element inside comment)
UPDATE: Changed postLink function to compile to make sure it doesn't work as well. It shows / hides the element but it always instantiates controller even if it is removed and that's what I want to avoid
I think you should be able to do it by making customIf hight priority directive. Then in compile function, you can check if host component/directive is allowed to continue or not. If not, customIf just removes element altogether. If check passes, then customIf needs to remove itself by unsetting own attribute and then recompile element again.
Something like this:
.directive('customIf', function($compile) {
return {
priority: 1000000,
terminal: true,
compile: function(element, attrs) {
if (element[0].tagName === 'MY-COMPONENT') {
element.remove();
}
else {
// remove customIf directive and recompile
attrs.$set('customIf', null);
return function(scope, element) {
$compile(element)(scope);
}
}
}
};
});
Demo: http://plnkr.co/edit/Y64i7K4vKCF1z3md6LES?p=preview

How can I be notified when DOM elements are added to my directive?

I've got a simple directive that draws a few elements, like in this example. I want to programatically set some style properties but in the link function, the elements are apparently not there yet.
Here's a fiddle.
What I think is happening is that when I call the colorSquares function, there are no squares yet in the DOM. Wrapping it in a $timeout, it works, but that just feels so wrong.
Is there any way I can be notified when the elements exist? Or is there a place that I can put the code which will access them that is guaranteed to run after they exist?
myApp.directive('myDirective', ['$timeout', function ($timeout) {
return {
restrict: 'E',
replace: false,
link: function (scope, elem, attr) {
scope.squares = [1,2,3,4,5];
function colorSquares() {
var squaresFromDOM = document.getElementsByClassName('square');
for (var i = 0; i < squaresFromDOM.length; i++) {
squaresFromDOM[i].style['background-color'] = '#44DD44';
}
}
// this does not work, apparently because the squares are not in the DOM yet
colorSquares();
// this works (usually). It always works if I give it a delay that is long enough.
//$timeout(colorSquares);
},
template: '<div><div ng-repeat="s in squares" class="square"></div></div>'
};
}]);
You should work with Angular rather than against it which is to say you should use data bindings to do what you are trying to do rather than events/notifications in this context.
http://jsfiddle.net/efdwob3v/5/
link: function (scope, elem, attr) {
scope.squares = [1,2,3,4,5];
scope.style = {"background-color": "red"};
},
template: '<div><div ng-repeat="s in squares" class="square" ng-style="style"></div></div>'
That said there's no difference in doing the above and just using a different class that has that red background color or even just doing style="background-color: red;"
you put the answer in your qeustion, "It always works if I give it a delay that is long enough.".
So just make the delay long enough, in this situation that can be achieved by adding an onload event because when the elements get added to the DOM it calls that event.
So instead of just colorSquares(); you could use:
window.addEventListener("load", colorSquares);
Though this may not be the ideal solution since it will also trigger when something else triggers the onload event.
Answering your question directly. To know if an element is added to a directive or to the DOM in general, you can simply put a directive on that element, since the directive will run only when the element on which it "sits" is already in the DOM.
Using part of your code as an example:
myApp.directive('myDirective', function () {
return {
...
//put custom directive that will notify when DOM is ready
template: '<div><div ng-repeat-ready ng-repeat="s in squares" class="square"></div></div>'
};
});
And here is the custom ng-repeat-ready directive:
myApp.directive('ngRepeatReady', function () {
return {
link: function (scope) {
if (scope.$last) {
//do notification stuff here
//for example $emit an event
scope.$emit('ng-repeat is ready');
}
}
}
});
This directive will run when the element on which is sits is already in the DOM and check if the element has $last property on the scope (ng-repeat sets this flag for the last element of the iterated object) which means that the ng-repeat directive is done and you can now operate on the DOM safely.

How to make this call in an angular scenario?

I'm using a youtube player called YTPlayer.
https://github.com/pupunzi/jquery.mb.YTPlayer
In this code he makes a JQuery call which works fine.
$(document).ready(function () {
$(".player").mb_YTPlayer();
});
How can i make such a call from my controller without using JQuery?
Thanks.
You create a directive. You can think of directives as extending html.
Your directive will look something like this:
.directive('ytPlayer', function() {
return {
scope: {
pathToVideo: '&'
},
link(scope, element, attr) {
//at this point, the DOM is ready and the element has been added to the page. It's safe to call mb_YTPlayer() here.
//also, element is already a jQuery object, so you don't need to wrap it in $()
element.mb_YTPlayer();
//scope.pathToVideo() will return '/video.mpg' here
}
}
}
And you'll add it to your page with this markup:
<yt-player path-to-video="/video.mpg"></yt-player>
It's OK to use jQuery inside of a directive if your video player is dependent on it. You should never need to use jQuery in an angular controller. If you find yourself doing so, you're not "thinking angular".
Many times, video players and other components require specific markup to work, so you can customize your template for the directive with the template property:
.directive('ytPlayer', function() {
return {
scope: {
pathToVideo: '&'
},
replace: true,
template: '<div><span></span></div>'
link(scope, element, attr) {
element.mb_YTPlayer();
//scope.pathToVideo() will return '/video.mpg' here
}
}
}
These two lines:
replace: true,
template: '<div><span></span></div>'
will cause angular to replace the yt-player markup with the markup in the template property.

Custom directive with watcher only fires once. Does not watch

I'm attempting to fire an animation using a custom directive, "activate" which I use as an attribute here, partials/test.html
<div activate="{{cardTapped}}" >
I define the directive following my app definition in js/app.js
myApp.directive('activate', function ($animate) {
return function(scope, element, attrs) {
scope.$watch(attrs.activate,function(newValue){
console.log('fire');
if(newValue){
$animate.addClass(element, "full");
}
else{
$animate.removeClass(element, "full");
}
},true);
};
});
However, $watch is only firing on page load. When cardTapped changes values, nothing registers. I've tried several variations of parameters here to no avail and I've seen a dozen questions similar to this but so far I havent found a solution
Any thoughts?
The problem is that you wrote it like this: activate="{{cardTapped}}" while it should be activate="cardTapped".
When you want to use a watcher, let it watch a variable, not a string.
JS Fiddle

Resources