Notification when transition completes - angularjs

I use Angular 1.3.1, ngAnimate with CSS transitions. And I want to know when the CSS transition starts and completes.
I was expecting to be able to use module.animation but it does not work as I expect it (and as the docs suggest). I'm not able to get a notification when the transition completes.
I have created a Plunker: http://plnkr.co/edit/UGCmbBoiLT1xvhx7JGst.
This is my HTML code:
<body ng-controller="Controller as ctrl">
<h1>Angular Animation issue!</h1>
<div class="animation" id="resizeme" ng-class="ctrl.className"></div>
<label>
<input type="checkbox" ng-model="ctrl.className"
ng-true-value="'big'" ng-false-value="''">
Bigger!
</label>
This is the CSS code:
#resizeme {
width: 100px;
height: 100px;
background-color: red;
-webkit-transition: width linear 1s;
transition: width linear 1s;
}
#resizeme.big {
width: 200px;
}
And this is the JavaScript code:
var module = angular.module('app', ['ngAnimate']);
module.controller('Controller', function() {
this.className = '';
});
module.animation('.animation', function() {
return {
addClass: function(elemennt, className) {
console.log('addClass animation starts');
return function(cancel) {
console.log('addClass animation completes')
}
},
removeClass: function(elemennt, className) {
console.log('removeClass animation starts');
return function(cancel) {
console.log('removeClass animation completes')
}
},
}
});
Checking the checkbox runs the "addClass" animation. I do get the "addClass animation starts" message in the console, but I don't get the "addClass animation completes" message when the transition is complete.
The Javascript-defined Animations section of ngAnimate docs says the following for the function returned by enter, addClass and friends:
//this (optional) function will be called when the animation
//completes or when the animation is cancelled (the cancelled
//flag will be set to true if cancelled).
So I'd expect the function that outputs "addClass animation completes" to be called when the transition completes. But it's not called.
Does anyone know how I can get a notification when the transition completes?
PS: I know I could use the $animate service and the promise returned by $animate.addClass. But I'm trying to use ng-class (and other animation-compatible Angular directives), so I don't have access to the promise provided by the $animate service.

Related

Angular $animate misunderstanding

There's something I'm missing in AngularJS $animate. I have no idea if my error is trivial or a massive misunderstanding. I tried to cut this down to the minimum example but it's still quite large. At least, however, it should run out of the box.
<!doctype html>
<html data-ng-app="MyModule">
<head>
<style type="text/css">
.msg.ng-leave {
transition: all 1s;
opacity: 1;
}
.msg.ng-leave.ng-leave-active {
opacity: 0;
}
.fade.ng-enter {
transition: opacity 1s;
opacity: 0;
}
.fade.ng-enter.ng-enter-active {
opacity: 1;
}
</style>
</head>
<body data-ng-controller="MyController">
<div class="navpanel">
<p class="msg"><strong>This is a message, make it go away</strong></p>
<h3>Menu</h3>
<ul id="menulist">
<li>Do This</li>
<li>Do That</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.min.js"></script>
<script>
angular.module('MyModule', ['ngAnimate'])
.controller('MyController', ['$scope', '$animate', function($scope, $animate) {
let npanel = angular.element(document.querySelector('.navpanel'));
let msg = angular.element(document.querySelector('.msg'));
let menulist = angular.element(document.getElementById('menulist'));
let counter = 0;
npanel.on('click', function() {
$animate.leave(msg);
let newpara = angular.element(document.createElement('p'));
newpara.html('new paragraph ' + (++counter)).addClass('fade');
let children = npanel.children();
$animate.enter(angular.element(newpara), npanel, menulist);
});
}]);
</script>
</body>
</html>
It's intended to do two main things on clicking. One, it should remove the message, two, it should insert a new paragraph. It does the second (using $animate.enter) but the first fails completely (using $animate.leave).
$animate.leave seems, from the documentation, to be supposed to run through any animation, then remove the item from the DOM, but my code does none of that. The $animate.enter by contrast, does not succeed in performing any animations, but does manage to insert the new item (which seems to imply that $animate has been properly injected).
What am I missing?
on is a jqLite/jQuery method and will not trigger AngularJS' digest cycle. This means that when you click on the element the attached event listener will execute, the DOM operations will be performed, but since the digest cycle doesn't start AngularJS' animation hooks will not handle the animation.
You can use $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.
For example:
npanel.on('click', function() {
$scope.$apply(function() {
$animate.leave(msg);
let newpara = angular.element(document.createElement('p'));
newpara.html('new paragraph ' + (++counter)).addClass('fade');
let children = npanel.children();
$animate.enter(angular.element(newpara), npanel, menulist);
});
});

Smoothly Transitioning Between ReactJS Prop Changes

There are two buttons that toggle the layout of an item. When either button is clicked, I'd like the item to fade out, change, then fade back in.
Using ReactJS, I'm running into two problems:
Using componentDidUpdate to trigger the "fade back in" event causes a loop; changing the state re-triggers componentDidUpdate endlessly.
Using componentWillReceiveProps allows me to update the class on the element to have it fade out, but it also immediately changes the layout. I need to delay the change until it's invisible.
Thoughts? Have I constructed this wrong?
(The code is below, but something is broken in Stack's version that works in JSFiddle: https://jsfiddle.net/nathanziarek/69z2wepo/15009/)
var Hello = React.createClass({
getInitialState: function() {
return { visible: " x-hidden" };
},
render: function() {
return <div className={"hello" + this.state.visible}>Hello {this.props.name}</div>;
},
componentDidMount: function(){
this.setState({ visible: "" })
},
componentWillReceiveProps: function(){
// The item is changing, make it invisible
// Wait until it's done being invisible before changing
// anything?
this.setState({ visible: " x-hidden" })
},
componentDidUpdate: function(){
// The item has changed, make it visible
// Setting anything here causes this function
// to get called again, creating a loop
// this.setState({ visible: "" })
}
});
var Button = React.createClass({
render: function() {
return <button onClick={this.handleClick}>{this.props.label}</button>;
},
handleClick: function() {
React.render(
<span>
<Button label="Universe"/>
<Button label="World"/>
<Hello name={this.props.label} />
</span>,
document.getElementById('container')
);
}
});
React.render(
<span>
<Button label="Universe"/>
<Button label="World"/>
<Hello name="_______" />
</span>,
document.getElementById('container')
);
.hello {
opacity: 1;
transition: 300ms opacity linear;
-webkit-transition: 300ms opacity linear;
}
.x-hidden { opacity: 0 }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.1/JSXTransformer.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.1/react-with-addons.js"></script>
<script src="https://facebook.github.io/react/js/jsfiddle-integration.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>
I would look into using React's CSSTransitionGroup add-on component. You'll have to do some restructuring, but this will give you the behavior you want without having to add a bunch of logic around setting CSS classes; React will take care of animating (in your case, fading) components that are entering/leaving the DOM.
Unrelated: I would also avoid re-rendering your entire component like how you're doing it in your button click event. This breaks the flow of React and will inevitably cause problems. Prefer changing state and pushing down new props.
Easiest way to do this is to have both on screen then to do the fade entirely with css, and essentially completely omit React from the process other than to place the appropriate classes.
If you're fading between them you need both sets of contents anyway. That, in turn, means there's no reason to not have these in the document, and that in turn means it's just CSS hide/show tomfoolery, and there's no need for any kind of React lifecycle stuff.
You're overcomplicating it by trying to have React handle way too much.
Supporting CSS:
#foo span {
opacity: 0;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
transition: all 0.5s ease;
}
#foo span.show {
opacity: 1;
}
Then the relevant React:
var Foo = react.createComponent({
render: function() {
var showKey = this.props.showKey,
Things = {
a: 'Apple',
b: 'Bear',
c: 'Cold inevitable doom'
};
return (
<div id="foo">
{ Things.map(X,k) {
return <span class={(showKey === k)? 'show':undefined}>{X}</span>
}}
</div>
);
}
});
You'll find that as you switch that control's showKey, it will fade in and out its relevant contents as appropriate.

CSS Transition not showing with directive

I'm playing with transitions and directives. I've created a Card directive that should show a clone of it self in fullscreen when clicked. The transition doesn't happen if I don't apply the altering css class in a timeout. Is that how it should be done?
<div ng-app='trans'>
<div data-card class='card'>timeout</div>
<div data-card='notimeout' class='card'>not timeout</div>
</div>
Between to original position and the fullscreen mode it should transition with a spin. The goto class is just so that i can add/remove transitions so that the card doesn't transition widht/height when the window is resized. I think it reads nice too =)
.card {
width:10vh;
height:14vh;
background-color:pink;
margin: 10px;
}
.card.goto.fullscreen {
transition: all 0.6s linear;
}
.card.fullscreen {
height:95vh;
width: 68vh;
position:absolut;
position: absolute;
top: 50% !important;
left: 50% !important;
margin: 0;
transform: translate(-50%, -50%) rotateY(360deg);
}
This is a simplified version of my directive.
var app = angular.module('trans', []);
app.directive('card', ['$document', '$timeout', function ($document, $timeout) {
return {
restrict: 'A',
link: link,
scope: {}
};
function link(scope, element, attrs) {
var clone;
element.on('click', function () {
if (clone) {
clone.off().remove();
}
clone = element.clone();
var spec = getCardSpecifications();
clone.css({
'margin': '0',
'top': spec.top + 'px',
'left': spec.left + 'px',
'position': 'absolute'
});
$document.find('body').append(clone);
clone.addClass('goto');
if (attrs.card == 'notimeout') {
clone.addClass('fullscreen');
} else {
$timeout(function () {
clone.addClass('fullscreen');
}, 0);
}
});
function getCardSpecifications() {
var spec = {};
spec.top = element.prop('offsetTop');
spec.left = element.prop('offsetLeft');
spec.height = element[0].offsetHeight;
spec.width = element[0].offsetWidth;
return spec;
}
}
}]);
I've created this jsfiddle that demonstrates the problem.
The problem doesn't have anything to do with Angular itself, but with creating a new DOM node and setting a class on it right after. Such a problem is described e.g. here, and it uses the same solution as yours in the first example.
DISCLAIMER: The real Angular way of doing this would be ngAnimate. What follows is a solution that is almost the same as the OP's, and one you'd only want to use if you don't want to depend on that module – but it's only ~11kb uncompressed, and 4kb gzipped. Choose wisely!
What also worked for me is waiting for the DOM node to be ready:
clone.ready(function() {
clone.addClass('fullscreen');
});
This amounts to almost the same thing as using a 0ms timeout, but is a. more descriptive and b. works in all cases, while the timeout solution apparently sometimes fails in Firefox (see linked article).
The second solution given in the article also reads a little more hackish (matter of opinion, really), and you'll have to retrieve the actual DOM element instead of the jqLite wrapper around it to use it.
Why exactly this happens, even though you are adding the class "after appending", I wasn't able to quickly find out. Perhaps appendChild, which append most likely uses internall, is asynchronous (i.e. pushes the DOM manipulation task onto the event queue)? Some more googling might be useful if you're really interested in the cause of this problem.
You should probably use animate to do an animation
$animate.addClass(clone, 'fullscreen'
I had issues trying to get the dependency for animate in fiddle so
I made a Plunker
When changing the DOM via the directive with methods like css, you have to inform the digest loop of these changes.
In order to accomplish that you should add scope.$apply() after adding the css class inside your if statement. The reason why $timeout works for you is that because it calls $apply after the timeout executes.

Angular animate not working correctly on first run when element is initially hidden

I am wiring up a status bar on my angular app, the purpose is when a request is made to the server the bar will show the response message, will have a background colour to denote success or error, and if it was successful to hide after a few seconds.
What I am seeing is that the first time this logic is run through after loading the page the animations are not run (both the fade in and timed fadeout fail to run), but only if the status bar element is initially hidden, if I set the ng-show variable to true on page load all the animations work as expected.
I did inspect the source via chrome's developer tools and during this first run the div has these classes alert-bar ng-hide-remove ng-hide-remove-active alert-bar-success ng-animate ng-hide after the animation should have been finished. When the animation does work the only classes present are alert-bar alert-bar-success ng-animate ng-hide.
The HTML:
<div class="alert-bar" ng-show="response.show" ng-class="(response.result == true) ? 'alert-bar-success' : 'alert-bar-danger'">
<div class="container">
<label>Message: {{response.message}}</label>
</div>
</div>
The CSS:
.alert-bar {
width: 100%;
margin-top: -20px;
margin-bottom: 20px;
}
.alert-bar-success {
background-color: #5cb85c;
border-color: #4cae4c;
color: #ffffff;
}
.alert-bar-danger {
color: #ffffff;
background-color: #d9534f;
border-color: #d43f3a;
}
.alert-bar.ng-hide-add, .alert-bar.ng-hide-remove {
-webkit-transition:all linear 0.3s;
-moz-transition:all linear 0.3s;
-o-transition:all linear 0.3s;
transition:all linear 0.3s;
display:block!important;
}
.alert-bar.ng-hide-add.ng-hide-add-active,
.alert-bar.ng-hide-remove {
opacity:0;
}
.alert-bar.ng-hide-add,
.alert-bar.ng-hide-remove.ng-hide-remove-active {
opacity:1;
}
The Controller:
controllers.controller("AppController", ['$scope', '$timeout', function($scope, $timeout) {
$scope.response = {};
$scope.response.received = function(message, result) {
$scope.response.message = message;
$scope.response.result = result;
$scope.response.show = true;
if (result == true) {
$timeout(function () {
$scope.response.show = false;
}, 4000);
}
};
}]);
This happens due to interplay between the animation code applied for both the ng-class and ng-hide directives. The only way I have made things like this work is to use separate <div> elements which are shown/hidden conditionally but that have static classes.
Here is a plunkr that demonstrates splitting the above into two <div> elements to create a working example for the problem in the question:
http://plnkr.co/edit/PFNGkOLKvs8Gx9h1T6Yk?p=preview
Alternatively, you could forego the use of ng-hide/ng-show entirely and use ng-class exclusively.
Edit: see http://plnkr.co/edit/JiLPc6cqiLHR21c64cCy?p=preview for a version that uses ng-class exclusively.
You don't need to use two divs. I guess the simplest solution is just putting animate css into class "ng-hide-add-active" only, not into "ng-hide-add". Like below:
.page-ready-animate.ng-hide-add-active {
-webkit-animation: 0.5s fadeOutDown;
animation: 0.5s fadeOutDown;
}

Can ngAnimate be used without defining CSS3 classes or JavaScript?

Is it possible to use ngAnimate without any CSS3 or JavaScript? Let's say if you simply need to toggle the opacity, can you just do it in the markup?
<div ng-show='foo == 'yes'' ng-animate="show: 'opacity:1', hide: 'opacity:0'" >
</div>
Animation in the browser either needs to happen by (1) letting the browser's rendering engine handle it via CSS, or (2) controlling it yourself with JavaScript. So, somewhere, one of these two things needs to happen.
That said, you could build your own directive that builds the correct CSS and/or JavaScript on the fly and attaches/applies it to the given element, but I believe that using ngAnimate as provided is probably easier.
An example for those coming into ngAnimate for the first time:
HTML:
<div ng-show="foo == 'yes'" ng-animate="{show: 'fade'}"></div>
CSS:
.enter-fade {
-webkit-transition: 1s linear opacity;
-moz-transition: 1s linear opacity;
-o-transition: 1s linear opacity;
transition: 1s linear opacity;
opacity: 0;
}
.enter-fade.enter-fade-active {
opacity: 1;
}
Or, if you're supporting browsers that don't support CSS3 transitions, you can do the transition with JavaScript instead:
myModule.animation('fade', function() {
return {
setup : function(element) {
element.css({'opacity': 0});
},
start : function(element, done, memo) {
element.animate({'opacity': 1}, function() {
done();
});
}
};
});
You can find more information at these great Yearofmoo articles:
Animation in AngularJS
Enhanced Animation in AngularJS

Resources