feat(ngAnimate): complete rewrite of animations

- ngAnimate directive is gone and was replaced with class based animations/transitions
- support for triggering animations on css class additions and removals
- done callback was added to all animation apis
- $animation and $animator where merged into a single $animate service with api:
  - $animate.enter(element, parent, after, done);
  - $animate.leave(element, done);
  - $animate.move(element, parent, after, done);
  - $animate.addClass(element, className, done);
  - $animate.removeClass(element, className, done);

BREAKING CHANGE: too many things changed, we'll write up a separate doc with migration instructions
This commit is contained in:
Matias Niemelä 2013-06-18 13:59:57 -04:00 committed by Misko Hevery
parent 11521a4cde
commit 81923f1e41
40 changed files with 3014 additions and 2244 deletions

View file

@ -160,6 +160,10 @@ module.exports = function(grunt) {
dest: 'build/angular-resource.js',
src: util.wrap(['src/ngResource/resource.js'], 'module')
},
animate: {
dest: 'build/angular-animate.js',
src: util.wrap(['src/ngAnimate/animate.js'], 'module')
},
route: {
dest: 'build/angular-route.js',
src: util.wrap([
@ -178,6 +182,7 @@ module.exports = function(grunt) {
min: {
angular: 'build/angular.js',
animate: 'build/angular-animate.js',
cookies: 'build/angular-cookies.js',
loader: 'build/angular-loader.js',
mobile: 'build/angular-mobile.js',

5
angularFiles.js vendored
View file

@ -10,8 +10,7 @@ angularFiles = {
'src/auto/injector.js',
'src/ng/anchorScroll.js',
'src/ng/animation.js',
'src/ng/animator.js',
'src/ng/animate.js',
'src/ng/browser.js',
'src/ng/cacheFactory.js',
'src/ng/compile.js',
@ -66,6 +65,7 @@ angularFiles = {
],
'angularSrcModules': [
'src/ngAnimate/animate.js',
'src/ngCookies/cookies.js',
'src/ngResource/resource.js',
'src/ngRoute/routeUtils.js',
@ -107,6 +107,7 @@ angularFiles = {
'test/*.js',
'test/auto/*.js',
'test/ng/**/*.js',
'test/ngAnimate/*.js',
'test/ngCookies/*.js',
'test/ngResource/*.js',
'test/ngRoute/**/*.js',

View file

@ -8,3 +8,7 @@
ng\:form {
display: block;
}
.ng-hide {
display: none;
}

View file

@ -67,41 +67,33 @@ describe('Docs Annotations', function() {
var $scope, parent, element, url, window;
beforeEach(function() {
module(function($provide, $animationProvider) {
module(function($provide, $animateProvider) {
$provide.value('$window', window = angular.mock.createMockWindow());
$animationProvider.register('foldout-enter', function($window) {
$animateProvider.register('.foldout', function($window) {
return {
start : function(element, done) {
enter : function(element, done) {
$window.setTimeout(done, 1000);
}
}
});
$animationProvider.register('foldout-hide', function($window) {
return {
start : function(element, done) {
},
show : function(element, done) {
$window.setTimeout(done, 500);
}
}
});
$animationProvider.register('foldout-show', function($window) {
return {
start : function(element, done) {
},
hide : function(element, done) {
$window.setTimeout(done, 200);
}
}
});
});
inject(function($rootScope, $compile, $templateCache, $rootElement, $animator) {
$animator.enabled(true);
inject(function($rootScope, $compile, $templateCache, $rootElement, $animate) {
$animate.enabled(true);
url = '/page.html';
$scope = $rootScope.$new();
parent = angular.element('<div class="parent"></div>');
element = angular.element('<div data-url="' + url + '" foldout></div>');
//we're injecting the element to the $rootElement since the changes in
//$animator only detect and perform animations if the root element has
//$animate only detect and perform animations if the root element has
//animations enabled. If the element is not apart of the DOM
//then animations are skipped.
element = angular.element('<div data-url="' + url + '" class="foldout" foldout></div>');
parent.append(element);
$rootElement.append(parent);
body.append($rootElement);
@ -142,16 +134,19 @@ describe('Docs Annotations', function() {
$httpBackend.flush();
window.setTimeout.expect(1).process();
window.setTimeout.expect(1000).process();
window.setTimeout.expect(0).process();
//hide
element.triggerHandler('click');
window.setTimeout.expect(1).process();
window.setTimeout.expect(500).process();
window.setTimeout.expect(200).process();
window.setTimeout.expect(0).process();
//show
element.triggerHandler('click');
window.setTimeout.expect(1).process();
window.setTimeout.expect(200).process();
window.setTimeout.expect(500).process();
window.setTimeout.expect(0).process();
}));
});
@ -160,7 +155,7 @@ describe('Docs Annotations', function() {
var window, $scope, ctrl;
beforeEach(function() {
module(function($provide, $animationProvider) {
module(function($provide, $animateProvider) {
$provide.value('$window', window = angular.mock.createMockWindow());
});
inject(function($rootScope, $controller, $location, $cookies, sections) {

View file

@ -183,8 +183,8 @@ directive.ngEvalJavascript = ['getEmbeddedTemplate', function(getEmbeddedTemplat
}];
directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location', '$sniffer',
function($templateCache, $browser, docsRootScope, $location, $sniffer) {
directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location', '$sniffer', '$animate',
function($templateCache, $browser, docsRootScope, $location, $sniffer, $animate) {
return {
terminal: true,
link: function(scope, element, attrs) {
@ -193,6 +193,7 @@ directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location',
deregisterEmbedRootScope;
modules.push(['$provide', function($provide) {
$provide.value('$animate', $animate);
$provide.value('$templateCache', $templateCache);
$provide.value('$anchorScroll', angular.noop);
$provide.value('$browser', $browser);

View file

@ -335,12 +335,11 @@ directive.tabPane = function() {
};
};
directive.foldout = ['$http', '$animator','$window', function($http, $animator, $window) {
directive.foldout = ['$http', '$animate','$window', function($http, $animate, $window) {
return {
restrict: 'A',
priority : 500,
link: function(scope, element, attrs) {
var animator = $animator(scope, { ngAnimate: "'foldout'" });
var container, loading, url = attrs.url;
if(/\/build\//.test($window.location.href)) {
url = '/build/docs' + url;
@ -353,7 +352,7 @@ directive.foldout = ['$http', '$animator','$window', function($http, $animator,
loading = true;
var par = element.parent();
container = angular.element('<div class="foldout">loading...</div>');
animator.enter(container, null, par);
$animate.enter(container, null, par);
$http.get(url, { cache : true }).success(function(html) {
loading = false;
@ -367,12 +366,12 @@ directive.foldout = ['$http', '$animator','$window', function($http, $animator,
//avoid showing the element if the user has already closed it
if(container.css('display') == 'block') {
container.css('display','none');
animator.show(container);
$animate.show(container);
}
});
}
else {
container.css('display') == 'none' ? animator.show(container) : animator.hide(container);
container.hasClass('ng-hide') ? $animate.show(container) : $animate.hide(container);
}
});
});

View file

@ -134,7 +134,7 @@ exports.Example.prototype.toHtmlTabs = function() {
exports.Example.prototype.toHtmlEmbed = function() {
var out = [];
out.push('<div class="well doc-example-live animator-container"');
out.push('<div class="well doc-example-live animate-container"');
if(this.animations) {
out.push(" ng-class=\"{'animations-off':animationsOff == true}\"");
}

View file

@ -494,6 +494,19 @@ Doc.prototype = {
html_usage_parameters: function(dom) {
var self = this;
var params = this.param ? this.param : [];
if(this.animations) {
dom.h('Animations', this.animations, function(animations){
dom.html('<ul>');
var animations = animations.split("\n");
animations.forEach(function(ani) {
dom.html('<li>');
dom.text(ani);
dom.html('</li>');
});
dom.html('</ul>');
});
dom.html('<a href="api/ngAnimate.$animate">Click here</a> to learn more about the steps involved in the animation.');
}
if(params.length > 0) {
dom.html('<h2 id="parameters">Parameters</h2>');
dom.html('<table class="variables-matrix table table-bordered table-striped">');
@ -538,18 +551,6 @@ Doc.prototype = {
dom.html('</tbody>');
dom.html('</table>');
}
if(this.animations) {
dom.h('Animations', this.animations, function(animations){
dom.html('<ul>');
var animations = animations.split("\n");
animations.forEach(function(ani) {
dom.html('<li>');
dom.text(ani);
dom.html('</li>');
});
dom.html('</ul>');
});
}
},
html_usage_returns: function(dom) {
@ -665,48 +666,6 @@ Doc.prototype = {
dom.text('</' + element + '>');
});
}
if(self.animations) {
var animations = [], matches = self.animations.split("\n");
matches.forEach(function(ani) {
var name = ani.match(/^\s*(.+?)\s*-/)[1];
animations.push(name);
});
dom.html('with <span id="animations">animations</span>');
var comment;
if(animations.length == 1) {
comment = 'The ' + animations[0] + ' animation is supported';
}
else {
var rhs = animations[animations.length-1];
var lhs = '';
for(var i=0;i<animations.length-1;i++) {
if(i>0) {
lhs += ', ';
}
lhs += animations[i];
}
comment = 'The ' + lhs + ' and ' + rhs + ' animations are supported';
}
var element = self.element || 'ANY';
dom.code(function() {
dom.text('//' + comment + "\n");
dom.text('<' + element + ' ');
dom.text(dashCase(self.shortName));
renderParams('\n ', '="', '"', true);
dom.text(' ng-animate="{');
animations.forEach(function(ani, index) {
if (index) {
dom.text(', ');
}
dom.text(ani + ': \'' + ani + '-animation\'');
});
dom.text('}">\n ...\n');
dom.text('</' + element + '>');
});
dom.html('<a href="api/ng.$animator#Methods">Click here</a> to learn more about the steps involved in the animation.');
}
}
self.html_usage_directiveInfo(dom);
self.html_usage_parameters(dom);

View file

@ -1,4 +1,4 @@
.reveal {
.reveal.ng-enter {
-webkit-transition:1s linear all;
-moz-transition:1s linear all;
-o-transition:1s linear all;
@ -6,7 +6,7 @@
opacity:0;
}
.reveal.reveal-active {
.reveal.ng-enter.ng-enter-active {
opacity:1;
}
@ -15,48 +15,45 @@
overflow:hidden;
}
.slide-reveal {
.slide-reveal > .ng-enter {
-webkit-transition:0.5s linear all;
-moz-transition:0.5s linear all;
-o-transition:0.5s linear all;
transition:0.5s linear all;
opacity:0.5;
opacity:0.5;
position:relative;
opacity:0;
top:10px;
}
.slide-reveal.slide-reveal-active {
.slide-reveal > .ng-enter.ng-enter-active {
top:0;
opacity:1;
}
.expand-enter {
.expand.ng-enter,
.expand.ng-leave {
-webkit-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
-moz-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
-o-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
}
.expand.ng-enter {
opacity:0;
line-height:0;
height:0!important;
}
.expand-enter.expand-enter-active {
.expand.ng-enter.expand.ng-enter-active {
opacity:1;
line-height:20px;
height:20px!important;
}
.expand-leave {
-webkit-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
-moz-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
-o-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
.expand.ng-leave {
opacity:1;
height:20px;
}
.expand-leave.expand-leave-active {
.expand.ng-leave.expand.ng-leave-active {
opacity:0;
height:0;
}
@ -73,32 +70,36 @@
padding:1em;
}
.animator-container.animations-off * {
.animate-container.animations-off * {
-webkit-transition: none;
-moz-transition: none;
-o-transition: color 0 ease-in; /* opera is special :) */
transition: none;
}
.foldout-show, .foldout-enter, .foldout-hide {
.foldout.ng-enter,
.foldout.ng-hide-add,
.foldout.ng-hide-remove {
-webkit-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
-moz-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
-o-transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
transition:0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) all;
}
.foldout-show, .foldout-enter {
.foldout.ng-hide-remove,
.foldout.ng-enter {
opacity:0;
}
.foldout-show.foldout-show-active, .foldout-hide.foldout-hide-active {
.foldout.ng-hide-remove.ng-hide-remove-active,
.foldout.ng-enter.ng-enter-active {
opacity:1;
}
.foldout-hide {
.foldout.ng-hide-add {
opacity:1;
}
.foldout-hide.foldout-hide-active {
.foldout.ng-hide-add.ng-hide-active {
opacity:0;
}

View file

@ -43,6 +43,7 @@
addTag('script', {src: path('angular-cookies.js') }, sync);
addTag('script', {src: path('angular-sanitize.js') }, sync);
addTag('script', {src: path('angular-mobile.js') }, sync);
addTag('script', {src: path('angular-animate.js') }, sync);
addTag('script', {src: 'components/angular-bootstrap.js' }, sync);
addTag('script', {src: 'components/angular-bootstrap-prettify.js' }, sync);
addTag('script', {src: 'components/google-code-prettify.js' }, sync);
@ -201,7 +202,7 @@
</header>
<div id="docs-fold-overlay" ng-show="docs_fold" ng-click="fold(null)"></div>
<div id="docs-fold" ng-show="docs_fold" ng-animate="'fold'">
<div class="foldout" id="docs-fold" ng-show="docs_fold">
<div id="docs-fold-close" ng-click="fold(null)">
<span class="icon-remove-sign"></span>
</div>
@ -283,21 +284,21 @@
<li class="nav-header section" ng-show="module.directives">
<a href="{{URL.directive}}" class="guide">directive</a>
</li>
<li ng-repeat="page in module.directives track by page.url" ng-class="navClass(page)" ng-animate="'expand'" class="api-list-item">
<li ng-repeat="page in module.directives track by page.url" ng-class="navClass(page)" class="expand api-list-item">
<a href="{{page.url}}" tabindex="2">{{page.shortName}}</a>
</li>
<li class="nav-header section" ng-show="module.filters">
<a href="{{URL.filter}}" class="guide">filter</a>
</li>
<li ng-repeat="page in module.filters track by page.url" ng-class="navClass(page)" ng-animate="'expand'" class="api-list-item">
<li ng-repeat="page in module.filters track by page.url" ng-class="navClass(page)" class="expand api-list-item">
<a href="{{page.url}}" tabindex="2">{{page.shortName}}</a>
</li>
<li class="nav-header section" ng-show="module.services">
<a href="{{URL.service}}" class="guide">service</a>
</li>
<li ng-repeat="service in module.services track by service.instance.url" ng-animate="'expand'" ng-class="navClass(service.instance, service.provider)" class="api-list-item">
<li ng-repeat="service in module.services track by service.instance.url" ng-class="navClass(service.instance, service.provider)" class="api-list-item expand">
<a ng-show="service.provider" class="pull-right" href="{{service.provider.url}}" tabindex="2"><i class="icon-cog"></i></a>
<a href="{{service.instance.url}}" tabindex="2">{{service.name}}</a>
</li>
@ -305,7 +306,7 @@
<li class="nav-header section" ng-show="module.types">
<a href="{{URL.type}}" class="guide">Types</a>
</li>
<li ng-repeat="page in module.types track by page.url" ng-class="navClass(page)" ng-animate="'expand'" class="api-list-item">
<li ng-repeat="page in module.types track by page.url" ng-class="navClass(page)" class="expand api-list-item">
<a href="{{page.url}}" tabindex="2">{{page.shortName}}</a>
</li>
@ -334,7 +335,7 @@
<div id="loading" ng-show="loading">Loading...</div>
<div ng-hide="loading" ng-include src="currentPage.partialUrl" onload="afterPartialLoaded()" autoscroll class="content" ng-animate="{enter: 'slide-reveal'}" ></div>
<div ng-hide="loading" ng-include src="currentPage.partialUrl" onload="afterPartialLoaded()" autoscroll class="content slide-reveal"></div>
<div id="disqus" class="disqus">
<h2>Discussion</h2>

View file

@ -803,7 +803,7 @@ docsApp.controller.DocsController = function($scope, $location, $window, $cookie
};
angular.module('docsApp', ['ngResource', 'ngRoute', 'ngCookies', 'ngSanitize', 'bootstrap', 'bootstrapPrettify', 'docsData']).
angular.module('docsApp', ['ngResource', 'ngRoute', 'ngCookies', 'ngSanitize', 'ngAnimate', 'bootstrap', 'bootstrapPrettify', 'docsData']).
config(function($locationProvider) {
$locationProvider.html5Mode(true).hashPrefix('!');
}).

View file

@ -15,6 +15,7 @@ module.exports = function(config) {
'build/angular-mobile.js',
'build/angular-sanitize.js',
'build/angular-route.js',
'build/angular-animate.js',
'build/docs/components/lunr.js',
'build/docs/components/google-code-prettify.js',

View file

@ -1053,13 +1053,13 @@ function bootstrap(element, modules) {
}]);
modules.unshift('ng');
var injector = createInjector(modules);
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animator',
function(scope, element, compile, injector, animator) {
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',
function(scope, element, compile, injector, animate) {
scope.$apply(function() {
element.data('$injector', injector);
compile(element)(scope);
});
animator.enabled(true);
animate.enabled(true);
}]
);
return injector;

View file

@ -106,8 +106,7 @@ function publishExternalAPI(angular){
directive(ngEventDirectives);
$provide.provider({
$anchorScroll: $AnchorScrollProvider,
$animation: $AnimationProvider,
$animator: $AnimatorProvider,
$animate: $AnimateProvider,
$browser: $BrowserProvider,
$cacheFactory: $CacheFactoryProvider,
$controller: $ControllerProvider,

View file

@ -173,24 +173,30 @@ function setupModuleLoader(window) {
* @param {Function} animationFactory Factory function for creating new instance of an animation.
* @description
*
* Defines an animation hook that can be later used with {@link ng.directive:ngAnimate ngAnimate}
* alongside {@link ng.directive:ngAnimate#Description common ng directives} as well as custom directives.
* <pre>
* module.animation('animation-name', function($inject1, $inject2) {
* return {
* //this gets called in preparation to setup an animation
* setup : function(element) { ... },
* **NOTE**: animations are take effect only if the **ngAnimate** module is loaded.
*
* //this gets called once the animation is run
* start : function(element, done, memo) { ... }
*
* Defines an animation hook that can be later used with {@link ngAnimate.$animate $animate} service and
* directives that use this service.
*
* <pre>
* module.animation('.animation-name', function($inject1, $inject2) {
* return {
* eventName : function(element, done) {
* //code to run the animation
* //once complete, then run done()
* return function cancellationFunction(element) {
* //code to cancel the animation
* }
* }
* }
* })
* </pre>
*
* See {@link ng.$animationProvider#register $animationProvider.register()} and
* {@link ng.directive:ngAnimate ngAnimate} for more information.
* See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and
* {@link ngAnimate ngAnimate module} for more information.
*/
animation: invokeLater('$animationProvider', 'register'),
animation: invokeLater('$animateProvider', 'register'),
/**
* @ngdoc method

112
src/ng/animate.js Normal file
View file

@ -0,0 +1,112 @@
'use strict';
/**
* @ngdoc object
* @name ng.$animateProvider
*
* @description
* Default implementation of $animate that doesn't perform any animations, instead just synchronously performs DOM
* updates and calls done() callbacks.
*
* In order to enable animations the ngAnimate module has to be loaded.
*
* To see the functional implementation check out src/ngAnimate/animate.js
*/
var $AnimateProvider = ['$provide', function($provide) {
this.$$selectors = [];
/**
* @ngdoc function
* @name ng.$animateProvider#register
* @methodOf ng.$animateProvider
*
* @description
* Registers a new injectable animation factory function. The factory function produces the animation object which
* contains callback functions for each event that is expected to be animated.
*
* * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` must be called once the
* element animation is complete. If a function is returned then the animation service will use this function to
* cancel the animation whenever a cancel event is triggered.
*
*
*<pre>
* return {
* eventFn : function(element, done) {
* //code to run the animation
* //once complete, then run done()
* return function cancellationFunction() {
* //code to cancel the animation
* }
* }
* }
*</pre>
*
* @param {string} name The name of the animation.
* @param {function} factory The factory function that will be executed to return the animation object.
*/
this.register = function(name, factory) {
var classes = name.substr(1).split('.');
name += '-animation';
this.$$selectors.push({
selectors : classes,
name : name
});
$provide.factory(name, factory);
};
this.$get = function() {
return {
enter : function(element, parent, after, done) {
var afterNode = after && after[after.length - 1];
var parentNode = parent && parent[0] || afterNode && afterNode.parentNode;
// IE does not like undefined so we have to pass null.
var afterNextSibling = (afterNode && afterNode.nextSibling) || null;
forEach(element, function(node) {
parentNode.insertBefore(node, afterNextSibling);
});
(done || noop)();
},
leave : function(element, done) {
element.remove();
(done || noop)();
},
move : function(element, parent, after, done) {
// Do not remove element before insert. Removing will cause data associated with the
// element to be dropped. Insert will implicitly do the remove.
this.enter(element, parent, after, done);
},
show : function(element, done) {
element.removeClass('ng-hide');
(done || noop)();
},
hide : function(element, done) {
element.addClass('ng-hide');
(done || noop)();
},
addClass : function(element, className, done) {
className = isString(className) ?
className :
isArray(className) ? className.join(' ') : '';
element.addClass(className);
(done || noop)();
},
removeClass : function(element, className, done) {
className = isString(className) ?
className :
isArray(className) ? className.join(' ') : '';
element.removeClass(className);
(done || noop)();
},
enabled : noop
};
};
}];

View file

@ -1,61 +0,0 @@
/**
* @ngdoc object
* @name ng.$animationProvider
* @description
*
* The $AnimationProvider provider allows developers to register and access custom JavaScript animations directly inside
* of a module.
*
*/
$AnimationProvider.$inject = ['$provide'];
function $AnimationProvider($provide) {
var suffix = 'Animation';
/**
* @ngdoc function
* @name ng.$animation#register
* @methodOf ng.$animationProvider
*
* @description
* Registers a new injectable animation factory function. The factory function produces the animation object which
* has these two properties:
*
* * `setup`: `function(Element):*` A function which receives the starting state of the element. The purpose
* of this function is to get the element ready for animation. Optionally the function returns an memento which
* is passed to the `start` function.
* * `start`: `function(Element, doneFunction, *)` The element to animate, the `doneFunction` to be called on
* element animation completion, and an optional memento from the `setup` function.
*
* @param {string} name The name of the animation.
* @param {function} factory The factory function that will be executed to return the animation object.
*
*/
this.register = function(name, factory) {
$provide.factory(camelCase(name) + suffix, factory);
};
this.$get = ['$injector', function($injector) {
/**
* @ngdoc function
* @name ng.$animation
* @function
*
* @description
* The $animation service is used to retrieve any defined animation functions. When executed, the $animation service
* will return a object that contains the setup and start functions that were defined for the animation.
*
* @param {String} name Name of the animation function to retrieve. Animation functions are registered and stored
* inside of the AngularJS DI so a call to $animate('custom') is the same as injecting `customAnimation`
* via dependency injection.
* @return {Object} the animation object which contains the `setup` and `start` functions that perform the animation.
*/
return function $animation(name) {
if (name) {
var animationName = camelCase(name) + suffix;
if ($injector.has(animationName)) {
return $injector.get(animationName);
}
}
};
}];
}

View file

@ -1,446 +0,0 @@
'use strict';
// NOTE: this is a pseudo directive.
/**
* @ngdoc directive
* @name ng.directive:ngAnimate
*
* @description
* The `ngAnimate` directive works as an attribute that is attached alongside pre-existing directives.
* It effects how the directive will perform DOM manipulation. This allows for complex animations to take place
* without burdening the directive which uses the animation with animation details. The built in directives
* `ngRepeat`, `ngInclude`, `ngSwitch`, `ngShow`, `ngHide` and `ngView` already accept `ngAnimate` directive.
* Custom directives can take advantage of animation through {@link ng.$animator $animator service}.
*
* Below is a more detailed breakdown of the supported callback events provided by pre-exisitng ng directives:
*
* | Directive | Supported Animations |
* |---------------------------------------------------------- |----------------------------------------------------|
* | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move |
* | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
* | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
* | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
* | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
* | {@link ng.directive:ngShow#animations ngShow & ngHide} | show and hide |
*
* You can find out more information about animations upon visiting each directive page.
*
* Below is an example of a directive that makes use of the ngAnimate attribute:
*
* <pre>
* <!-- you can also use data-ng-animate, ng:animate or x-ng-animate as well -->
* <ANY ng-directive ng-animate="{event1: 'animation-name', event2: 'animation-name-2'}"></ANY>
*
* <!-- you can also use a short hand -->
* //!annotate="animation" ngAnimate|This *expands* to `{ enter: 'animation-enter', leave: 'animation-leave', ...}`</strong>
* <ANY ng-directive ng-animate=" 'animation' "></ANY>
*
* <!-- keep in mind that ng-animate can take expressions -->
* //!annotate="computeCurrentAnimation\(\)" Scope Function|This will be called each time the scope changes...
* <ANY ng-directive ng-animate=" computeCurrentAnimation() "></ANY>
* </pre>
*
* The `event1` and `event2` attributes refer to the animation events specific to the directive that has been assigned.
*
* Keep in mind that if an animation is running, no child element of such animation can also be animated.
*
* <h2>CSS-defined Animations</h2>
* By default, ngAnimate attaches two CSS classes per animation event to the DOM element to achieve the animation.
* It is up to you, the developer, to ensure that the animations take place using cross-browser CSS3 transitions as
* well as CSS animations.
*
* The following code below demonstrates how to perform animations using **CSS transitions** with ngAnimate:
*
* <pre>
* <style type="text/css">
* /&#42;
* The animate-enter CSS class is the event name that you
* have provided within the ngAnimate attribute.
* &#42;/
* .animate-enter {
* -webkit-transition: 1s linear all; /&#42; Safari/Chrome &#42;/
* -moz-transition: 1s linear all; /&#42; Firefox &#42;/
* -o-transition: 1s linear all; /&#42; Opera &#42;/
* transition: 1s linear all; /&#42; IE10+ and Future Browsers &#42;/
*
* /&#42; The animation preparation code &#42;/
* opacity: 0;
* }
*
* /&#42;
* Keep in mind that you want to combine both CSS
* classes together to avoid any CSS-specificity
* conflicts
* &#42;/
* .animate-enter.animate-enter-active {
* /&#42; The animation code itself &#42;/
* opacity: 1;
* }
* </style>
*
* <div ng-directive ng-animate="{enter: 'animate-enter'}"></div>
* </pre>
*
* The following code below demonstrates how to perform animations using **CSS animations** with ngAnimate:
*
* <pre>
* <style type="text/css">
* .animate-enter {
* -webkit-animation: enter_sequence 1s linear; /&#42; Safari/Chrome &#42;/
* -moz-animation: enter_sequence 1s linear; /&#42; Firefox &#42;/
* -o-animation: enter_sequence 1s linear; /&#42; Opera &#42;/
* animation: enter_sequence 1s linear; /&#42; IE10+ and Future Browsers &#42;/
* }
* &#64-webkit-keyframes enter_sequence {
* from { opacity:0; }
* to { opacity:1; }
* }
* &#64-moz-keyframes enter_sequence {
* from { opacity:0; }
* to { opacity:1; }
* }
* &#64-o-keyframes enter_sequence {
* from { opacity:0; }
* to { opacity:1; }
* }
* &#64keyframes enter_sequence {
* from { opacity:0; }
* to { opacity:1; }
* }
* </style>
*
* <div ng-directive ng-animate="{enter: 'animate-enter'}"></div>
* </pre>
*
* ngAnimate will first examine any CSS animation code and then fallback to using CSS transitions.
*
* Upon DOM mutation, the event class is added first, then the browser is allowed to reflow the content and then,
* the active class is added to trigger the animation. The ngAnimate directive will automatically extract the duration
* of the animation to determine when the animation ends. Once the animation is over then both CSS classes will be
* removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end
* immediately resulting in a DOM element that is at it's final state. This final state is when the DOM element
* has no CSS transition/animation classes surrounding it.
*
* <h2>JavaScript-defined Animations</h2>
* In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations to browsers that do not
* yet support them, then you can make use of JavaScript animations defined inside of your AngularJS module.
*
* <pre>
* var ngModule = angular.module('YourApp', []);
* ngModule.animation('animate-enter', function() {
* return {
* setup : function(element) {
* //prepare the element for animation
* element.css({ 'opacity': 0 });
* var memo = "..."; //this value is passed to the start function
* return memo;
* },
* start : function(element, done, memo) {
* //start the animation
* element.animate({
* 'opacity' : 1
* }, function() {
* //call when the animation is complete
* done()
* });
* }
* }
* });
* </pre>
*
* As you can see, the JavaScript code follows a similar template to the CSS3 animations. Once defined, the animation
* can be used in the same way with the ngAnimate attribute. Keep in mind that, when using JavaScript-enabled
* animations, ngAnimate will also add in the same CSS classes that CSS-enabled animations do (even if you're not using
* CSS animations) to animated the element, but it will not attempt to find any CSS3 transition or animation duration/delay values.
* It will instead close off the animation once the provided done function is executed. So it's important that you
* make sure your animations remember to fire off the done function once the animations are complete.
*
* @param {expression} ngAnimate Used to configure the DOM manipulation animations.
*
*/
var $AnimatorProvider = function() {
var NG_ANIMATE_CONTROLLER = '$ngAnimateController';
var rootAnimateController = {running:true};
this.$get = ['$animation', '$window', '$sniffer', '$rootElement', '$rootScope',
function($animation, $window, $sniffer, $rootElement, $rootScope) {
$rootElement.data(NG_ANIMATE_CONTROLLER, rootAnimateController);
/**
* @ngdoc function
* @name ng.$animator
* @function
*
* @description
* The $animator.create service provides the DOM manipulation API which is decorated with animations.
*
* @param {Scope} scope the scope for the ng-animate.
* @param {Attributes} attr the attributes object which contains the ngAnimate key / value pair. (The attributes are
* passed into the linking function of the directive using the `$animator`.)
* @return {object} the animator object which contains the enter, leave, move, show, hide and animate methods.
*/
var AnimatorService = function(scope, attrs) {
var animator = {};
/**
* @ngdoc function
* @name ng.animator#enter
* @methodOf ng.$animator
* @function
*
* @description
* Injects the element object into the DOM (inside of the parent element) and then runs the enter animation.
*
* @param {jQuery/jqLite element} element the element that will be the focus of the enter animation
* @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the enter animation
* @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the enter animation
*/
animator.enter = animateActionFactory('enter', insert, noop);
/**
* @ngdoc function
* @name ng.animator#leave
* @methodOf ng.$animator
* @function
*
* @description
* Runs the leave animation operation and, upon completion, removes the element from the DOM.
*
* @param {jQuery/jqLite element} element the element that will be the focus of the leave animation
* @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the leave animation
*/
animator.leave = animateActionFactory('leave', noop, remove);
/**
* @ngdoc function
* @name ng.animator#move
* @methodOf ng.$animator
* @function
*
* @description
* Fires the move DOM operation. Just before the animation starts, the animator will either append it into the parent container or
* add the element directly after the after element if present. Then the move animation will be run.
*
* @param {jQuery/jqLite element} element the element that will be the focus of the move animation
* @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the move animation
* @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the move animation
*/
animator.move = animateActionFactory('move', move, noop);
/**
* @ngdoc function
* @name ng.animator#show
* @methodOf ng.$animator
* @function
*
* @description
* Reveals the element by setting the CSS property `display` to `block` and then starts the show animation directly after.
*
* @param {jQuery/jqLite element} element the element that will be rendered visible or hidden
*/
animator.show = animateActionFactory('show', show, noop);
/**
* @ngdoc function
* @name ng.animator#hide
* @methodOf ng.$animator
*
* @description
* Starts the hide animation first and sets the CSS `display` property to `none` upon completion.
*
* @param {jQuery/jqLite element} element the element that will be rendered visible or hidden
*/
animator.hide = animateActionFactory('hide', noop, hide);
/**
* @ngdoc function
* @name ng.animator#animate
* @methodOf ng.$animator
*
* @description
* Triggers a custom animation event to be executed on the given element
*
* @param {string} event the name of the custom event
* @param {jQuery/jqLite element} element the element that will be animated
*/
animator.animate = function(event, element) {
animateActionFactory(event, noop, noop)(element);
}
return animator;
function animateActionFactory(type, beforeFn, afterFn) {
return function(element, parent, after) {
var ngAnimateValue = scope.$eval(attrs.ngAnimate);
var className = ngAnimateValue
? isObject(ngAnimateValue) ? ngAnimateValue[type] : ngAnimateValue + '-' + type
: '';
var animationPolyfill = $animation(className);
var polyfillSetup = animationPolyfill && animationPolyfill.setup;
var polyfillStart = animationPolyfill && animationPolyfill.start;
var polyfillCancel = animationPolyfill && animationPolyfill.cancel;
if (!className) {
beforeFn(element, parent, after);
afterFn(element, parent, after);
} else {
var activeClassName = className + '-active';
if (!parent) {
parent = after ? after.parent() : element.parent();
}
var disabledAnimation = { running : true };
if ((!$sniffer.transitions && !polyfillSetup && !polyfillStart) ||
(parent.inheritedData(NG_ANIMATE_CONTROLLER) || disabledAnimation).running) {
beforeFn(element, parent, after);
afterFn(element, parent, after);
return;
}
var animationData = element.data(NG_ANIMATE_CONTROLLER) || {};
if(animationData.running) {
(polyfillCancel || noop)(element);
animationData.done();
}
element.data(NG_ANIMATE_CONTROLLER, {running:true, done:done});
element.addClass(className);
beforeFn(element, parent, after);
if (element.length == 0) return done();
var memento = (polyfillSetup || noop)(element);
// $window.setTimeout(beginAnimation, 0); this was causing the element not to animate
// keep at 1 for animation dom rerender
$window.setTimeout(beginAnimation, 1);
}
function parseMaxTime(str) {
var total = 0, values = isString(str) ? str.split(/\s*,\s*/) : [];
forEach(values, function(value) {
total = Math.max(parseFloat(value) || 0, total);
});
return total;
}
function beginAnimation() {
element.addClass(activeClassName);
if (polyfillStart) {
polyfillStart(element, done, memento);
} else if (isFunction($window.getComputedStyle)) {
//one day all browsers will have these properties
var w3cAnimationProp = 'animation';
var w3cTransitionProp = 'transition';
//but some still use vendor-prefixed styles
var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation';
var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition';
var durationKey = 'Duration',
delayKey = 'Delay',
animationIterationCountKey = 'IterationCount',
duration = 0;
//we want all the styles defined before and after
var ELEMENT_NODE = 1;
forEach(element, function(element) {
if (element.nodeType == ELEMENT_NODE) {
var elementStyles = $window.getComputedStyle(element) || {};
var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]),
parseMaxTime(elementStyles[vendorTransitionProp + delayKey]));
var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]),
parseMaxTime(elementStyles[vendorAnimationProp + delayKey]));
var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]),
parseMaxTime(elementStyles[vendorTransitionProp + durationKey]));
var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]),
parseMaxTime(elementStyles[vendorAnimationProp + durationKey]));
if(animationDuration > 0) {
animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0,
parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0,
1);
}
duration = Math.max(animationDelay + animationDuration,
transitionDelay + transitionDuration,
duration);
}
});
$window.setTimeout(done, duration * 1000);
} else {
done();
}
}
function done() {
if(!done.run) {
done.run = true;
afterFn(element, parent, after);
element.removeClass(className);
element.removeClass(activeClassName);
element.removeData(NG_ANIMATE_CONTROLLER);
}
}
};
}
function show(element) {
element.css('display', '');
}
function hide(element) {
element.css('display', 'none');
}
function insert(element, parent, after) {
var afterNode = after && after[after.length - 1];
var parentNode = parent && parent[0] || afterNode && afterNode.parentNode;
var afterNextSibling = afterNode && afterNode.nextSibling;
forEach(element, function(node) {
if (afterNextSibling) {
parentNode.insertBefore(node, afterNextSibling);
} else {
parentNode.appendChild(node);
}
});
}
function remove(element) {
element.remove();
}
function move(element, parent, after) {
// Do not remove element before insert. Removing will cause data associated with the
// element to be dropped. Insert will implicitly do the remove.
insert(element, parent, after);
}
};
/**
* @ngdoc function
* @name ng.animator#enabled
* @methodOf ng.$animator
* @function
*
* @param {Boolean=} If provided then set the animation on or off.
* @return {Boolean} Current animation state.
*
* @description
* Globally enables/disables animations.
*
*/
AnimatorService.enabled = function(value) {
if (arguments.length) {
rootAnimateController.running = !value;
}
return !rootAnimateController.running;
};
return AnimatorService;
}];
};

View file

@ -2,59 +2,72 @@
function classDirective(name, selector) {
name = 'ngClass' + name;
return ngDirective(function(scope, element, attr) {
var oldVal = undefined;
return ['$animate', function($animate) {
return {
restrict: 'AC',
link: function(scope, element, attr) {
var oldVal = undefined;
scope.$watch(attr[name], ngClassWatchAction, true);
scope.$watch(attr[name], ngClassWatchAction, true);
attr.$observe('class', function(value) {
var ngClass = scope.$eval(attr[name]);
ngClassWatchAction(ngClass, ngClass);
});
attr.$observe('class', function(value) {
var ngClass = scope.$eval(attr[name]);
ngClassWatchAction(ngClass, ngClass);
});
if (name !== 'ngClass') {
scope.$watch('$index', function($index, old$index) {
var mod = $index & 1;
if (mod !== old$index & 1) {
if (mod === selector) {
addClass(scope.$eval(attr[name]));
} else {
removeClass(scope.$eval(attr[name]));
if (name !== 'ngClass') {
scope.$watch('$index', function($index, old$index) {
var mod = $index & 1;
if (mod !== old$index & 1) {
if (mod === selector) {
addClass(scope.$eval(attr[name]));
} else {
removeClass(scope.$eval(attr[name]));
}
}
});
}
function ngClassWatchAction(newVal) {
if (selector === true || scope.$index % 2 === selector) {
if (oldVal && !equals(newVal,oldVal)) {
removeClass(oldVal);
}
addClass(newVal);
}
oldVal = copy(newVal);
}
});
}
function ngClassWatchAction(newVal) {
if (selector === true || scope.$index % 2 === selector) {
if (oldVal && !equals(newVal,oldVal)) {
removeClass(oldVal);
function removeClass(classVal) {
$animate.removeClass(element, flattenClasses(classVal));
}
addClass(newVal);
}
oldVal = copy(newVal);
}
function removeClass(classVal) {
if (isObject(classVal) && !isArray(classVal)) {
classVal = map(classVal, function(v, k) { if (v) return k });
}
element.removeClass(isArray(classVal) ? classVal.join(' ') : classVal);
}
function addClass(classVal) {
$animate.addClass(element, flattenClasses(classVal));
}
function flattenClasses(classVal) {
if(isArray(classVal)) {
return classVal.join(' ');
} else if (isObject(classVal)) {
var classes = [], i = 0;
forEach(classVal, function(v, k) {
if (v) {
classes.push(k);
}
});
return classes.join(' ');
}
function addClass(classVal) {
if (isObject(classVal) && !isArray(classVal)) {
classVal = map(classVal, function(v, k) { if (v) return k });
return classVal;
};
}
if (classVal) {
element.addClass(isArray(classVal) ? classVal.join(' ') : classVal);
}
}
});
};
}];
}
/**
@ -70,6 +83,10 @@ function classDirective(name, selector) {
* When the expression changes, the previously added classes are removed and only then the
* new classes are added.
*
* @animations
* add - happens just before the class is applied to the element
* remove - happens just before the class is removed from the element
*
* @element ANY
* @param {expression} ngClass {@link guide/expression Expression} to eval. The result
* of the evaluation can be a string representing space delimited class
@ -78,7 +95,7 @@ function classDirective(name, selector) {
* element.
*
* @example
<example>
<example animations="true">
<file name="index.html">
<input type="button" value="set" ng-click="myVar='my-class'">
<input type="button" value="clear" ng-click="myVar=''">
@ -86,8 +103,23 @@ function classDirective(name, selector) {
<span ng-class="myVar">Sample Text</span>
</file>
<file name="style.css">
.my-class {
.my-class-add,
.my-class-remove {
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
}
.my-class,
.my-class-add.my-class-add-active {
color: red;
font-size:3em;
}
.my-class-remove.my-class-remove-active {
font-size:1.0em;
color:black;
}
</file>
<file name="scenario.js">

View file

@ -30,7 +30,7 @@
* jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element
* the added class will be lost because the original compiled state is used to regenerate the element.
*
* Additionally, you can provide animations via the ngAnimate attribute to animate the **enter**
* Additionally, you can provide animations via the ngAnimate module to animate the **enter**
* and **leave** effects.
*
* @animations
@ -47,36 +47,32 @@
<file name="index.html">
Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/>
Show when checked:
<span ng-if="checked" ng-animate="'example'">
<span ng-if="checked" class="example-if">
I'm removed when the checkbox is unchecked.
</span>
</file>
<file name="animations.css">
.example-leave, .example-enter {
.example-if.ng-enter,
.example-if.ng-leave {
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
}
.example-enter {
.example-if.ng-enter,
.example-if.ng-leave.ng-leave-active {
opacity:0;
}
.example-enter.example-enter-active {
opacity:1;
}
.example-leave {
.example-if.ng-enter.ng-enter-active,
.example-if.ng-leave {
opacity:1;
}
.example-leave.example-leave-active {
opacity:0;
}
</file>
</example>
*/
var ngIfDirective = ['$animator', function($animator) {
var ngIfDirective = ['$animate', function($animate) {
return {
transclude: 'element',
priority: 1000,
@ -84,11 +80,10 @@ var ngIfDirective = ['$animator', function($animator) {
restrict: 'A',
compile: function (element, attr, transclude) {
return function ($scope, $element, $attr) {
var animate = $animator($scope, $attr);
var childElement, childScope;
$scope.$watch($attr.ngIf, function ngIfWatchAction(value) {
if (childElement) {
animate.leave(childElement);
$animate.leave(childElement);
childElement = undefined;
}
if (childScope) {
@ -99,7 +94,7 @@ var ngIfDirective = ['$animator', function($animator) {
childScope = $scope.$new();
transclude(childScope, function (clone) {
childElement = clone;
animate.enter(clone, $element.parent(), $element);
$animate.enter(clone, $element.parent(), $element);
});
}
});

View file

@ -23,9 +23,6 @@
* (e.g. ngInclude won't work for cross-domain requests on all browsers and for `file://`
* access on some browsers)
*
* Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**
* and **leave** effects.
*
* @animations
* enter - happens just after the ngInclude contents change and a new DOM element is created and injected into the ngInclude container
* leave - happens just after the ngInclude contents change and just before the former contents are removed from the DOM
@ -143,8 +140,8 @@
* @description
* Emitted every time the ngInclude content is reloaded.
*/
var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', '$animator', '$sce',
function($http, $templateCache, $anchorScroll, $compile, $animator, $sce) {
var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', '$animate', '$sce',
function($http, $templateCache, $anchorScroll, $compile, $animate, $sce) {
return {
restrict: 'ECA',
terminal: true,
@ -154,7 +151,6 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
autoScrollExp = attr.autoscroll;
return function(scope, element, attr) {
var animate = $animator(scope, attr);
var changeCounter = 0,
childScope;
@ -163,7 +159,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
childScope.$destroy();
childScope = null;
}
animate.leave(element.contents(), element);
$animate.leave(element.contents());
};
scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) {
@ -175,11 +171,11 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
if (childScope) childScope.$destroy();
childScope = scope.$new();
animate.leave(element.contents(), element);
$animate.leave(element.contents());
var contents = jqLite('<div/>').html(response).contents();
animate.enter(contents, element);
$animate.enter(contents, element);
$compile(contents)(childScope);
if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {

View file

@ -20,9 +20,6 @@
* | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). |
* | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). |
*
* Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**,
* **leave** and **move** effects.
*
*
* # Special repeat start and end points
* To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
@ -131,46 +128,40 @@
I have {{friends.length}} friends. They are:
<input type="search" ng-model="q" placeholder="filter friends..." />
<ul>
<li ng-repeat="friend in friends | filter:q"
ng-animate="{enter: 'example-repeat-enter',
leave: 'example-repeat-leave',
move: 'example-repeat-move'}">
<li class="animate-repeat" ng-repeat="friend in friends | filter:q">
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
</li>
</ul>
</div>
</file>
<file name="animations.css">
.example-repeat-enter,
.example-repeat-leave,
.example-repeat-move {
.animate-repeat {
-webkit-transition:all linear 0.5s;
-moz-transition:all linear 0.5s;
-ms-transition:all linear 0.5s;
-o-transition:all linear 0.5s;
transition:all linear 0.5s;
}
.example-repeat-enter {
.animate-repeat.ng-enter {
line-height:0;
opacity:0;
}
.example-repeat-enter.example-repeat-enter-active {
.animate-repeat.ng-enter.ng-enter-active {
line-height:20px;
opacity:1;
}
.example-repeat-leave {
.animate-repeat.ng-leave {
opacity:1;
line-height:20px;
}
.example-repeat-leave.example-repeat-leave-active {
.animate-repeat.ng-leave.ng-leave-active {
opacity:0;
line-height:0;
}
.example-repeat-move { }
.example-repeat-move.example-repeat-move-active { }
.animate-repeat.ng-move { }
.animate-repeat.ng-move.ng-move-active { }
</file>
<file name="scenario.js">
it('should render initial data set', function() {
@ -195,7 +186,7 @@
</file>
</example>
*/
var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
var NG_REMOVED = '$$NG_REMOVED';
var ngRepeatMinErr = minErr('ngRepeat');
return {
@ -204,7 +195,6 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
terminal: true,
compile: function(element, attr, linker) {
return function($scope, $element, $attr){
var animate = $animator($scope, $attr);
var expression = $attr.ngRepeat;
var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
trackByExp, trackByExpGetter, trackByIdFn, trackByIdArrayFn, trackByIdObjFn, lhs, rhs, valueIdentifier, keyIdentifier,
@ -316,7 +306,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
for (key in lastBlockMap) {
if (lastBlockMap.hasOwnProperty(key)) {
block = lastBlockMap[key];
animate.leave(block.elements);
$animate.leave(block.elements);
forEach(block.elements, function(element) { element[NG_REMOVED] = true});
block.scope.$destroy();
}
@ -342,7 +332,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
// do nothing
} else {
// existing item which got moved
animate.move(block.elements, null, jqLite(previousNode));
$animate.move(block.elements, null, jqLite(previousNode));
}
previousNode = block.endNode;
} else {
@ -360,7 +350,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
if (!block.startNode) {
linker(childScope, function(clone) {
animate.enter(clone, null, jqLite(previousNode));
$animate.enter(clone, null, jqLite(previousNode));
previousNode = clone;
block.scope = childScope;
block.startNode = clone[0];

View file

@ -12,8 +12,6 @@
* With ngHide this is the reverse whereas true values cause the element itself to become
* hidden.
*
* Additionally, you can also provide animations via the ngAnimate attribute to animate the **show**
* and **hide** effects.
*
* @animations
* show - happens after the ngShow expression evaluates to a truthy value and the contents are set to visible
@ -29,36 +27,37 @@
Click me: <input type="checkbox" ng-model="checked"><br/>
<div>
Show:
<span class="check-element"
ng-show="checked"
ng-animate="{show: 'example-show', hide: 'example-hide'}">
<span class="check-element example-show-hide" ng-show="checked">
<span class="icon-thumbs-up"></span> I show up when your checkbox is checked.
</span>
</div>
<div>
Hide:
<span class="check-element"
ng-hide="checked"
ng-animate="{show: 'example-show', hide: 'example-hide'}">
<span class="check-element example-show-hide" ng-hide="checked">
<span class="icon-thumbs-down"></span> I hide when your checkbox is checked.
</span>
</div>
</file>
<file name="animations.css">
.example-show, .example-hide {
.example-show-hide {
-webkit-transition:all linear 0.5s;
-moz-transition:all linear 0.5s;
-ms-transition:all linear 0.5s;
-o-transition:all linear 0.5s;
transition:all linear 0.5s;
display:block;
}
.example-show-hide.ng-hide {
display:none;
}
.example-show {
.example-show-hide.ng-hide-remove {
display:block;
line-height:0;
opacity:0;
padding:0 10px;
}
.example-show-active.example-show-active {
.example-show-hide.ng-hide-remove.ng-hide-remove-active {
line-height:20px;
opacity:1;
padding:10px;
@ -66,14 +65,14 @@
background:white;
}
.example-hide {
.example-show-hide.ng-hide-add {
line-height:20px;
opacity:1;
padding:10px;
border:1px solid black;
background:white;
}
.example-hide-active.example-hide-active {
.example-show-hide.ng-hide-add.ng-hide-add-active {
line-height:0;
opacity:0;
padding:0 10px;
@ -98,12 +97,10 @@
</file>
</example>
*/
//TODO(misko): refactor to remove element from the DOM
var ngShowDirective = ['$animator', function($animator) {
var ngShowDirective = ['$animate', function($animate) {
return function(scope, element, attr) {
var animate = $animator(scope, attr);
scope.$watch(attr.ngShow, function ngShowWatchAction(value){
animate[toBoolean(value) ? 'show' : 'hide'](element);
$animate[toBoolean(value) ? 'show' : 'hide'](element);
});
};
}];
@ -121,9 +118,6 @@ var ngShowDirective = ['$animator', function($animator) {
* With ngHide this is the reverse whereas true values cause the element itself to become
* hidden.
*
* Additionally, you can also provide animations via the ngAnimate attribute to animate the **show**
* and **hide** effects.
*
* @animations
* show - happens after the ngHide expression evaluates to a non truthy value and the contents are set to visible
* hide - happens after the ngHide expression evaluates to a truthy value and just before the contents are set to hidden
@ -138,36 +132,36 @@ var ngShowDirective = ['$animator', function($animator) {
Click me: <input type="checkbox" ng-model="checked"><br/>
<div>
Show:
<span class="check-element"
ng-show="checked"
ng-animate="{show: 'example-show', hide: 'example-hide'}">
<span class="check-element example-show-hide" ng-show="checked">
<span class="icon-thumbs-up"></span> I show up when your checkbox is checked.
</span>
</div>
<div>
Hide:
<span class="check-element"
ng-hide="checked"
ng-animate="{show: 'example-show', hide: 'example-hide'}">
<span class="check-element example-show-hide" ng-hide="checked">
<span class="icon-thumbs-down"></span> I hide when your checkbox is checked.
</span>
</div>
</file>
<file name="animations.css">
.example-show, .example-hide {
.example-show-hide {
-webkit-transition:all linear 0.5s;
-moz-transition:all linear 0.5s;
-ms-transition:all linear 0.5s;
-o-transition:all linear 0.5s;
transition:all linear 0.5s;
display:block;
}
.example-show-hide.ng-hide {
display:none;
}
.example-show {
.example-show-hide.ng-hide-remove {
display:block;
line-height:0;
opacity:0;
padding:0 10px;
}
.example-show.example-show-active {
.example-show-hide.ng-hide-remove.ng-hide-remove-active {
line-height:20px;
opacity:1;
padding:10px;
@ -175,14 +169,14 @@ var ngShowDirective = ['$animator', function($animator) {
background:white;
}
.example-hide {
.example-show-hide.ng-hide-add {
line-height:20px;
opacity:1;
padding:10px;
border:1px solid black;
background:white;
}
.example-hide.example-hide-active {
.example-show-hide.ng-hide-add.ng-hide-add-active {
line-height:0;
opacity:0;
padding:0 10px;
@ -207,12 +201,10 @@ var ngShowDirective = ['$animator', function($animator) {
</file>
</example>
*/
//TODO(misko): refactor to remove element from the DOM
var ngHideDirective = ['$animator', function($animator) {
var ngHideDirective = ['$animate', function($animate) {
return function(scope, element, attr) {
var animate = $animator(scope, attr);
scope.$watch(attr.ngHide, function ngHideWatchAction(value){
animate[toBoolean(value) ? 'hide' : 'show'](element);
$animate[toBoolean(value) ? 'hide' : 'show'](element);
});
};
}];

View file

@ -19,9 +19,6 @@
* expression is evaluated. If a matching expression is not found via a when attribute then an element with the default
* attribute is displayed.
*
* Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**
* and **leave** effects.
*
* @animations
* enter - happens after the ngSwtich contents change and the matched child element is placed inside the container
* leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM
@ -55,9 +52,8 @@
<tt>selection={{selection}}</tt>
<hr/>
<div
class="example-animate-container"
ng-switch on="selection"
ng-animate="{enter: 'example-enter', leave: 'example-leave'}">
class="example-animate-container animate-switch"
ng-switch on="selection">
<div ng-switch-when="settings">Settings Div</div>
<div ng-switch-when="home">Home Span</div>
<div ng-switch-default>default</div>
@ -71,10 +67,9 @@
}
</file>
<file name="animations.css">
.example-leave, .example-enter {
.animate-switch > * {
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
@ -90,17 +85,17 @@
padding:10px;
}
.example-enter {
.animate-switch > .ng-enter {
top:-50px;
}
.example-enter.example-enter-active {
.animate-switch > .ng-enter.ng-enter-active {
top:0;
}
.example-leave {
.animate-switch > .ng-leave {
top:0;
}
.example-leave.example-leave-active {
.animate-switch > .ng-leave.ng-leave-active {
top:50px;
}
</file>
@ -119,7 +114,7 @@
</file>
</example>
*/
var ngSwitchDirective = ['$animator', function($animator) {
var ngSwitchDirective = ['$animate', function($animate) {
return {
restrict: 'EA',
require: 'ngSwitch',
@ -129,7 +124,6 @@ var ngSwitchDirective = ['$animator', function($animator) {
this.cases = {};
}],
link: function(scope, element, attr, ngSwitchController) {
var animate = $animator(scope, attr);
var watchExpr = attr.ngSwitch || attr.on,
selectedTranscludes,
selectedElements,
@ -138,7 +132,7 @@ var ngSwitchDirective = ['$animator', function($animator) {
scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
for (var i= 0, ii=selectedScopes.length; i<ii; i++) {
selectedScopes[i].$destroy();
animate.leave(selectedElements[i]);
$animate.leave(selectedElements[i]);
}
selectedElements = [];
@ -153,7 +147,7 @@ var ngSwitchDirective = ['$animator', function($animator) {
var anchor = selectedTransclude.element;
selectedElements.push(caseElement);
animate.enter(caseElement, anchor.parent(), anchor);
$animate.enter(caseElement, anchor.parent(), anchor);
});
});
}

714
src/ngAnimate/animate.js Normal file
View file

@ -0,0 +1,714 @@
/**
* @ngdoc overview
* @name ngAnimate
* @description
*
* ngAnimate
* =========
*
* The ngAnimate module is an optional module that comes packed with AngularJS that can be included within an AngularJS
* application to provide support for CSS and JavaScript animation hooks.
*
* To make use of animations with AngularJS, the `angular-animate.js` JavaScript file must be included into your application
* and the `ngAnimate` module must be included as a dependency.
*
* <pre>
* angular.module('App', ['ngAnimate']);
* </pre>
*
* Then, to see animations in action, all that is required is to define the appropriate CSS classes
* or to register a JavaScript animation via the $animation service. The directives that support animation automatically are:
* `ngRepeat`, `ngInclude`, `ngSwitch`, `ngShow`, `ngHide` and `ngView`. Custom directives can take advantage of animation
* by using the `$animate` service.
*
* Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives:
*
* | Directive | Supported Animations |
* |========================================================== |====================================================|
* | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move |
* | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
* | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
* | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
* | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
* | {@link ng.directive:ngShow#animations ngShow & ngHide} | show and hide |
* | {@link ng.directive:ngShow#animations ngClass} | add and remove |
*
* You can find out more information about animations upon visiting each directive page.
*
* Below is an example of how to apply animations to a directive that supports animation hooks:
*
* <pre>
* <style type="text/css">
* .slide.ng-enter > div,
* .slide.ng-leave > div {
* -webkit-transition:0.5s linear all;
* -moz-transition:0.5s linear all;
* -o-transition:0.5s linear all;
* transition:0.5s linear all;
* }
*
* .slide > .ng-enter { } /&#42; starting animations for enter &#42;/
* .slide > .ng-enter-active { } /&#42; terminal animations for enter &#42;/
* .slide > .ng-leave { } /&#42; starting animations for leave &#42;/
* .slide > .ng-leave-active { } /&#42; terminal animations for leave &#42;/
* </style>
*
* <!--
* the animate service will automatically add .ng-enter and .ng-leave to the element
* to trigger the CSS animations
* -->
* <ANY class="slide" ng-include="..."></ANY>
* </pre>
*
* Keep in mind that if an animation is running, any child elements cannot be animated until the parent element's
* animation has completed.
*
* <h2>CSS-defined Animations</h2>
* The animate service will automatically apply two CSS classes to the animated element and these two CSS classes
* are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported
* and can be used to play along with this naming structure.
*
* The following code below demonstrates how to perform animations using **CSS transitions** with Angular:
*
* <pre>
* <style type="text/css">
* /&#42;
* The animate class is apart of the element and the ng-enter class
* is attached to the element once the enter animation event is triggered
* &#42;/
* .reveal-animation.ng-enter {
* -webkit-transition: 1s linear all; /&#42; Safari/Chrome &#42;/
* -moz-transition: 1s linear all; /&#42; Firefox &#42;/
* -o-transition: 1s linear all; /&#42; Opera &#42;/
* transition: 1s linear all; /&#42; IE10+ and Future Browsers &#42;/
*
* /&#42; The animation preparation code &#42;/
* opacity: 0;
* }
*
* /&#42;
* Keep in mind that you want to combine both CSS
* classes together to avoid any CSS-specificity
* conflicts
* &#42;/
* .reveal-animation.ng-enter.ng-enter-active {
* /&#42; The animation code itself &#42;/
* opacity: 1;
* }
* </style>
*
* <div class="view-container">
* <div ng-view class="reveal-animation"></div>
* </div>
* </pre>
*
* The following code below demonstrates how to perform animations using **CSS animations** with Angular:
*
* <pre>
* <style type="text/css">
* .reveal-animation.ng-enter {
* -webkit-animation: enter_sequence 1s linear; /&#42; Safari/Chrome &#42;/
* -moz-animation: enter_sequence 1s linear; /&#42; Firefox &#42;/
* -o-animation: enter_sequence 1s linear; /&#42; Opera &#42;/
* animation: enter_sequence 1s linear; /&#42; IE10+ and Future Browsers &#42;/
* }
* &#64-webkit-keyframes enter_sequence {
* from { opacity:0; }
* to { opacity:1; }
* }
* &#64-moz-keyframes enter_sequence {
* from { opacity:0; }
* to { opacity:1; }
* }
* &#64-o-keyframes enter_sequence {
* from { opacity:0; }
* to { opacity:1; }
* }
* &#64keyframes enter_sequence {
* from { opacity:0; }
* to { opacity:1; }
* }
* </style>
*
* <div class="view-container">
* <div ng-view class="reveal-animation"></div>
* </div>
* </pre>
*
* Both CSS3 animations and transitions can be used together and the animate service will figure out the correct duration and delay timing.
*
* Upon DOM mutation, the event class is added first (something like `ng-enter`), then the browser prepares itself to add
* the active class (in this case `ng-enter-active`) which then triggers the animation. The animation module will automatically
* detect the CSS code to determine when the animation ends. Once the animation is over then both CSS classes will be
* removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end
* immediately resulting in a DOM element that is at its final state. This final state is when the DOM element
* has no CSS transition/animation classes applied to it.
*
* <h2>JavaScript-defined Animations</h2>
* In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not
* yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module.
*
* <pre>
* //!annotate="YourApp" Your AngularJS Module|Replace this or ngModule with the module that you used to define your application.
* var ngModule = angular.module('YourApp', []);
* ngModule.animation('.my-crazy-animation', function() {
* return {
* enter: function(element, done) {
* //run the animation
* //!annotate Cancel Animation|This function (if provided) will perform the cancellation of the animation when another is triggered
* return function(element, done) {
* //cancel the animation
* }
* }
* leave: function(element, done) { },
* move: function(element, done) { },
* show: function(element, done) { },
* hide: function(element, done) { },
* addClass: function(element, className, done) { },
* removeClass: function(element, className, done) { },
* }
* });
* </pre>
*
* JavaScript-defined animations are created with a CSS-like class selector and a collection of events which are set to run
* a javascript callback function. When an animation is triggered, $animate will look for a matching animation which fits
* the element's CSS class attribute value and then run the matching animation event function (if found).
* In other words, if the CSS classes present on the animated element match any of the JavaScript animations then the callback function
* be executed. It should be also noted that only simple or compound class selectors are allowed.
*
* Within a JavaScript animation, an object containing various event callback animation functions is expected to be returned.
* As explained above, these callbacks are triggered based on the animation event. Therefore if an enter animation is run,
* and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation
* or transition code that is defined via a stylesheet).
*
*/
angular.module('ngAnimate', ['ng'])
/**
* @ngdoc object
* @name ngAnimate.$animateProvider
* @description
*
* The $AnimationProvider provider allows developers to register and access custom JavaScript animations directly inside
* of a module. When an animation is triggered, the $animate service will query the $animation function to find any
* animations that match the provided name value.
*
* Please visit the {@link ngAnimate ngAnimate} module overview page learn more about how to use animations in your application.
*
*/
.config(['$provide', '$animateProvider', function($provide, $animateProvider) {
var selectors = $animateProvider.$$selectors;
var NG_ANIMATE_STATE = '$$ngAnimateState';
var rootAnimateState = {running:true};
$provide.decorator('$animate', ['$delegate', '$injector', '$window', '$sniffer', '$rootElement',
function($delegate, $injector, $window, $sniffer, $rootElement) {
var noop = angular.noop;
var forEach = angular.forEach;
$rootElement.data(NG_ANIMATE_STATE, rootAnimateState);
function lookup(name) {
if (name) {
var classes = name.substr(1).split('.'),
classMap = {};
for (var i = 0, ii = classes.length; i < ii; i++) {
classMap[classes[i]] = true;
}
var matches = [];
for (var i = 0, ii = selectors.length; i < ii; i++) {
var selectorFactory = selectors[i];
var found = true;
for(var j = 0, jj = selectorFactory.selectors.length; j < jj; j++) {
var klass = selectorFactory.selectors[j];
if(klass.length > 0) {
found = found && classMap[klass];
}
}
if(found) {
matches.push($injector.get(selectorFactory.name));
}
}
return matches;
}
};
/**
* @ngdoc object
* @name ngAnimate.$animate
* @requires $window, $sniffer, $rootElement
* @function
*
* @description
* The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move)
* as well as during addClass and removeClass operations. When any of these operations are run, the $animate service
* will examine any JavaScript-defined animations (which are defined by using the $animateProvider provider object)
* as well as any CSS-defined animations against the CSS classes present on the element once the DOM operation is run.
*
* The `$animate` service is used behind the scenes with pre-existing directives and animation with these directives
* will work out of the box without any extra configuration.
*
* Please visit the {@link ngAnimate ngAnimate} module overview page learn more about how to use animations in your application.
*
*/
return {
/**
* @ngdoc function
* @name ngAnimate.$animate#enter
* @methodOf ngAnimate.$animate
* @function
*
* @description
* Appends the element to the parent element that resides in the document and then runs the enter animation. Once
* the animation is started, the following CSS classes will be present on the element for the duration of the animation:
* <pre>
* .ng-enter
* .ng-enter-active
* </pre>
*
* Once the animation is complete then the done callback, if provided, will be also fired.
*
* @param {jQuery/jqLite element} element the element that will be the focus of the enter animation
* @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the enter animation
* @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the enter animation
* @param {function()=} done callback function that will be called once the animation is complete
*/
enter : function(element, parent, after, done) {
$delegate.enter(element, parent, after);
performAnimation('enter', 'ng-enter', element, parent, after, done);
},
/**
* @ngdoc function
* @name ngAnimate.$animate#leave
* @methodOf ngAnimate.$animate
* @function
*
* @description
* Runs the leave animation operation and, upon completion, removes the element from the DOM. Once
* the animation is started, the following CSS classes will be added for the duration of the animation:
* <pre>
* .ng-leave
* .ng-leave-active
* </pre>
*
* Once the animation is complete then the done callback, if provided, will be also fired.
*
* @param {jQuery/jqLite element} element the element that will be the focus of the leave animation
* @param {function()=} done callback function that will be called once the animation is complete
*/
leave : function(element, done) {
performAnimation('leave', 'ng-leave', element, null, null, function() {
$delegate.leave(element, done);
});
},
/**
* @ngdoc function
* @name ngAnimate.$animate#move
* @methodOf ngAnimate.$animate
* @function
*
* @description
* Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parent container or
* add the element directly after the after element if present. Then the move animation will be run. Once
* the animation is started, the following CSS classes will be added for the duration of the animation:
* <pre>
* .ng-move
* .ng-move-active
* </pre>
*
* Once the animation is complete then the done callback, if provided, will be also fired.
*
* @param {jQuery/jqLite element} element the element that will be the focus of the move animation
* @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the move animation
* @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the move animation
* @param {function()=} done callback function that will be called once the animation is complete
*/
move : function(element, parent, after, done) {
$delegate.move(element, parent, after);
performAnimation('move', 'ng-move', element, null, null, done);
},
/**
* @ngdoc function
* @name ngAnimate.$animate#show
* @methodOf ngAnimate.$animate
* @function
*
* @description
* Reveals the element by removing the `ng-hide` class thus performing an animation in the process. During
* this animation the CSS classes present on the element will be:
*
* <pre>
* .ng-hide //already on the element if hidden
* .ng-hide-remove
* .ng-hide-remove-active
* </pre>
*
* Once the animation is complete then all three CSS classes will be removed from the element.
* The done callback, if provided, will be also fired once the animation is complete.
*
* @param {jQuery/jqLite element} element the element that will be rendered visible or hidden
* @param {function()=} done callback function that will be called once the animation is complete
*/
show : function(element, done) {
performAnimation('show', 'ng-hide-remove', element, null, null, function() {
$delegate.show(element, done);
});
},
/**
* @ngdoc function
* @name ngAnimate.$animate#hide
* @methodOf ngAnimate.$animate
*
* @description
* Sets the element to hidden by adding the `ng-hide` class it. However, before the class is applied
* the following CSS classes will be added temporarily to trigger any animation code:
*
* <pre>
* .ng-hide-add
* .ng-hide-add-active
* </pre>
*
* Once the animation is complete then both CSS classes will be removed and `ng-hide` will be added to the element.
* The done callback, if provided, will be also fired once the animation is complete.
*
* @param {jQuery/jqLite element} element the element that will be rendered visible or hidden
* @param {function()=} done callback function that will be called once the animation is complete
*/
hide : function(element, done) {
performAnimation('hide', 'ng-hide-add', element, null, null, function() {
$delegate.hide(element, done);
});
},
/**
* @ngdoc function
* @name ngAnimate.$animate#addClass
* @methodOf ngAnimate.$animate
*
* @description
* Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class.
* Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide
* the animate service the setup and active CSS classes in order to trigger the animation.
*
* For example, upon execution of:
*
* <pre>
* $animate.addClass(element, 'super');
* </pre>
*
* The generated CSS class values present on element will look like:
* <pre>
* .super-add
* .super-add-active
* </pre>
*
* And upon completion, the generated animation CSS classes will be removed from the element, but the className
* value will be attached to the element. In this case, based on the previous example, the resulting CSS class for the element
* will look like so:
*
* <pre>
* .super
* </pre>
*
* Once this is complete, then the done callback, if provided, will be fired.
*
* @param {jQuery/jqLite element} element the element that will be animated
* @param {string} className the CSS class that will be animated and then attached to the element
* @param {function()=} done callback function that will be called once the animation is complete
*/
addClass : function(element, className, done) {
performAnimation('addClass', className, element, null, null, function() {
$delegate.addClass(element, className, done);
});
},
/**
* @ngdoc function
* @name ngAnimate.$animate#removeClass
* @methodOf ngAnimate.$animate
*
* @description
* Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value
* from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in
* order to provide the animate service the setup and active CSS classes in order to trigger the animation.
*
* For example, upon the execution of:
*
* <pre>
* $animate.removeClass(element, 'super');
* </pre>
*
* The CSS class values present on element during the animation will look like:
*
* <pre>
* .super //this was here from before
* .super-remove
* .super-remove-active
* </pre>
*
* And upon completion, the generated animation CSS classes will be removed from the element as well as the
* className value that was provided (in this case {@type super} will be removed). Once that is complete, then, if provided,
* the done callback will be fired.
*
* @param {jQuery/jqLite element} element the element that will be animated
* @param {string} className the CSS class that will be animated and then removed from the element
* @param {function()=} done callback function that will be called once the animation is complete
*/
removeClass : function(element, className, done) {
performAnimation('removeClass', className, element, null, null, function() {
$delegate.removeClass(element, className, done);
});
},
/**
* @ngdoc function
* @name ngAnimate.$animate#enabled
* @methodOf ngAnimate.$animate
* @function
*
* @param {boolean=} If provided then set the animation on or off.
* @return {boolean} Current animation state.
*
* @description
* Globally enables/disables animations.
*
*/
enabled : function(value) {
if (arguments.length) {
rootAnimateState.running = !value;
}
return !rootAnimateState.running;
}
};
/*
all animations call this shared animation triggering function internally.
The event variable refers to the JavaScript animation event that will be triggered
and the className value is the name of the animation that will be applied within the
CSS code. Element, parent and after are provided DOM elements for the animation
and the onComplete callback will be fired once the animation is fully complete.
*/
function performAnimation(event, className, element, parent, after, onComplete) {
if(nothingToAnimate(className, element)) {
(onComplete || noop)();
} else {
var classes = ((element.attr('class') || '') + ' ' + className),
animationLookup = (' ' + classes).replace(/\s+/g,'.'),
animations = [];
forEach(lookup(animationLookup), function(animation, index) {
animations.push({
start : animation[event],
done : false
});
});
if (!parent) {
parent = after ? after.parent() : element.parent();
}
var disabledAnimation = { running : true };
//skip the animation if animations are disabled, a parent is already being animated
//or the element is not currently attached to the document body.
if ((parent.inheritedData(NG_ANIMATE_STATE) || disabledAnimation).running) {
//avoid calling done() since there is no need to remove any
//data or className values since this happens earlier than that
(onComplete || noop)();
return;
}
var animationData = element.data(NG_ANIMATE_STATE) || {};
//if an animation is currently running on the element then lets take the steps
//to cancel that animation and fire any required callbacks
if(animationData.running) {
cancelAnimations(animationData.animations);
animationData.done();
}
element.data(NG_ANIMATE_STATE, {
running:true,
animations:animations,
done:done
});
if(event == 'addClass') {
className = suffixClasses(className, '-add');
} else if(event == 'removeClass') {
className = suffixClasses(className, '-remove');
}
element.addClass(className);
forEach(animations, function(animation, index) {
var fn = function() {
progress(index);
};
if(animation.start) {
if(event == 'addClass' || event == 'removeClass') {
animation.cancel = animation.start(element, className, fn);
} else {
animation.cancel = animation.start(element, fn);
}
} else {
fn();
}
});
}
function nothingToAnimate(className, element) {
return !(className && className.length > 0 && element.length > 0);
}
function cancelAnimations(animations) {
forEach(animations, function(animation) {
(animation.cancel || noop)(element);
});
}
function suffixClasses(classes, suffix) {
var className = '';
classes = angular.isArray(classes) ? classes : classes.split(/\s+/);
forEach(classes, function(klass, i) {
if(klass && klass.length > 0) {
className += (i > 0 ? ' ' : '') + klass + suffix;
}
});
return className;
}
function progress(index) {
animations[index].done = true;
for(var i=0;i<animations.length;i++) {
if(!animations[i].done) return;
}
done();
};
function done() {
if(!done.hasBeenRun) {
done.hasBeenRun = true;
element.removeClass(className);
element.removeData(NG_ANIMATE_STATE);
(onComplete || noop)();
}
}
}
}]);
}])
.animation('', ['$window','$sniffer', function($window, $sniffer) {
return {
enter : function(element, done) {
return animate(element, 'ng-enter', done);
},
leave : function(element, done) {
return animate(element, 'ng-leave', done);
},
move : function(element, done) {
return animate(element, 'ng-move', done);
},
show : function(element, done) {
return animate(element, 'ng-hide-remove', done);
},
hide : function(element, done) {
return animate(element, 'ng-hide-add', done);
},
addClass : function(element, className, done) {
return animate(element, className, done);
},
removeClass : function(element, className, done) {
return animate(element, className, done);
}
};
function animate(element, className, done) {
if (!($sniffer.transitions || $sniffer.animations)) {
done();
} else {
var activeClassName = '';
$window.setTimeout(startAnimation, 1);
//this acts as the cancellation function in case
//a new animation is triggered while another animation
//is still going on (otherwise the active className
//would still hang around until the timer is complete).
return onComplete;
}
function parseMaxTime(str) {
var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : [];
forEach(values, function(value) {
total = Math.max(parseFloat(value) || 0, total);
});
return total;
}
function startAnimation() {
var duration = 0;
forEach(className.split(' '), function(klass, i) {
activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
});
element.addClass(activeClassName);
//one day all browsers will have these properties
var w3cAnimationProp = 'animation';
var w3cTransitionProp = 'transition';
//but some still use vendor-prefixed styles
var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation';
var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition';
var durationKey = 'Duration',
delayKey = 'Delay',
animationIterationCountKey = 'IterationCount';
//we want all the styles defined before and after
var ELEMENT_NODE = 1;
forEach(element, function(element) {
if (element.nodeType == ELEMENT_NODE) {
var elementStyles = $window.getComputedStyle(element) || {};
var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]),
parseMaxTime(elementStyles[vendorTransitionProp + delayKey]));
var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]),
parseMaxTime(elementStyles[vendorAnimationProp + delayKey]));
var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]),
parseMaxTime(elementStyles[vendorTransitionProp + durationKey]));
var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]),
parseMaxTime(elementStyles[vendorAnimationProp + durationKey]));
if(animationDuration > 0) {
animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0,
parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0,
1);
}
duration = Math.max(animationDelay + animationDuration,
transitionDelay + transitionDuration,
duration);
}
});
$window.setTimeout(onComplete, duration * 1000);
}
function onComplete() {
element.removeClass(activeClassName);
done();
};
};
}]);

View file

@ -627,6 +627,43 @@ angular.mock.$LogProvider = function() {
angular.mock.TzDate.prototype = Date.prototype;
})();
angular.mock.animate = angular.module('mock.animate', ['ng'])
.config(['$provide', function($provide) {
$provide.decorator('$animate', function($delegate) {
var animate = {
queue : [],
enabled : $delegate.enabled,
process : function(name) {
var tick = animate.queue.shift();
expect(tick.method).toBe(name);
tick.fn();
return tick;
}
};
forEach(['enter','leave','move','show','hide','addClass','removeClass'], function(method) {
animate[method] = function() {
var params = arguments;
animate.queue.push({
method : method,
params : params,
element : angular.isElement(params[0]) && params[0],
parent : angular.isElement(params[1]) && params[1],
after : angular.isElement(params[2]) && params[2],
fn : function() {
$delegate[method].apply($delegate, params);
}
});
};
});
return animate;
});
}]);
/**
* @ngdoc function
* @name angular.mock.createMockWindow

View file

@ -14,9 +14,6 @@ ngRouteModule.directive('ngView', ngViewFactory);
* Every time the current route changes, the included view changes with it according to the
* configuration of the `$route` service.
*
* Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**
* and **leave** effects.
*
* @animations
* enter - happens just after the ngView contents are changed (when the new view DOM element is inserted into the DOM)
* leave - happens just after the current ngView contents change and just before the former contents are removed from the DOM
@ -35,8 +32,8 @@ ngRouteModule.directive('ngView', ngViewFactory);
<div
ng-view
class="example-animate-container"
ng-animate="{enter: 'example-enter', leave: 'example-leave'}"></div>
class="example-$animate-container"
ng-$animate="{enter: 'example-enter', leave: 'example-leave'}"></div>
<hr />
<pre>$location.path() = {{main.$location.path()}}</pre>
@ -71,12 +68,12 @@ ngRouteModule.directive('ngView', ngViewFactory);
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
}
.example-animate-container {
.example-$animate-container {
position:relative;
height:100px;
}
.example-animate-container > * {
.example-$animate-container > * {
display:block;
width:100%;
border-left:1px solid black;
@ -162,15 +159,14 @@ ngRouteModule.directive('ngView', ngViewFactory);
* @description
* Emitted every time the ngView content is reloaded.
*/
ngViewFactory.$inject = ['$route', '$anchorScroll', '$compile', '$controller', '$animator'];
function ngViewFactory( $route, $anchorScroll, $compile, $controller, $animator) {
ngViewFactory.$inject = ['$route', '$anchorScroll', '$compile', '$controller', '$animate'];
function ngViewFactory( $route, $anchorScroll, $compile, $controller, $animate) {
return {
restrict: 'ECA',
terminal: true,
link: function(scope, element, attr) {
var lastScope,
onloadExp = attr.onload || '',
animate = $animator(scope, attr);
onloadExp = attr.onload || '';
scope.$on('$routeChangeSuccess', update);
update();
@ -184,7 +180,7 @@ function ngViewFactory( $route, $anchorScroll, $compile, $controller,
}
function clearContent() {
animate.leave(element.contents(), element);
$animate.leave(element.contents());
destroyLastScope();
}
@ -195,7 +191,7 @@ function ngViewFactory( $route, $anchorScroll, $compile, $controller,
if (template) {
clearContent();
var enterElements = jqLite('<div></div>').html(template).contents();
animate.enter(enterElements, element);
$animate.enter(enterElements, element);
var link = $compile(enterElements),
current = $route.current,

View file

@ -30,11 +30,23 @@ beforeEach(function() {
return -1;
}
function isNgElementHidden(element) {
return angular.element(element).hasClass('ng-hide');
};
this.addMatchers({
toBeInvalid: cssMatcher('ng-invalid', 'ng-valid'),
toBeValid: cssMatcher('ng-valid', 'ng-invalid'),
toBeDirty: cssMatcher('ng-dirty', 'ng-pristine'),
toBePristine: cssMatcher('ng-pristine', 'ng-dirty'),
toBeShown: function() {
this.message = valueFn("Expected element to not have 'ng-hide' class");
return !isNgElementHidden(this.actual);
},
toBeHidden: function() {
this.message = valueFn("Expected element to have 'ng-hide' class");
return isNgElementHidden(this.actual);
},
toEqual: function(expected) {
if (this.actual && this.actual.$$log) {

53
test/ng/animateSpec.js Normal file
View file

@ -0,0 +1,53 @@
describe("$animate", function() {
describe("without animation", function() {
beforeEach(inject(function($compile, _$rootElement_, $rootScope) {
element = $compile('<div></div>')($rootScope);
$rootElement = _$rootElement_;
}));
it("should add element at the start of enter animation", inject(function($animate, $compile, $rootScope) {
var child = $compile('<div></div>')($rootScope);
expect(element.contents().length).toBe(0);
$animate.enter(child, element);
expect(element.contents().length).toBe(1);
}));
it("should remove the element at the end of leave animation", inject(function($animate, $compile, $rootScope) {
var child = $compile('<div></div>')($rootScope);
element.append(child);
expect(element.contents().length).toBe(1);
$animate.leave(child);
expect(element.contents().length).toBe(0);
}));
it("should reorder the move animation", inject(function($animate, $compile, $rootScope) {
var child1 = $compile('<div>1</div>')($rootScope);
var child2 = $compile('<div>2</div>')($rootScope);
element.append(child1);
element.append(child2);
expect(element.text()).toBe('12');
$animate.move(child1, element, child2);
expect(element.text()).toBe('21');
}));
it("should animate the show animation event", inject(function($animate) {
element.addClass('ng-hide');
$animate.show(element);
expect(element).toBeShown();
}));
it("should animate the hide animation event", inject(function($animate) {
expect(element).toBeShown();
$animate.hide(element);
expect(element).toBeHidden();
}));
it("should still perform DOM operations even if animations are disabled", inject(function($animate) {
$animate.enabled(false);
expect(element).toBeShown();
$animate.hide(element);
expect(element).toBeHidden();
}));
});
});

View file

@ -1,15 +0,0 @@
'use strict';
describe('$animation', function() {
it('should allow animation registration', function() {
var noopCustom = function(){};
module(function($animationProvider) {
$animationProvider.register('noop-custom', valueFn(noopCustom));
});
inject(function($animation) {
expect($animation('noop-custom')).toBe(noopCustom);
});
});
});

View file

@ -1,773 +0,0 @@
'use strict';
describe("$animator", function() {
var body, element, $rootElement;
function html(html) {
body.append($rootElement);
$rootElement.html(html);
element = $rootElement.children().eq(0);
return element;
}
beforeEach(function() {
// we need to run animation on attached elements;
body = jqLite(document.body);
});
afterEach(function(){
dealoc(body);
});
describe("enable / disable", function() {
beforeEach(function() {
module(function($animationProvider, $provide) {
$provide.value('$window', angular.mock.createMockWindow());
});
});
it("should disable and enable the animations", function() {
var initialState = null;
var animator;
angular.bootstrap(body, [function() {
return function($animator) {
animator = $animator;
initialState = $animator.enabled();
}
}]);
expect(initialState).toBe(false);
expect(animator.enabled()).toBe(true);
expect(animator.enabled(0)).toBe(false);
expect(animator.enabled()).toBe(false);
expect(animator.enabled(1)).toBe(true);
expect(animator.enabled()).toBe(true);
});
});
describe("without animation", function() {
var window, animator;
beforeEach(function() {
module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
})
inject(function($animator, $compile, $rootScope, _$rootElement_) {
animator = $animator($rootScope, {});
element = $compile('<div></div>')($rootScope);
$rootElement = _$rootElement_;
})
});
it("should add element at the start of enter animation", inject(function($animator, $compile, $rootScope) {
var child = $compile('<div></div>')($rootScope);
expect(element.contents().length).toBe(0);
animator.enter(child, element);
expect(element.contents().length).toBe(1);
}));
it("should remove the element at the end of leave animation", inject(function($animator, $compile, $rootScope) {
var child = $compile('<div></div>')($rootScope);
element.append(child);
expect(element.contents().length).toBe(1);
animator.leave(child, element);
expect(element.contents().length).toBe(0);
}));
it("should reorder the move animation", inject(function($animator, $compile, $rootScope) {
var child1 = $compile('<div>1</div>')($rootScope);
var child2 = $compile('<div>2</div>')($rootScope);
element.append(child1);
element.append(child2);
expect(element.text()).toBe('12');
animator.move(child1, element, child2);
expect(element.text()).toBe('21');
}));
it("should animate the show animation event", inject(function() {
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
expect(element[0].style.display).toBe('');
}));
it("should animate the hide animation event", inject(function() {
element.css('display','block');
expect(element.css('display')).toBe('block');
animator.hide(element);
expect(element.css('display')).toBe('none');
}));
it("should still perform DOM operations even if animations are disabled", inject(function($animator) {
$animator.enabled(false);
element.css('display','block');
expect(element.css('display')).toBe('block');
animator.hide(element);
expect(element.css('display')).toBe('none');
}));
});
describe("with polyfill", function() {
var child, after, window, animator;
beforeEach(function() {
module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
$animationProvider.register('custom', function() {
return {
start: function(element, done) {
done();
}
}
});
$animationProvider.register('custom-delay', function() {
return {
start: function(element, done) {
window.setTimeout(done, 2000);
},
cancel : function(element) {
element.addClass('animation-cancelled');
}
}
});
$animationProvider.register('setup-memo', function() {
return {
setup: function(element) {
return "memento";
},
start: function(element, done, memento) {
element.text(memento);
done();
}
}
});
})
inject(function($animator, $compile, $rootScope, $rootElement) {
element = $compile('<div></div>')($rootScope);
child = $compile('<div></div>')($rootScope);
after = $compile('<div></div>')($rootScope);
$rootElement.append(element);
});
})
it("should animate the enter animation event", inject(function($animator, $rootScope) {
$animator.enabled(true);
animator = $animator($rootScope, {
ngAnimate : '{enter: \'custom\'}'
});
expect(element.contents().length).toBe(0);
animator.enter(child, element);
window.setTimeout.expect(1).process();
}));
it("should animate the leave animation event", inject(function($animator, $rootScope) {
$animator.enabled(true);
animator = $animator($rootScope, {
ngAnimate : '{leave: \'custom\'}'
});
element.append(child);
expect(element.contents().length).toBe(1);
animator.leave(child, element);
window.setTimeout.expect(1).process();
expect(element.contents().length).toBe(0);
}));
it("should animate the move animation event", inject(function($animator, $compile, $rootScope) {
$animator.enabled(true);
animator = $animator($rootScope, {
ngAnimate : '{move: \'custom\'}'
});
$rootScope.$digest();
var child1 = $compile('<div>1</div>')($rootScope);
var child2 = $compile('<div>2</div>')($rootScope);
element.append(child1);
element.append(child2);
expect(element.text()).toBe('12');
animator.move(child1, element, child2);
expect(element.text()).toBe('21');
window.setTimeout.expect(1).process();
}));
it("should animate the show animation event", inject(function($animator, $rootScope) {
$animator.enabled(true);
animator = $animator($rootScope, {
ngAnimate : '{show: \'custom\'}'
});
$rootScope.$digest();
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
expect(element[0].style.display).toBe('');
window.setTimeout.expect(1).process();
expect(element[0].style.display).toBe('');
}));
it("should animate the hide animation event", inject(function($animator, $rootScope) {
$animator.enabled(true);
animator = $animator($rootScope, {
ngAnimate : '{hide: \'custom\'}'
});
$rootScope.$digest();
element.css('display','block');
expect(element.css('display')).toBe('block');
animator.hide(element);
expect(element.css('display')).toBe('block');
window.setTimeout.expect(1).process();
expect(element.css('display')).toBe('none');
}));
it("should assign the ngAnimate string to all events if a string is given",
inject(function($animator, $sniffer, $rootScope) {
$animator.enabled(true);
if (!$sniffer.transitions) return;
animator = $animator($rootScope, {
ngAnimate : '"custom"'
});
$rootScope.$digest();
//enter
animator.enter(child, element);
expect(child.attr('class')).toContain('custom-enter');
window.setTimeout.expect(1).process();
expect(child.attr('class')).toContain('custom-enter-active');
window.setTimeout.expect(0).process();
//leave
element.append(after);
animator.move(child, element, after);
expect(child.attr('class')).toContain('custom-move');
window.setTimeout.expect(1).process();
expect(child.attr('class')).toContain('custom-move-active');
window.setTimeout.expect(0).process();
//hide
animator.hide(child);
expect(child.attr('class')).toContain('custom-hide');
window.setTimeout.expect(1).process();
expect(child.attr('class')).toContain('custom-hide-active');
window.setTimeout.expect(0).process();
//show
animator.show(child);
expect(child.attr('class')).toContain('custom-show');
window.setTimeout.expect(1).process();
expect(child.attr('class')).toContain('custom-show-active');
window.setTimeout.expect(0).process();
//leave
animator.leave(child);
expect(child.attr('class')).toContain('custom-leave');
window.setTimeout.expect(1).process();
expect(child.attr('class')).toContain('custom-leave-active');
window.setTimeout.expect(0).process();
}));
it("should run polyfillSetup and return the memento", inject(function($animator, $rootScope) {
$animator.enabled(true);
animator = $animator($rootScope, {
ngAnimate : '{show: \'setup-memo\'}'
});
$rootScope.$digest();
expect(element.text()).toEqual('');
animator.show(element);
window.setTimeout.expect(1).process();
expect(element.text()).toBe('memento');
}));
it("should not run if animations are disabled", inject(function($animator, $rootScope) {
$animator.enabled(false);
animator = $animator($rootScope, {
ngAnimate : '{show: \'setup-memo\'}'
});
$rootScope.$digest();
element.text('123');
expect(element.text()).toBe('123');
animator.show(element);
expect(element.text()).toBe('123');
$animator.enabled(true);
animator.show(element);
window.setTimeout.expect(1).process();
expect(element.text()).toBe('memento');
}));
it("should only call done() once and right away if another animation takes place in between",
inject(function($animator, $rootScope) {
$animator.enabled(true);
animator = $animator($rootScope, {
ngAnimate : '{hide: \'custom-delay\', leave: \'custom-delay\'}'
});
element.append(child);
child.css('display','block');
animator.hide(child);
window.setTimeout.expect(1).process();
expect(child.css('display')).toBe('block');
animator.leave(child);
expect(child.css('display')).toBe('none'); //hides instantly
//lets change this to prove that done doesn't fire anymore for the previous hide() operation
child.css('display','block');
window.setTimeout.expect(2000).process();
expect(child.css('display')).toBe('block'); //doesn't run the done() method to hide it
expect(element.children().length).toBe(1); //still animating
window.setTimeout.expect(1).process();
window.setTimeout.expect(2000).process();
expect(element.children().length).toBe(0);
}));
it("should call the cancel callback when another animation is called on the same element",
inject(function($animator, $rootScope) {
$animator.enabled(true);
animator = $animator($rootScope, {
ngAnimate : '{hide: \'custom-delay\', show: \'custom-delay\'}'
});
child.css('display','none');
element.data('foo', 'bar');
animator.show(element);
window.setTimeout.expect(1).process();
animator.hide(element);
expect(element.hasClass('animation-cancelled')).toBe(true);
expect(element.data('foo')).toEqual('bar');
}));
it("should NOT clobber all data on an element when animation is finished",
inject(function($animator, $rootScope) {
$animator.enabled(true);
animator = $animator($rootScope, {
ngAnimate : '{hide: \'custom-delay\', show: \'custom-delay\'}'
});
child.css('display','none');
element.data('foo', 'bar');
animator.show(element);
window.setTimeout.expect(1).process();
animator.hide(element);
expect(element.data('foo')).toEqual('bar');
}));
it("should properly animate custom animation events", inject(function($animator, $rootScope) {
$animator.enabled(true);
animator = $animator($rootScope, {
ngAnimate : '{custom: \'setup-memo\'}'
});
element.text('123');
animator.animate('custom',element);
window.setTimeout.expect(1).process();
expect(element.text()).toBe('memento');
}));
});
describe("with CSS3", function() {
var window, animator, prefix, vendorPrefix;
beforeEach(function() {
module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
return function($sniffer, _$rootElement_, $animator) {
vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-';
$rootElement = _$rootElement_;
$animator.enabled(true);
};
})
});
it("should properly animate custom animations for specific animation events",
inject(function($animator, $rootScope, $compile, $sniffer) {
$animator.enabled(true);
var element = $compile(html('<div></div>'))($rootScope);
animator = $animator($rootScope, {
ngAnimate : '{custom: \'special\'}'
});
animator.animate('custom',element);
if($sniffer.transitions) {
expect(element.hasClass('special')).toBe(true);
window.setTimeout.expect(1).process();
expect(element.hasClass('special-active')).toBe(true);
}
else {
expect(window.setTimeout.queue.length).toBe(0);
}
}));
it("should not animate custom animations if not specifically defined",
inject(function($animator, $rootScope, $compile) {
$animator.enabled(true);
var element = $compile(html('<div></div>'))($rootScope);
animator = $animator($rootScope, {
ngAnimate : '{custom: \'special\'}'
});
expect(window.setTimeout.queue.length).toBe(0);
animator.animate('custom1',element);
expect(element.hasClass('special')).toBe(false);
expect(window.setTimeout.queue.length).toBe(0);
}));
it("should properly animate custom animations for general animation events",
inject(function($animator, $rootScope, $compile, $sniffer) {
$animator.enabled(true);
var element = $compile(html('<div></div>'))($rootScope);
animator = $animator($rootScope, {
ngAnimate : "'special'"
});
animator.animate('custom',element);
if($sniffer.transitions) {
expect(element.hasClass('special-custom')).toBe(true);
window.setTimeout.expect(1).process();
expect(element.hasClass('special-custom-active')).toBe(true);
}
else {
expect(window.setTimeout.queue.length).toBe(0);
}
}));
describe("Animations", function() {
it("should properly detect and make use of CSS Animations",
inject(function($animator, $rootScope, $compile, $sniffer) {
var style = 'animation: some_animation 4s linear 0s 1 alternate;' +
vendorPrefix + 'animation: some_animation 4s linear 0s 1 alternate;';
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'inline-show\'}'
});
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
if ($sniffer.animations) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(4000).process();
}
else {
expect(window.setTimeout.queue.length).toBe(0);
}
expect(element[0].style.display).toBe('');
}));
it("should properly detect and make use of CSS Animations with multiple iterations",
inject(function($animator, $rootScope, $compile, $sniffer) {
var style = 'animation-duration: 2s;' +
'animation-iteration-count: 3;' +
vendorPrefix + 'animation-duration: 2s;' +
vendorPrefix + 'animation-iteration-count: 3;';
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'inline-show\'}'
});
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
if ($sniffer.animations) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(6000).process();
}
else {
expect(window.setTimeout.queue.length).toBe(0);
}
expect(element[0].style.display).toBe('');
}));
it("should fallback to the animation duration if an infinite iteration is provided",
inject(function($animator, $rootScope, $compile, $sniffer) {
var style = 'animation-duration: 2s;' +
'animation-iteration-count: infinite;' +
vendorPrefix + 'animation-duration: 2s;' +
vendorPrefix + 'animation-iteration-count: infinite;';
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'inline-show\'}'
});
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
if ($sniffer.animations) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(2000).process();
}
else {
expect(window.setTimeout.queue.length).toBe(0);
}
expect(element[0].style.display).toBe('');
}));
it("should consider the animation delay is provided",
inject(function($animator, $rootScope, $compile, $sniffer) {
var style = 'animation-duration: 2s;' +
'animation-delay: 10s;' +
'animation-iteration-count: 5;' +
vendorPrefix + 'animation-duration: 2s;' +
vendorPrefix + 'animation-delay: 10s;' +
vendorPrefix + 'animation-iteration-count: 5;';
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'inline-show\'}'
});
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(20000).process();
}
else {
expect(window.setTimeout.queue.length).toBe(0);
}
expect(element[0].style.display).toBe('');
}));
it("should skip animations if disabled and run when enabled",
inject(function($animator, $rootScope, $compile, $sniffer) {
$animator.enabled(false);
var style = 'animation: some_animation 2s linear 0s 1 alternate;' +
vendorPrefix + 'animation: some_animation 2s linear 0s 1 alternate;'
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'inline-show\'}'
});
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
expect(element[0].style.display).toBe('');
}));
it("should finish the previous animation when a new animation is started",
inject(function($animator, $rootScope, $compile, $sniffer) {
var style = 'animation: some_animation 2s linear 0s 1 alternate;' +
vendorPrefix + 'animation: some_animation 2s linear 0s 1 alternate;'
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'show\', hide: \'hide\'}'
});
animator.show(element);
if($sniffer.animations) {
window.setTimeout.expect(1).process();
expect(element.hasClass('show')).toBe(true);
expect(element.hasClass('show-active')).toBe(true);
}
else { //animation is skipped
expect(window.setTimeout.queue.length).toBe(0);
}
animator.hide(element);
if(!$sniffer.animations) {
expect(window.setTimeout.queue.length).toBe(0);
}
expect(element.hasClass('show')).toBe(false);
expect(element.hasClass('show-active')).toBe(false);
}));
});
describe("Transitions", function() {
it("should skip transitions if disabled and run when enabled",
inject(function($animator, $rootScope, $compile, $sniffer) {
$animator.enabled(false);
element = $compile(html('<div style="' + vendorPrefix + 'transition: 1s linear all">1</div>'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'inline-show\'}'
});
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
expect(element[0].style.display).toBe('');
$animator.enabled(true);
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(1000).process();
}
else {
expect(window.setTimeout.queue.length).toBe(0);
}
expect(element[0].style.display).toBe('');
}));
it("should skip animations if disabled and run when enabled picking the longest specified duration",
inject(function($animator, $rootScope, $compile, $sniffer) {
$animator.enabled(true);
element = $compile(html('<div style="' + vendorPrefix + 'transition-duration: 1s, 2000ms, 1s; ' + vendorPrefix + 'transition-property: height, left, opacity">foo</div>'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'inline-show\'}'
});
element.css('display','none');
animator.show(element);
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(2000).process();
}
else {
expect(window.setTimeout.queue.length).toBe(0);
}
expect(element[0].style.display).toBe('');
}));
it("should skip animations if disabled and run when enabled picking the longest specified duration/delay combination",
inject(function($animator, $rootScope, $compile, $sniffer) {
$animator.enabled(false);
element = $compile(html('<div style="' + vendorPrefix +
'transition-duration: 1s, 0s, 1s; ' + vendorPrefix +
'transition-delay: 2s, 1000ms, 2s; ' + vendorPrefix +
'transition-property: height, left, opacity">foo</div>'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'inline-show\'}'
});
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
expect(element[0].style.display).toBe('');
$animator.enabled(true);
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(3000).process();
}
else {
expect(window.setTimeout.queue.length).toBe(0);
}
expect(element[0].style.display).toBe('');
}));
it("should select the highest duration and delay",
inject(function($animator, $rootScope, $compile, $sniffer) {
var styles = 'transition:1s linear all 2s;' +
vendorPrefix + 'transition:1s linear all 2s;' +
'animation:my_ani 10s 1s;' +
vendorPrefix + 'animation:my_ani 10s 1s;';
element = $compile(html('<div style="' + styles + '">foo</div>'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'inline-show\'}'
});
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(11000).process();
}
else {
expect(window.setTimeout.queue.length).toBe(0);
}
expect(element[0].style.display).toBe('');
}));
it("should finish the previous transition when a new animation is started",
inject(function($animator, $rootScope, $compile, $sniffer) {
var style = 'transition: 1s linear all;' +
vendorPrefix + 'transition: 1s linear all;'
element = $compile(html('<div style="' + style + '">1</div>'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'show\', hide: \'hide\'}'
});
animator.show(element);
if($sniffer.transitions) {
window.setTimeout.expect(1).process();
expect(element.hasClass('show')).toBe(true);
expect(element.hasClass('show-active')).toBe(true);
}
else { //animation is skipped
expect(window.setTimeout.queue.length).toBe(0);
}
animator.hide(element);
if(!$sniffer.transitions) {
expect(window.setTimeout.queue.length).toBe(0);
}
expect(element.hasClass('show')).toBe(false);
expect(element.hasClass('show-active')).toBe(false);
}));
});
});
describe('anmation evaluation', function () {
it('should re-evaluate the animation expression on each animation', inject(function($animator, $rootScope) {
var parent = jqLite('<div><span></span></div>');
var element = parent.find('span');
$rootScope.animationFn = function () { throw new Error('too early'); };
var animate = $animator($rootScope, { ngAnimate: 'animationFn()' });
var log = '';
$rootScope.animationFn = function () { log = 'abc' };
animate.enter(element, parent);
expect(log).toEqual('abc');
$rootScope.animationFn = function () { log = 'xyz' };
animate.enter(element, parent);
expect(log).toEqual('xyz');
}));
});
it("should throw an error when an invalid ng-animate syntax is provided", inject(function($animator, $rootScope) {
expect(function() {
var animate = $animator($rootScope, { ngAnimate: ':' });
animate.enter();
}).toThrow("[$parse:syntax] Syntax Error: Token ':' not a primary expression at column 1 of the expression [:] starting at [:].");
}));
});

View file

@ -3096,8 +3096,8 @@ describe('$compile', function() {
'</div>')($rootScope);
$rootScope.$digest();
var spans = element.find('span');
expect(spans.eq(0).css('display')).toBe('none');
expect(spans.eq(1).css('display')).toBe('none');
expect(spans.eq(0)).toBeHidden();
expect(spans.eq(1)).toBeHidden();
}));
@ -3216,10 +3216,10 @@ describe('$compile', function() {
'</div>')($rootScope);
$rootScope.$digest();
var spans = element.find('span');
expect(spans.eq(0).css('display')).toBe('none');
expect(spans.eq(1).css('display')).toBe('none');
expect(spans.eq(2).css('display')).toBe('none');
expect(spans.eq(3).css('display')).toBe('none');
expect(spans.eq(0)).toBeHidden();
expect(spans.eq(1)).toBeHidden();
expect(spans.eq(2)).toBeHidden();
expect(spans.eq(3)).toBeHidden();
}));
});
});

View file

@ -75,8 +75,7 @@ describe('ngIf', function () {
});
describe('ngIf ngAnimate', function () {
var vendorPrefix, window;
describe('ngIf animations', function () {
var body, element, $rootElement;
function html(html) {
@ -85,6 +84,8 @@ describe('ngIf ngAnimate', function () {
return element;
}
beforeEach(module('mock.animate'));
beforeEach(module(function() {
// we need to run animation on attached elements;
return function(_$rootElement_) {
@ -99,97 +100,52 @@ describe('ngIf ngAnimate', function () {
dealoc(element);
});
beforeEach(module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
return function($sniffer, $animator) {
vendorPrefix = '-' + $sniffer.vendorPrefix + '-';
$animator.enabled(true);
beforeEach(module(function($animateProvider, $provide) {
return function($animate) {
$animate.enabled(true);
};
}));
it('should fire off the enter animation + add and remove the css classes',
inject(function($compile, $rootScope, $sniffer) {
it('should fire off the enter animation',
inject(function($compile, $rootScope, $animate) {
var item;
var $scope = $rootScope.$new();
var style = vendorPrefix + 'transition: 1s linear all';
element = $compile(html(
'<div>' +
'<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' +
'<div ng-if="value"><div>Hi</div></div>' +
'</div>'
))($scope);
$rootScope.$digest();
$scope.$apply('value = true');
item = $animate.process('enter').element;
expect(item.text()).toBe('Hi');
expect(element.children().length).toBe(1);
var first = element.children()[0];
if ($sniffer.transitions) {
expect(first.className).toContain('custom-enter');
window.setTimeout.expect(1).process();
expect(first.className).toContain('custom-enter-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(first.className).not.toContain('custom-enter');
expect(first.className).not.toContain('custom-enter-active');
}));
it('should fire off the leave animation + add and remove the css classes',
inject(function ($compile, $rootScope, $sniffer) {
it('should fire off the leave animation',
inject(function ($compile, $rootScope, $animate) {
var item;
var $scope = $rootScope.$new();
var style = vendorPrefix + 'transition: 1s linear all';
element = $compile(html(
'<div>' +
'<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' +
'<div ng-if="value"><div>Hi</div></div>' +
'</div>'
))($scope);
$scope.$apply('value = true');
expect(element.children().length).toBe(1);
var first = element.children()[0];
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
item = $animate.process('enter').element;
expect(item.text()).toBe('Hi');
$scope.$apply('value = false');
expect(element.children().length).toBe($sniffer.transitions ? 1 : 0);
expect(element.children().length).toBe(1);
if ($sniffer.transitions) {
expect(first.className).toContain('custom-leave');
window.setTimeout.expect(1).process();
expect(first.className).toContain('custom-leave-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
item = $animate.process('leave').element;
expect(item.text()).toBe('Hi');
expect(element.children().length).toBe(0);
}));
it('should catch and use the correct duration for animation',
inject(function ($compile, $rootScope, $sniffer) {
var $scope = $rootScope.$new();
var style = vendorPrefix + 'transition: 0.5s linear all';
element = $compile(html(
'<div>' +
'<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' +
'</div>'
))($scope);
$scope.$apply('value = true');
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(500).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
}));
});

View file

@ -341,8 +341,7 @@ describe('ngInclude', function() {
});
});
describe('ngInclude ngAnimate', function() {
var vendorPrefix, window;
describe('ngInclude animations', function() {
var body, element, $rootElement;
function html(html) {
@ -351,11 +350,6 @@ describe('ngInclude ngAnimate', function() {
return element;
}
function applyCSS(element, cssProp, cssValue) {
element.css(cssProp, cssValue);
element.css(vendorPrefix + cssProp, cssValue);
}
beforeEach(module(function() {
// we need to run animation on attached elements;
return function(_$rootElement_) {
@ -370,107 +364,51 @@ describe('ngInclude ngAnimate', function() {
dealoc(element);
});
beforeEach(module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
return function($sniffer, $animator) {
vendorPrefix = '-' + $sniffer.vendorPrefix + '-';
$animator.enabled(true);
};
}));
beforeEach(module('mock.animate'));
afterEach(function(){
dealoc(element);
});
it('should fire off the enter animation + add and remove the css classes',
inject(function($compile, $rootScope, $templateCache, $sniffer) {
it('should fire off the enter animation',
inject(function($compile, $rootScope, $templateCache, $animate) {
var item;
$templateCache.put('enter', [200, '<div>data</div>', {}]);
$rootScope.tpl = 'enter';
element = $compile(html(
'<div ' +
'ng-include="tpl" ' +
'ng-animate="{enter: \'custom-enter\'}">' +
'ng-include="tpl">' +
'</div>'
))($rootScope);
$rootScope.$digest();
//if we add the custom css stuff here then it will get picked up before the animation takes place
var child = jqLite(element.children()[0]);
applyCSS(child, 'transition', '1s linear all');
if ($sniffer.transitions) {
expect(child.attr('class')).toContain('custom-enter');
window.setTimeout.expect(1).process();
expect(child.attr('class')).toContain('custom-enter-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(child.attr('class')).not.toContain('custom-enter');
expect(child.attr('class')).not.toContain('custom-enter-active');
item = $animate.process('leave').element;
item = $animate.process('enter').element;
expect(item.text()).toBe('data');
}));
it('should fire off the leave animation + add and remove the css classes',
inject(function($compile, $rootScope, $templateCache, $sniffer) {
it('should fire off the leave animation',
inject(function($compile, $rootScope, $templateCache, $animate) {
var item;
$templateCache.put('enter', [200, '<div>data</div>', {}]);
$rootScope.tpl = 'enter';
element = $compile(html(
'<div ' +
'ng-include="tpl" ' +
'ng-animate="{leave: \'custom-leave\'}">' +
'ng-include="tpl">' +
'</div>'
))($rootScope);
$rootScope.$digest();
//if we add the custom css stuff here then it will get picked up before the animation takes place
var child = jqLite(element.children()[0]);
applyCSS(child, 'transition', '1s linear all');
item = $animate.process('leave').element;
item = $animate.process('enter').element;
expect(item.text()).toBe('data');
$rootScope.tpl = '';
$rootScope.$digest();
if ($sniffer.transitions) {
expect(child.attr('class')).toContain('custom-leave');
window.setTimeout.expect(1).process();
expect(child.attr('class')).toContain('custom-leave-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(child.attr('class')).not.toContain('custom-leave');
expect(child.attr('class')).not.toContain('custom-leave-active');
}));
it('should catch and use the correct duration for animation',
inject(function($compile, $rootScope, $templateCache, $sniffer) {
$templateCache.put('enter', [200, '<div>data</div>', {}]);
$rootScope.tpl = 'enter';
element = $compile(html(
'<div ' +
'ng-include="tpl" ' +
'ng-animate="{enter: \'custom-enter\'}">' +
'</div>'
))($rootScope);
$rootScope.$digest();
//if we add the custom css stuff here then it will get picked up before the animation takes place
var child = jqLite(element.children()[0]);
applyCSS(child, 'transition', '0.5s linear all');
$rootScope.tpl = 'enter';
$rootScope.$digest();
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(500).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
item = $animate.process('leave').element;
expect(item.text()).toBe('data');
}));
});

View file

@ -822,212 +822,6 @@ describe('ngRepeat', function() {
expect(newLis[2]).toEqual(lis[1]);
});
});
});
describe('ngRepeat ngAnimate', function() {
var vendorPrefix, window;
var body, element, $rootElement;
function html(html) {
$rootElement.html(html);
element = $rootElement.children().eq(0);
return element;
}
function applyCSS(element, cssProp, cssValue) {
element.css(cssProp, cssValue);
element.css(vendorPrefix + cssProp, cssValue);
}
beforeEach(module(function() {
// we need to run animation on attached elements;
return function(_$rootElement_) {
$rootElement = _$rootElement_;
body = jqLite(document.body);
body.append($rootElement);
};
}));
afterEach(function(){
dealoc(body);
dealoc(element);
});
beforeEach(module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
return function($sniffer, $animator) {
vendorPrefix = '-' + $sniffer.vendorPrefix + '-';
$animator.enabled(true);
};
}));
it('should fire off the enter animation + add and remove the css classes',
inject(function($compile, $rootScope, $sniffer) {
element = $compile(html(
'<div><div ' +
'ng-repeat="item in items" ' +
'ng-animate="{enter: \'custom-enter\'}">' +
'{{ item }}' +
'</div></div>'
))($rootScope);
$rootScope.$digest(); // re-enable the animations;
$rootScope.items = ['1','2','3'];
$rootScope.$digest();
//if we add the custom css stuff here then it will get picked up before the animation takes place
var kids = element.children();
for(var i=0;i<kids.length;i++) {
kids[i] = jqLite(kids[i]);
applyCSS(kids[i], 'transition', '1s linear all');
}
if ($sniffer.transitions) {
angular.forEach(kids, function(kid) {
expect(kid.attr('class')).toContain('custom-enter');
window.setTimeout.expect(1).process();
});
angular.forEach(kids, function(kid) {
expect(kid.attr('class')).toContain('custom-enter-active');
window.setTimeout.expect(1000).process();
});
} else {
expect(window.setTimeout.queue).toEqual([]);
}
angular.forEach(kids, function(kid) {
expect(kid.attr('class')).not.toContain('custom-enter');
expect(kid.attr('class')).not.toContain('custom-enter-active');
});
}));
it('should fire off the leave animation + add and remove the css classes',
inject(function($compile, $rootScope, $sniffer) {
element = $compile(html(
'<div><div ' +
'ng-repeat="item in items" ' +
'ng-animate="{leave: \'custom-leave\'}">' +
'{{ item }}' +
'</div></div>'
))($rootScope);
$rootScope.items = ['1','2','3'];
$rootScope.$digest();
//if we add the custom css stuff here then it will get picked up before the animation takes place
var kids = element.children();
for(var i=0;i<kids.length;i++) {
kids[i] = jqLite(kids[i]);
applyCSS(kids[i], 'transition', '1s linear all');
}
$rootScope.items = ['1','3'];
$rootScope.$digest();
//the last element gets pushed down when it animates
var kid = jqLite(element.children()[1]);
if ($sniffer.transitions) {
expect(kid.attr('class')).toContain('custom-leave');
window.setTimeout.expect(1).process();
expect(kid.attr('class')).toContain('custom-leave-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(kid.attr('class')).not.toContain('custom-leave');
expect(kid.attr('class')).not.toContain('custom-leave-active');
}));
it('should fire off the move animation + add and remove the css classes',
inject(function($compile, $rootScope, $sniffer) {
element = $compile(html(
'<div>' +
'<div ng-repeat="item in items" ng-animate="{move: \'custom-move\'}">' +
'{{ item }}' +
'</div>' +
'</div>'
))($rootScope);
$rootScope.items = ['1','2','3'];
$rootScope.$digest();
//if we add the custom css stuff here then it will get picked up before the animation takes place
var kids = element.children();
for(var i=0;i<kids.length;i++) {
kids[i] = jqLite(kids[i]);
applyCSS(kids[i], 'transition', '1s linear all');
}
$rootScope.items = ['2','3','1'];
$rootScope.$digest();
//the last element gets pushed down when it animates
kids = element.children();
var first = jqLite(kids[0]);
var left = jqLite(kids[1]);
var right = jqLite(kids[2]);
if ($sniffer.transitions) {
expect(first.attr('class')).toContain('custom-move');
window.setTimeout.expect(1).process();
expect(left.attr('class')).toContain('custom-move');
window.setTimeout.expect(1).process();
expect(first.attr('class')).toContain('custom-move-active');
window.setTimeout.expect(1000).process();
expect(left.attr('class')).toContain('custom-move-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(first.attr('class')).not.toContain('custom-move');
expect(first.attr('class')).not.toContain('custom-move-active');
expect(left.attr('class')).not.toContain('custom-move');
expect(left.attr('class')).not.toContain('custom-move-active');
expect(right.attr('class')).not.toContain('custom-move');
expect(right.attr('class')).not.toContain('custom-move-active');
}));
it('should catch and use the correct duration for animation',
inject(function($compile, $rootScope, $sniffer) {
element = $compile(html(
'<div><div ' +
'ng-repeat="item in items" ' +
'ng-animate="{enter: \'custom-enter\'}">' +
'{{ item }}' +
'</div></div>'
))($rootScope);
$rootScope.$digest(); // re-enable the animations;
$rootScope.items = ['a','b'];
$rootScope.$digest();
//if we add the custom css stuff here then it will get picked up before the animation takes place
var kids = element.children();
var first = jqLite(kids[0]);
var second = jqLite(kids[1]);
var cssProp = 'transition';
var cssValue = '0.5s linear all';
applyCSS(first, cssProp, cssValue);
applyCSS(second, cssProp, cssValue);
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(1).process();
window.setTimeout.expect(500).process();
window.setTimeout.expect(500).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
}));
it('should grow multi-node repeater', inject(function($compile, $rootScope) {
$rootScope.show = false;
@ -1050,3 +844,123 @@ describe('ngRepeat ngAnimate', function() {
});
describe('ngRepeat animations', function() {
var body, element, $rootElement;
function html(html) {
$rootElement.html(html);
element = $rootElement.children().eq(0);
return element;
}
beforeEach(module('mock.animate'));
beforeEach(module(function() {
// we need to run animation on attached elements;
return function(_$rootElement_) {
$rootElement = _$rootElement_;
body = jqLite(document.body);
body.append($rootElement);
};
}));
afterEach(function(){
dealoc(body);
dealoc(element);
});
it('should fire off the enter animation',
inject(function($compile, $rootScope, $animate) {
var item;
element = $compile(html(
'<div><div ' +
'ng-repeat="item in items">' +
'{{ item }}' +
'</div></div>'
))($rootScope);
$rootScope.$digest(); // re-enable the animations;
$rootScope.items = ['1','2','3'];
$rootScope.$digest();
item = $animate.process('enter').element;
expect(item.text()).toBe('1');
item = $animate.process('enter').element;
expect(item.text()).toBe('2');
item = $animate.process('enter').element;
expect(item.text()).toBe('3');
}));
it('should fire off the leave animation',
inject(function($compile, $rootScope, $animate) {
var item;
element = $compile(html(
'<div><div ' +
'ng-repeat="item in items">' +
'{{ item }}' +
'</div></div>'
))($rootScope);
$rootScope.items = ['1','2','3'];
$rootScope.$digest();
item = $animate.process('enter').element;
expect(item.text()).toBe('1');
item = $animate.process('enter').element;
expect(item.text()).toBe('2');
item = $animate.process('enter').element;
expect(item.text()).toBe('3');
$rootScope.items = ['1','3'];
$rootScope.$digest();
item = $animate.process('leave').element;
expect(item.text()).toBe('2');
}));
it('should fire off the move animation',
inject(function($compile, $rootScope, $animate) {
var item;
element = $compile(html(
'<div>' +
'<div ng-repeat="item in items">' +
'{{ item }}' +
'</div>' +
'</div>'
))($rootScope);
$rootScope.items = ['1','2','3'];
$rootScope.$digest();
item = $animate.process('enter').element;
expect(item.text()).toBe('1');
item = $animate.process('enter').element;
expect(item.text()).toBe('2');
item = $animate.process('enter').element;
expect(item.text()).toBe('3');
$rootScope.items = ['2','3','1'];
$rootScope.$digest();
item = $animate.process('move').element;
expect(item.text()).toBe('2');
item = $animate.process('move').element;
expect(item.text()).toBe('1');
}));
});

View file

@ -13,20 +13,20 @@ describe('ngShow / ngHide', function() {
element = jqLite('<div ng-show="exp"></div>');
element = $compile(element)($rootScope);
$rootScope.$digest();
expect(isCssVisible(element)).toEqual(false);
expect(element).toBeHidden();
$rootScope.exp = true;
$rootScope.$digest();
expect(isCssVisible(element)).toEqual(true);
expect(element).toBeShown();
}));
it('should make hidden element visible', inject(function($rootScope, $compile) {
element = jqLite('<div style="display: none" ng-show="exp"></div>');
element = jqLite('<div class="ng-hide" ng-show="exp"></div>');
element = $compile(element)($rootScope);
expect(isCssVisible(element)).toBe(false);
expect(element).toBeHidden();
$rootScope.exp = true;
$rootScope.$digest();
expect(isCssVisible(element)).toBe(true);
expect(element).toBeShown();
}));
});
@ -34,17 +34,15 @@ describe('ngShow / ngHide', function() {
it('should hide an element', inject(function($rootScope, $compile) {
element = jqLite('<div ng-hide="exp"></div>');
element = $compile(element)($rootScope);
expect(isCssVisible(element)).toBe(true);
expect(element).toBeShown();
$rootScope.exp = true;
$rootScope.$digest();
expect(isCssVisible(element)).toBe(false);
expect(element).toBeHidden();
}));
});
});
describe('ngShow / ngHide - ngAnimate', function() {
var window;
var vendorPrefix;
describe('ngShow / ngHide animations', function() {
var body, element, $rootElement;
function html(html) {
@ -65,152 +63,57 @@ describe('ngShow / ngHide - ngAnimate', function() {
body.removeAttr('ng-animation-running');
});
beforeEach(module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
return function($sniffer, _$rootElement_, $animator) {
vendorPrefix = '-' + $sniffer.vendorPrefix + '-';
beforeEach(module('mock.animate'));
beforeEach(module(function($animateProvider, $provide) {
return function(_$rootElement_) {
$rootElement = _$rootElement_;
$animator.enabled(true);
};
}));
describe('ngShow', function() {
it('should fire off the animator.show and animator.hide animation', inject(function($compile, $rootScope, $sniffer) {
it('should fire off the $animate.show and $animate.hide animation', inject(function($compile, $rootScope, $animate) {
var item;
var $scope = $rootScope.$new();
$scope.on = true;
element = $compile(html(
'<div ' +
'style="'+vendorPrefix+'transition: 1s linear all"' +
'ng-show="on" ' +
'ng-animate="{show: \'custom-show\', hide: \'custom-hide\', animateFirst: true}">' +
'</div>'
'<div ng-show="on">data</div>'
))($scope);
$scope.$digest();
if ($sniffer.transitions) {
expect(element.attr('class')).toContain('custom-show');
window.setTimeout.expect(1).process();
expect(element.attr('class')).toContain('custom-show-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(element.attr('class')).not.toContain('custom-show-active');
expect(element.attr('class')).not.toContain('custom-show');
item = $animate.process('show').element;
expect(item.text()).toBe('data');
expect(item).toBeShown();
$scope.on = false;
$scope.$digest();
if ($sniffer.transitions) {
expect(element.attr('class')).toContain('custom-hide');
window.setTimeout.expect(1).process();
expect(element.attr('class')).toContain('custom-hide-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(element.attr('class')).not.toContain('custom-hide-active');
expect(element.attr('class')).not.toContain('custom-hide');
item = $animate.process('hide').element;
expect(item.text()).toBe('data');
expect(item).toBeHidden();
}));
it('should skip animation if parent animation running', function() {
var fired = false;
inject(function($animator, $compile, $rootScope, $sniffer) {
$animator.enabled(true);
$rootScope.$digest();
$rootScope.val = true;
var element = $compile(html('<div ng-show="val" ng-animate="\'animation\'">123</div>'))($rootScope);
$rootElement.controller('ngAnimate').running = true;
element.css('display','none');
expect(element.css('display')).toBe('none');
$rootScope.$digest();
expect(element[0].style.display).toBe('');
expect(fired).toBe(false);
$rootElement.controller('ngAnimate').running = false;
$rootScope.val = false;
$rootScope.$digest();
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(0).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(element[0].style.display).toBe('none');
});
});
});
describe('ngHide', function() {
it('should fire off the animator.show and animator.hide animation', inject(function($compile, $rootScope, $sniffer) {
it('should fire off the $animate.show and $animate.hide animation', inject(function($compile, $rootScope, $animate) {
var item;
var $scope = $rootScope.$new();
$scope.off = true;
element = $compile(html(
'<div ' +
'style="'+vendorPrefix+'transition: 1s linear all"' +
'ng-hide="off" ' +
'ng-animate="{show: \'custom-show\', hide: \'custom-hide\', animateFirst: true}">' +
'</div>'
'<div ng-hide="off">datum</div>'
))($scope);
$scope.$digest();
if ($sniffer.transitions) {
expect(element.attr('class')).toContain('custom-hide');
window.setTimeout.expect(1).process();
expect(element.attr('class')).toContain('custom-hide-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(element.attr('class')).not.toContain('custom-hide-active');
expect(element.attr('class')).not.toContain('custom-hide');
item = $animate.process('hide').element;
expect(item.text()).toBe('datum');
expect(item).toBeHidden();
$scope.off = false;
$scope.$digest();
if ($sniffer.transitions) {
expect(element.attr('class')).toContain('custom-show');
window.setTimeout.expect(1).process();
expect(element.attr('class')).toContain('custom-show-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(element.attr('class')).not.toContain('custom-show-active');
expect(element.attr('class')).not.toContain('custom-show');
item = $animate.process('show').element;
expect(item.text()).toBe('datum');
expect(item).toBeShown();
}));
it('should disable animation when parent animation is running', function() {
var fired = false;
module(function($animationProvider) {
$animationProvider.register('destructive-animation', function() {
return {
setup : function() {},
start : function(element, done) {
fired = true;
}
};
});
});
inject(function($compile, $rootScope) {
$rootScope.val = false;
var element = $compile(html('<div ng-hide="val" ng-animate="{ hide:\'destructive-animation\' }">123</div>'))($rootScope);
$rootElement.controller('ngAnimate').running = true;
element.css('display','block');
expect(element.css('display')).toBe('block');
$rootScope.val = true;
$rootScope.$digest();
expect(element.css('display')).toBe('none');
expect(fired).toBe(false);
});
});
});
});

View file

@ -214,8 +214,7 @@ describe('ngSwitch', function() {
}));
});
describe('ngSwitch ngAnimate', function() {
var vendorPrefix, window;
describe('ngSwitch animations', function() {
var body, element, $rootElement;
function html(html) {
@ -224,6 +223,8 @@ describe('ngSwitch ngAnimate', function() {
return element;
}
beforeEach(module('mock.animate'));
beforeEach(module(function() {
// we need to run animation on attached elements;
return function(_$rootElement_) {
@ -238,23 +239,15 @@ describe('ngSwitch ngAnimate', function() {
dealoc(element);
});
beforeEach(module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
return function($sniffer, $animator) {
vendorPrefix = '-' + $sniffer.vendorPrefix + '-';
$animator.enabled(true);
};
}));
it('should fire off the enter animation + set and remove the classes',
inject(function($compile, $rootScope, $sniffer) {
it('should fire off the enter animation',
inject(function($compile, $rootScope, $animate) {
var item;
var $scope = $rootScope.$new();
var style = vendorPrefix + 'transition: 1s linear all';
element = $compile(html(
'<div ng-switch on="val" ng-animate="{enter: \'cool-enter\', leave: \'cool-leave\'}">' +
'<div ng-switch-when="one" style="' + style + '">one</div>' +
'<div ng-switch-when="two" style="' + style + '">two</div>' +
'<div ng-switch-when="three" style="' + style + '">three</div>' +
'<div ng-switch on="val">' +
'<div ng-switch-when="one">one</div>' +
'<div ng-switch-when="two">two</div>' +
'<div ng-switch-when="three">three</div>' +
'</div>'
))($scope);
@ -262,33 +255,20 @@ describe('ngSwitch ngAnimate', function() {
$scope.val = 'one';
$scope.$digest();
expect(element.children().length).toBe(1);
var first = element.children()[0];
if ($sniffer.transitions) {
expect(first.className).toContain('cool-enter');
window.setTimeout.expect(1).process();
expect(first.className).toContain('cool-enter-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(first.className).not.toContain('cool-enter');
expect(first.className).not.toContain('cool-enter-active');
item = $animate.process('enter').element;
expect(item.text()).toBe('one');
}));
it('should fire off the leave animation + set and remove the classes',
inject(function($compile, $rootScope, $sniffer) {
it('should fire off the leave animation',
inject(function($compile, $rootScope, $animate) {
var item;
var $scope = $rootScope.$new();
var style = vendorPrefix + 'transition: 1s linear all';
element = $compile(html(
'<div ng-switch on="val" ng-animate="{enter: \'cool-enter\', leave: \'cool-leave\'}">' +
'<div ng-switch-when="one" style="' + style + '">one</div>' +
'<div ng-switch-when="two" style="' + style + '">two</div>' +
'<div ng-switch-when="three" style="' + style + '">three</div>' +
'<div ng-switch on="val">' +
'<div ng-switch-when="one">one</div>' +
'<div ng-switch-when="two">two</div>' +
'<div ng-switch-when="three">three</div>' +
'</div>'
))($scope);
@ -296,59 +276,17 @@ describe('ngSwitch ngAnimate', function() {
$scope.val = 'two';
$scope.$digest();
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
item = $animate.process('enter').element;
expect(item.text()).toBe('two');
$scope.val = 'three';
$scope.$digest();
expect(element.children().length).toBe($sniffer.transitions ? 2 : 1);
var first = element.children()[0];
item = $animate.process('leave').element;
expect(item.text()).toBe('two');
if ($sniffer.transitions) {
expect(first.className).toContain('cool-leave');
window.setTimeout.expect(1).process();
window.setTimeout.expect(1).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
if ($sniffer.transitions) {
expect(first.className).toContain('cool-leave-active');
window.setTimeout.expect(1000).process();
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
expect(first.className).not.toContain('cool-leave');
expect(first.className).not.toContain('cool-leave-active');
}));
it('should catch and use the correct duration for animation',
inject(function($compile, $rootScope, $sniffer) {
element = $compile(html(
'<div ng-switch on="val" ng-animate="{enter: \'cool-enter\', leave: \'cool-leave\'}">' +
'<div ng-switch-when="one" style="' + vendorPrefix + 'transition: 0.5s linear all">one</div>' +
'</div>'
))($rootScope);
$rootScope.$digest(); // re-enable the animations;
$rootScope.val = 'one';
$rootScope.$digest();
if ($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect(500).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
item = $animate.process('enter').element;
expect(item.text()).toBe('three');
}));
});

File diff suppressed because it is too large Load diff

View file

@ -7,9 +7,9 @@ describe('ngView', function() {
beforeEach(module(function($provide) {
$provide.value('$window', angular.mock.createMockWindow());
return function($rootScope, $compile, $animator) {
return function($rootScope, $compile, $animate) {
element = $compile('<ng:view onload="load()"></ng:view>')($rootScope);
$animator.enabled(true);
$animate.enabled(true);
};
}));
@ -509,8 +509,7 @@ describe('ngView', function() {
});
});
describe('ngAnimate ', function() {
var window, vendorPrefix;
describe('animations', function() {
var body, element, $rootElement;
function html(html) {
@ -520,11 +519,6 @@ describe('ngView', function() {
return element;
}
function applyCSS(element, cssProp, cssValue) {
element.css(cssProp, cssValue);
element.css(vendorPrefix + cssProp, cssValue);
}
beforeEach(module(function() {
// we need to run animation on attached elements;
return function(_$rootElement_) {
@ -540,128 +534,131 @@ describe('ngView', function() {
beforeEach(module(function($provide, $routeProvider) {
$provide.value('$window', window = angular.mock.createMockWindow());
$routeProvider.when('/foo', {controller: noop, templateUrl: '/foo.html'});
return function($sniffer, $templateCache, $animator) {
vendorPrefix = '-' + $sniffer.vendorPrefix + '-';
return function($templateCache) {
$templateCache.put('/foo.html', [200, '<div>data</div>', {}]);
$animator.enabled(true);
}
}));
it('should fire off the enter animation + add and remove the css classes',
inject(function($compile, $rootScope, $sniffer, $location) {
element = $compile(html('<div ng-view ng-animate="{enter: \'custom-enter\'}"></div>'))($rootScope);
describe('hooks', function() {
beforeEach(module('mock.animate'));
$location.path('/foo');
$rootScope.$digest();
it('should fire off the enter animation',
inject(function($compile, $rootScope, $location, $animate) {
var item;
element = $compile(html('<div ng-view></div>'))($rootScope);
//if we add the custom css stuff here then it will get picked up before the animation takes place
var child = jqLite(element.children()[0]);
applyCSS(child, 'transition', '1s linear all');
$location.path('/foo');
$rootScope.$digest();
if ($sniffer.transitions) {
expect(child.attr('class')).toContain('custom-enter');
window.setTimeout.expect(1).process();
item = $animate.process('leave').element;
item = $animate.process('leave').element;
item = $animate.process('leave').element;
expect(child.attr('class')).toContain('custom-enter-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
item = $animate.process('enter').element;
expect(item.text()).toBe('data');
expect(child.attr('class')).not.toContain('custom-enter');
expect(child.attr('class')).not.toContain('custom-enter-active');
}));
item = $animate.process('leave').element;
item = $animate.process('enter').element;
expect(item.text()).toBe('data');
}));
it('should fire off the leave animation + add and remove the css classes',
inject(function($compile, $rootScope, $sniffer, $location, $templateCache) {
$templateCache.put('/foo.html', [200, '<div>foo</div>', {}]);
element = $compile(html('<div ng-view ng-animate="{leave: \'custom-leave\'}"></div>'))($rootScope);
it('should fire off the leave animation',
inject(function($compile, $rootScope, $location, $templateCache, $animate) {
$location.path('/foo');
$rootScope.$digest();
var item;
$templateCache.put('/foo.html', [200, '<div>foo</div>', {}]);
element = $compile(html('<div ng-view></div>'))($rootScope);
//if we add the custom css stuff here then it will get picked up before the animation takes place
var child = jqLite(element.children()[0]);
applyCSS(child, 'transition', '1s linear all');
$location.path('/foo');
$rootScope.$digest();
$location.path('/');
$rootScope.$digest();
item = $animate.process('leave').element;
item = $animate.process('leave').element;
item = $animate.process('leave').element;
if ($sniffer.transitions) {
expect(child.attr('class')).toContain('custom-leave');
window.setTimeout.expect(1).process();
item = $animate.process('enter').element;
expect(item.text()).toBe('foo');
expect(child.attr('class')).toContain('custom-leave-active');
window.setTimeout.expect(1000).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
item = $animate.process('leave').element;
item = $animate.process('enter').element;
expect(item.text()).toBe('foo');
expect(child.attr('class')).not.toContain('custom-leave');
expect(child.attr('class')).not.toContain('custom-leave-active');
}));
$location.path('/');
$rootScope.$digest();
it('should catch and use the correct duration for animations',
inject(function($compile, $rootScope, $sniffer, $location, $templateCache) {
$templateCache.put('/foo.html', [200, '<div>foo</div>', {}]);
element = $compile(html(
'<div ' +
'ng-view ' +
'ng-animate="{enter: \'customEnter\'}">' +
'</div>'
))($rootScope);
$location.path('/foo');
$rootScope.$digest();
//if we add the custom css stuff here then it will get picked up before the animation takes place
var child = jqLite(element.children()[0]);
applyCSS(child, 'transition', '0.5s linear all');
if($sniffer.transitions) {
window.setTimeout.expect(1).process();
window.setTimeout.expect($sniffer.transitions ? 500 : 0).process();
} else {
expect(window.setTimeout.queue).toEqual([]);
}
}));
item = $animate.process('leave').element;
expect(item.text()).toBe('foo');
item = $animate.process('leave').element;
expect(item.text()).toBe('foo');
}));
});
it('should not double compile when route changes', function() {
module(function($routeProvider, $animationProvider, $provide) {
module('ngAnimate');
module('mock.animate');
module(function($routeProvider, $animateProvider, $provide) {
$routeProvider.when('/foo', {template: '<div ng-repeat="i in [1,2]">{{i}}</div>'});
$routeProvider.when('/bar', {template: '<div ng-repeat="i in [3,4]">{{i}}</div>'});
$animationProvider.register('my-animation-leave', function() {
$animateProvider.register('.my-animation', function() {
return {
start: function(element, done) {
leave: function(element, done) {
dump('yes');
done();
}
};
});
});
inject(function($rootScope, $compile, $location, $route, $window, $rootElement, $sniffer) {
element = $compile(html('<ng:view onload="load()" ng-animate="\'my-animation\'"></ng:view>'))($rootScope);
inject(function($rootScope, $compile, $location, $route, $window, $rootElement, $sniffer, $animate) {
if (!$sniffer.transitions) return;
element = $compile(html('<ng:view onload="load()"></ng:view>'))($rootScope);
$location.path('/foo');
$rootScope.$digest();
if ($sniffer.transitions) {
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(0).process();
}
$animate.process('leave');
$animate.process('leave');
$animate.process('leave');
$animate.process('enter');
$animate.process('leave');
$animate.process('enter');
$animate.process('enter');
$animate.process('enter');
$animate.process('enter');
$animate.process('enter');
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(0).process();
$window.setTimeout.expect(0).process();
$window.setTimeout.expect(0).process();
expect(element.text()).toEqual('12');
$location.path('/bar');
$rootScope.$digest();
$animate.process('leave');
$animate.process('enter');
$animate.process('leave');
$animate.process('enter');
$animate.process('enter');
$animate.process('enter');
$animate.process('enter');
$animate.process('enter');
expect(n(element.text())).toEqual('1234');
if ($sniffer.transitions) {
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(1).process();
} else {
$window.setTimeout.expect(1).process();
}
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(0).process();
$window.setTimeout.expect(0).process();
$window.setTimeout.expect(0).process();
$window.setTimeout.expect(0).process();
expect(element.text()).toEqual('34');
function n(text) {

View file

@ -233,7 +233,7 @@ function sortedHtml(element, showNgClass) {
*/
function isCssVisible(node) {
var display = node.css('display');
return display != 'none';
return !node.hasClass('ng-hide') && display != 'none';
}
function assertHidden(node) {