diff --git a/docs/content/guide/animations.ngdoc b/docs/content/guide/animations.ngdoc index 8499b9984bc1..7c9d33d3853b 100644 --- a/docs/content/guide/animations.ngdoc +++ b/docs/content/guide/animations.ngdoc @@ -6,20 +6,26 @@ # Animations -AngularJS provides animation hooks for common directives such as `ngRepeat`, `ngSwitch`, and `ngView`, as well as custom directives -via the `$animate` service. These animation hooks are set in place to trigger animations during the life cycle of various directives and when -triggered, will attempt to perform a CSS Transition, CSS Keyframe Animation or a JavaScript callback Animation (depending on if an animation is -placed on the given directive). Animations can be placed using vanilla CSS by following the naming conventions set in place by AngularJS -or with JavaScript code when it's defined as a factory. +AngularJS provides animation hooks for common directives such as +{@link ng.directive:ngRepeat ngRepeat}, {@link ng.directive:ngSwitch ngSwitch}, and +{@link ngRoute.directive:ngView ngView}, as well as custom directives via the `$animate` service. +These animation hooks are set in place to trigger animations during the life cycle of various +directives and when triggered, will attempt to perform a CSS Transition, CSS Keyframe Animation or a +JavaScript callback Animation (depending on whether an animation is placed on the given directive). +Animations can be placed using vanilla CSS by following the naming conventions set in place by +AngularJS or with JavaScript code, defined as a factory.
- Note that we have used non-prefixed CSS transition properties in our examples as the major browsers now support non-prefixed - properties. If you intend to support older browsers or certain mobile browsers then you will need to include prefixed - versions of the transition properties. Take a look at http://caniuse.com/#feat=css-transitions for what browsers require prefixes, - and https://github.com/postcss/autoprefixer for a tool that can automatically generate the prefixes for you. + Note that we have used non-prefixed CSS transition properties in our examples as the major + browsers now support non-prefixed properties. If you intend to support older browsers or certain + mobile browsers then you will need to include prefixed versions of the transition properties. Take + a look at http://caniuse.com/#feat=css-transitions for what browsers require prefixes, and + https://github.com/postcss/autoprefixer for a tool that can automatically generate the prefixes + for you.
-Animations are not available unless you include the {@link ngAnimate `ngAnimate` module} as a dependency within your application. +Animations are not available unless you include the {@link ngAnimate `ngAnimate` module} as a +dependency of your application. Below is a quick example of animations being enabled for `ngShow` and `ngHide`: @@ -59,8 +65,9 @@ You may also want to setup a separate CSS file for defining CSS-based animations ## How they work -Animations in AngularJS are completely based on CSS classes. As long as you have a CSS class attached to a HTML element within -your website, you can apply animations to it. Lets say for example that we have an HTML template with a repeater in it like so: +Animations in AngularJS are completely based on CSS classes. As long as you have a CSS class +attached to a HTML element within your application, you can apply animations to it. Lets say for +example that we have an HTML template with a repeater like so: ```html
@@ -68,22 +75,21 @@ your website, you can apply animations to it. Lets say for example that we have
``` -As you can see, the `.repeated-item` class is present on the element that will be repeated and this class will be -used as a reference within our application's CSS and/or JavaScript animation code to tell AngularJS to perform an animation. +As you can see, the `repeated-item` class is present on the element that will be repeated and this +class will be used as a reference within our application's CSS and/or JavaScript animation code to +tell AngularJS to perform an animation. -As ngRepeat does its thing, each time a new item is added into the list, ngRepeat will add -a `ng-enter` class name to the element that is being added. When removed it will apply a `ng-leave` class name and when moved around -it will apply a `ng-move` class name. +As `ngRepeat` does its thing, each time a new item is added into the list, `ngRepeat` will add an +`ng-enter` class to the element that is being added. When removed it will apply an `ng-leave` class +and when moved around it will apply an `ng-move` class. -Taking a look at the following CSS code, we can see some transition and keyframe animation code set for each of those events that -occur when ngRepeat triggers them: +Taking a look at the following CSS code, we can see some transition and keyframe animation code set +up for each of those events that occur when `ngRepeat` triggers them: ```css /* - We're using CSS transitions for when - the enter and move events are triggered - for the element that has the .repeated-item - class + We are using CSS transitions for when the enter and move events + are triggered for the element that has the `repeated-item` class */ .repeated-item.ng-enter, .repeated-item.ng-move { transition: all 0.5s linear; @@ -91,10 +97,8 @@ occur when ngRepeat triggers them: } /* - The ng-enter-active and ng-move-active - are where the transition destination properties - are set so that the animation knows what to - animate. + `.ng-enter-active` and `.ng-move-active` are where the transition destination + properties are set so that the animation knows what to animate */ .repeated-item.ng-enter.ng-enter-active, .repeated-item.ng-move.ng-move-active { @@ -102,73 +106,64 @@ occur when ngRepeat triggers them: } /* - We're using CSS keyframe animations for when - the leave event is triggered for the element - that has the .repeated-item class + We are using CSS keyframe animations for when the `leave` event + is triggered for the element that has the `repeated-item` class */ .repeated-item.ng-leave { animation: 0.5s my_animation; } @keyframes my_animation { - from { opacity:1; } - to { opacity:0; } + from { opacity: 1; } + to { opacity: 0; } } - ``` -The same approach to animation can be used using JavaScript code (**jQuery is used within to perform animations**): +The same approach to animation can be used using JavaScript code +(**for simplicity, we rely on jQuery to perform animations here**): ```js myModule.animation('.repeated-item', function() { return { enter: function(element, done) { - element.css('opacity',0); - jQuery(element).animate({ - opacity: 1 - }, done); - - // optional onDone or onCancel callback - // function to handle any post-animation - // cleanup operations + // Initialize the element's opacity + element.css('opacity', 0); + + // Animate the element's opacity + // (`element.animate()` is provided by jQuery) + element.animate({opacity: 1}, done); + + // Optional `onDone`/`onCancel` callback function + // to handle any post-animation cleanup operations return function(isCancelled) { - if(isCancelled) { - jQuery(element).stop(); + if (isCancelled) { + // Abort the animation if cancelled + // (`element.stop()` is provided by jQuery) + element.stop(); } - } + }; }, leave: function(element, done) { + // Initialize the element's opacity element.css('opacity', 1); - jQuery(element).animate({ - opacity: 0 - }, done); - // optional onDone or onCancel callback - // function to handle any post-animation - // cleanup operations - return function(isCancelled) { - if(isCancelled) { - jQuery(element).stop(); - } - } - }, - move: function(element, done) { - element.css('opacity', 0); - jQuery(element).animate({ - opacity: 1 - }, done); + // Animate the element's opacity + // (`element.animate()` is provided by jQuery) + element.animate({opacity: 0}, done); - // optional onDone or onCancel callback - // function to handle any post-animation - // cleanup operations + // Optional `onDone`/`onCancel` callback function + // to handle any post-animation cleanup operations return function(isCancelled) { - if(isCancelled) { - jQuery(element).stop(); + if (isCancelled) { + // Abort the animation if cancelled + // (`element.stop()` is provided by jQuery) + element.stop(); } - } + }; }, - // you can also capture these animation events + // We can also capture the following animation events: + move: function(element, done) {}, addClass: function(element, className, done) {}, removeClass: function(element, className, done) {} } @@ -176,74 +171,84 @@ myModule.animation('.repeated-item', function() { ``` With these generated CSS class names present on the element at the time, AngularJS automatically -figures out whether to perform a CSS and/or JavaScript animation. If both CSS and JavaScript animation -code is present, and match the CSS class name on the element, then AngularJS will run both animations at the same time. +figures out whether to perform a CSS and/or JavaScript animation. Note that you can't have both CSS +and JavaScript animations based on the same CSS class. See +{@link ngAnimate#css-js-animations-together here} for more details. -## Class and ngClass animation hooks +## Class and `ngClass` animation hooks -AngularJS also pays attention to CSS class changes on elements by triggering the **add** and **remove** hooks. -This means that if a CSS class is added to or removed from an element then an animation can be executed in between, -before the CSS class addition or removal is finalized. (Keep in mind that AngularJS will only be -able to capture class changes if an **expression** or the **ng-class** directive is used on the element.) +AngularJS also pays attention to CSS class changes on elements by triggering the **add** and +**remove** hooks. This means that if a CSS class is added to or removed from an element then an +animation can be executed in between, before the CSS class addition or removal is finalized. +(Keep in mind that AngularJS will only be able to capture class changes if an +**interpolated expression** or the **ng-class** directive is used on the element.) The example below shows how to perform animations during class changes: - -

- - -
- CSS-Animated Text -

-
- - .css-class-add, .css-class-remove { - transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - } - - .css-class, - .css-class-add.css-class-add-active { - color: red; - font-size:3em; - } - - .css-class-remove.css-class-remove-active { - font-size:1.0em; - color: black; - } - + +

+ + +
+ CSS-Animated Text +

+
+ + .css-class-add, .css-class-remove { + transition: all 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940); + } + + .css-class, + .css-class-add.css-class-add-active { + color: red; + font-size: 3em; + } + + .css-class-remove.css-class-remove-active { + font-size: 1em; + color: black; + } +
Although the CSS is a little different than what we saw before, the idea is the same. ## Which directives support animations? -A handful of common AngularJS directives support and trigger animation hooks whenever any major event occurs during its life cycle. -The table below explains in detail which animation events are triggered - -| 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:ngClass#animations ngClass or {{class}}} | add and remove | -| {@link ng.directive:ngShow#animations ngShow & ngHide} | add and remove (the ng-hide class value) | - -For a full breakdown of the steps involved during each animation event, refer to the {@link ng.$animate API docs}. +A handful of common AngularJS directives support and trigger animation hooks whenever any major +event occurs during their life cycle. The table below explains in detail which animation events are +triggered: + +| Directive | Supported Animations | +|-------------------------------------------------------------------------------|---------------------------------------------------------------------------| +| {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move | +| {@link ng.directive:ngIf#animations ngIf} | enter and leave | +| {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | +| {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | +| {@link ngRoute.directive:ngView#animations ngView} | enter and leave | +| {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave | +| {@link ng.directive:ngClass#animations ngClass / {{class}​}} | add and remove | +| {@link ng.directive:ngClass#animations ngClassEven / ngClassOdd} | add and remove | +| {@link ng.directive:ngHide#animations ngHide} | add and remove (the `ng-hide` class) | +| {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) | +| {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) | +| {@link ng.directive:form#animation-hooks form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) | +| {@link module:ngMessages#animations ngMessages} | add and remove (the `ng-active`/`ng-inactive` classes) | + +For a full breakdown of the steps involved during each animation event, refer to the +{@link ng.$animate API docs}. ## How do I use animations in my own directives? -Animations within custom directives can also be established by injecting `$animate` directly into your directive and -making calls to it when needed. +Animations within custom directives can also be established by injecting `$animate` directly into +your directive and making calls to it when needed. ```js myModule.directive('my-directive', ['$animate', function($animate) { - return function(scope, element, attrs) { + return function(scope, element) { element.on('click', function() { - if(element.hasClass('clicked')) { + if (element.hasClass('clicked')) { $animate.removeClass(element, 'clicked'); } else { $animate.addClass(element, 'clicked'); @@ -255,17 +260,19 @@ myModule.directive('my-directive', ['$animate', function($animate) { ## Animations on app bootstrap / page load -By default, animations are disabled when the AngularJS app {@link guide/bootstrap bootstraps}. If you are using the {@link ngApp} directive, -this happens in the `DOMContentLoaded` event, so immediately after the page has been loaded. -Animations are disabled, so that UI and content are instantly visible. Otherwise, with many animations on -the page, the loading process may become too visually overwhelming, and the performance may suffer. +By default, animations are disabled when the AngularJS app {@link guide/bootstrap bootstraps}. If you +are using the {@link ngApp} directive, this happens in the `DOMContentLoaded` event, so immediately +after the page has been loaded. Animations are disabled, so that UI and content are instantly +visible. Otherwise, with many animations on the page, the loading process may become too visually +overwhelming, and the performance may suffer. -Internally, `ngAnimate` waits until all template downloads that are started right after bootstrap have finished. -Then, it waits for the currently running {@link ng.$rootScope.Scope#$digest} and the one after that to finish. -This ensures that the whole app has been compiled fully before animations are attempted. +Internally, `ngAnimate` waits until all template downloads that are started right after bootstrap +have finished. Then, it waits for the currently running {@link ng.$rootScope.Scope#$digest $digest} +and one more after that, to finish. This ensures that the whole app has been compiled fully before +animations are attempted. -If you do want your animations to play when the app bootstraps, you can enable animations globally in -your main module's {@link angular.Module#run run} function: +If you do want your animations to play when the app bootstraps, you can enable animations globally +in your main module's {@link angular.Module#run run} function: ```js myModule.run(function($animate) { @@ -275,17 +282,50 @@ myModule.run(function($animate) { ## How to (selectively) enable, disable and skip animations -There are three different ways to disable animations, both globally and for specific animations. -Disabling specific animations can help to speed up the render performance, for example for large `ngRepeat` -lists that don't actually have animations. Because ngAnimate checks at runtime if animations are present, -performance will take a hit even if an element has no animation. +There are several different ways to disable animations, both globally and for specific animations. +Disabling specific animations can help to speed up the render performance, for example for large +`ngRepeat` lists that don't actually have animations. Because `ngAnimate` checks at runtime if +animations are present, performance will take a hit even if an element has no animation. + +### During the config: {@link $animateProvider#customFilter $animateProvider.customFilter()} + +This function can be called during the {@link angular.Module#config config} phase of an app. It +takes a filter function as the only argument, which will then be used to "filter" animations (based +on the animated element, the event type, and the animation options). Only when the filter function +returns `true`, will the animation be performed. This allows great flexibility - you can easily +create complex rules, such as allowing specific events only or enabling animations on specific +subtrees of the DOM, and dynamically modify them, for example disabling animations at certain points +in time or under certain circumstances. + +```js +app.config(function($animateProvider) { + $animateProvider.customFilter(function(node, event, options) { + // Example: Only animate `enter` and `leave` operations. + return event === 'enter' || event === 'leave'; + }); +}); +``` + +The `customFilter` approach generally gives a big speed boost compared to other strategies, because +the matching is done before other animation disabling strategies are checked. + +
+ **Best Practice:** + Keep the filtering function as lean as possible, because it will be called for each DOM + action (e.g. insertion, removal, class change) performed by "animation-aware" directives. + See {@link guide/animations#which-directives-support-animations- here} for a list of built-in + directives that support animations. + Performing computationally expensive or time-consuming operations on each call of the + filtering function can make your animations sluggish. +
-### In the config: {@link $animateProvider#classNameFilter $animateProvider.classNameFilter()} +### During the config: {@link $animateProvider#classNameFilter $animateProvider.classNameFilter()} -This function can be called in the {@link angular.Module#config config} phase of an app. It takes a regex as the only argument, -which will then be matched against the classes of any element that is about to be animated. The regex -allows a lot of flexibility - you can either allow animations only for specific classes (useful when -you are working with 3rd party animations), or exclude specific classes from getting animated. +This function too can be called during the {@link angular.Module#config config} phase of an app. It +takes a regex as the only argument, which will then be matched against the classes of any element +that is about to be animated. The regex allows a lot of flexibility - you can either allow +animations for specific classes only (useful when you are working with 3rd party animations), or +exclude specific classes from getting animated. ```js app.config(function($animateProvider) { @@ -294,42 +334,43 @@ app.config(function($animateProvider) { ``` ```css -/* prefixed with animate- */ +/* prefixed with `animate-` */ .animate-fade-add.animate-fade-add-active { transition: all 1s linear; opacity: 0; } ``` -The classNameFilter approach generally applies the biggest speed boost, because the matching is -done before any other animation disabling strategies are checked. However, that also means it is not -possible to override class name matching with the two following strategies. It's of course still possible -to enable / disable animations by changing an element's class name at runtime. +The `classNameFilter` approach generally gives a big speed boost compared to other strategies, +because the matching is done before other animation disabling strategies are checked. However, that +also means it is not possible to override class name matching with the two following strategies. +It's of course still possible to enable / disable animations by changing an element's class name at +runtime. ### At runtime: {@link ng.$animate#enabled $animate.enabled()} This function can be used to enable / disable animations in two different ways: -With a single `boolean` argument, it enables / disables animations globally: `$animate.enabled(false)` -disables all animations in your app. +With a single `boolean` argument, it enables / disables animations globally: +`$animate.enabled(false)` disables all animations in your app. When the first argument is a native DOM or jqLite/jQuery element, the function enables / disables -animations on this element *and all its children*: `$animate.enabled(myElement, false)`. This is the -most flexible way to change the animation state. For example, even if you have used it to disable -animations on a parent element, you can still re-enable it for a child element. And compared to the -`classNameFilter`, you can change the animation status at runtime instead of during the config phase. +animations on this element *and all its children*: `$animate.enabled(myElement, false)`. You can +still use it to re-enable animations for a child element, even if you have disabled them on a parent +element. And compared to the `classNameFilter`, you can change the animation status at runtime +instead of during the config phase. -Note however that the `$animate.enabled()` state for individual elements does not overwrite disabling -rules that have been set in the {@link $animateProvider#classNameFilter classNameFilter}. +Note however that the `$animate.enabled()` state for individual elements does not overwrite +disabling rules that have been set in the {@link $animateProvider#classNameFilter classNameFilter}. ### Via CSS styles: overwriting styles in the `ng-animate` CSS class -Whenever an animation is started, ngAnimate applies the `ng-animate` class to the element for the -whole duration of the animation. By applying CSS transition / animation styling to the class, -you can skip an animation: -```css +Whenever an animation is started, `ngAnimate` applies the `ng-animate` class to the element for the +whole duration of the animation. By applying CSS transition / animation styling to that class, you +can skip an animation: -.my-class{ +```css +.my-class { transition: transform 2s; } @@ -340,23 +381,23 @@ you can skip an animation: my-class.ng-animate { transition: 0s; } - ``` -By setting `transition: 0s`, ngAnimate will ignore the existing transition styles, and not try to animate them (Javascript -animations will still execute, though). This can be used to prevent {@link guide/animations#preventing-collisions-with-existing-animations-and-third-party-libraries -issues with existing animations interfering with ngAnimate}. +By setting `transition: 0s`, `ngAnimate` will ignore the existing transition styles, and not try to +animate them (Javascript animations will still execute, though). This can be used to prevent +{@link guide/animations#preventing-collisions-with-existing-animations-and-third-party-libraries +issues with existing animations interfering with `ngAnimate`}. ## Preventing flicker before an animation starts -When nesting elements with structural animations such as `ngIf` into elements that have class-based -animations such as `ngClass`, it sometimes happens that before the actual animation starts, there is a brief flicker or flash of content -where the animated element is briefly visible. +When nesting elements with structural animations, such as `ngIf`, into elements that have +class-based animations such as `ngClass`, it sometimes happens that before the actual animation +starts, there is a brief flicker or flash of content where the animated element is briefly visible. -To prevent this, you can apply styles to the `ng-[event]-prepare` class, which is added as soon as an animation is initialized, -but removed before the actual animation starts (after waiting for a $digest). This class is only added for *structural* -animations (`enter`, `move`, and `leave`). +To prevent this, you can apply styles to the `ng-[event]-prepare` class, which is added as soon as +an animation is initialized, but removed before the actual animation starts (after waiting for a +`$digest`). This class is only added for *structural* animations (`enter`, `move`, and `leave`). Here's an example where you might see flickering: @@ -368,8 +409,9 @@ Here's an example where you might see flickering: ``` -It is possible that during the `enter` event, the `.message` div will be briefly visible before it starts animating. -In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts: +It is possible that during the `enter` event, the `.message` div will be briefly visible before it +starts animating. In that case, you can add styles to the CSS that make sure the element stays +hidden before the animation starts: ```css .message.ng-enter-prepare { @@ -379,66 +421,71 @@ In that case, you can add styles to the CSS that make sure the element stays hid /* Other animation styles ... */ ``` -## Preventing Collisions with Existing Animations and Third Party Libraries -By default, any `ngAnimate` enabled directives will assume any transition / animation styles on the -element are part of an `ngAnimate` animation. This can lead to problems when the styles are actually -for animations that are independent of `ngAnimate`. +## Preventing collisions with existing animations and third-party libraries -For example, an element acts as a loading spinner. It has an infinite css animation on it, and also an -{@link ngIf `ngIf`} directive, for which no animations are defined: +By default, any `ngAnimate`-enabled directives will assume that `transition` / `animation` styles on +the element are part of an `ngAnimate` animation. This can lead to problems when the styles are +actually for animations that are independent of `ngAnimate`. + +For example, an element acts as a loading spinner. It has an infinite css animation on it, and also +an {@link ngIf `ngIf`} directive, for which no animations are defined: ```css -@keyframes rotating { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } +.spinner { + animation: rotating 2s linear infinite; } -.spinner { - animation: rotating 2s linear infinite; +@keyframes rotating { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } ``` -Now, when the `ngIf` changes, `ngAnimate` will see the spinner animation and use -it to animate the `enter`/`leave` event, which doesn't work because -the animation is infinite. The element will still be added / removed after a timeout, but there will be a -noticable delay. +Now, when the `ngIf` expression changes, `ngAnimate` will see the spinner animation and use it to +animate the `enter`/`leave` event, which doesn't work because the animation is infinite. The element +will still be added / removed after a timeout, but there will be a noticeable delay. -This might also happen because some third-party frameworks place animation duration defaults -across many element or className selectors in order to make their code small and reuseable. +This might also happen because some third-party frameworks place animation duration defaults across +many element or className selectors in order to make their code small and reusable. -You can prevent this unwanted behavior by adding CSS to the `.ng-animate` class that is added -for the whole duration of an animation. Simply overwrite the transition / animation duration. In the +You can prevent this unwanted behavior by adding CSS to the `.ng-animate` class, that is added for +the whole duration of each animation. Simply overwrite the transition / animation duration. In the case of the spinner, this would be: +```css .spinner.ng-animate { - transition: 0s none; - animation: 0s none; + animation: 0s none; + transition: 0s none; } +``` -If you do have CSS transitions / animations defined for the animation events, make sure they have higher priority -than any styles that are independent from ngAnimate. +If you do have CSS transitions / animations defined for the animation events, make sure they have a +higher priority than any styles that are not related to `ngAnimate`. -You can also use one of the two other {@link guide/animations#how-to-selectively-enable-disable-and-skip-animations strategies to disable animations}. +You can also use one of the other +{@link guide/animations#how-to-selectively-enable-disable-and-skip-animations +strategies to disable animations}. -### Enable animations for elements outside of the AngularJS application DOM tree: {@link ng.$animate#pin $animate.pin()} +## Enable animations outside of the application DOM tree: {@link ng.$animate#pin $animate.pin()} -Before animating, `ngAnimate` checks to see if the element being animated is inside the application DOM tree, -and if it is not, no animation is run. Usually, this is not a problem as most apps use the `ngApp` -attribute / bootstrap the app on the `html` or `body` element. +Before animating, `ngAnimate` checks if the animated element is inside the application DOM tree. If +not, no animation is run. Usually, this is not a problem since most apps use the `html` or `body` +elements as their root. Problems arise when the application is bootstrapped on a different element, and animations are -attempted on elements that are outside the application tree, e.g. when libraries append popup and modal -elements as the last child in the body tag. +attempted on elements that are outside the application tree, e.g. when libraries append popup or +modal elements to the body tag. -You can use {@link ng.$animate#pin `$animate.pin(elementToAnimate, parentHost)`} to specify that an -element belongs to your application. Simply call it before the element is added to the DOM / before -the animation starts, with the element you want to animate, and the element which should be its -assumed parent. +You can use {@link ng.$animate#pin `$animate.pin(element, parentHost)`} to associate an element with +another element that belongs to your application. Simply call it before the element is added to the +DOM / before the animation starts, with the element you want to animate, and the element which +should be its assumed parent. ## More about animations -For a full breakdown of each method available on `$animate`, see the {@link ng.$animate API documentation}. +For a full breakdown of each method available on `$animate`, see the +{@link ng.$animate API documentation}. -To see a complete demo, see the {@link tutorial/step_14 animation step within the AngularJS phonecat tutorial}. +To see a complete demo, see the {@link tutorial/step_14 animation step in the phonecat tutorial}. diff --git a/src/ng/animate.js b/src/ng/animate.js index be650faee7e3..1f9bc9028cf0 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -180,6 +180,7 @@ var $$CoreAnimateQueueProvider = /** @this */ function() { var $AnimateProvider = ['$provide', /** @this */ function($provide) { var provider = this; var classNameFilter = null; + var customFilter = null; this.$$registeredAnimations = Object.create(null); @@ -232,6 +233,51 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { $provide.factory(key, factory); }; + /** + * @ngdoc method + * @name $animateProvider#customFilter + * + * @description + * Sets and/or returns the custom filter function that is used to "filter" animations, i.e. + * determine if an animation is allowed or not. When no filter is specified (the default), no + * animation will be blocked. Setting the `customFilter` value will only allow animations for + * which the filter function's return value is truthy. + * + * This allows to easily create arbitrarily complex rules for filtering animations, such as + * allowing specific events only, or enabling animations on specific subtrees of the DOM, etc. + * Filtering animations can also boost performance for low-powered devices, as well as + * applications containing a lot of structural operations. + * + *
+ * **Best Practice:** + * Keep the filtering function as lean as possible, because it will be called for each DOM + * action (e.g. insertion, removal, class change) performed by "animation-aware" directives. + * See {@link guide/animations#which-directives-support-animations- here} for a list of built-in + * directives that support animations. + * Performing computationally expensive or time-consuming operations on each call of the + * filtering function can make your animations sluggish. + *
+ * + * **Note:** If present, `customFilter` will be checked before + * {@link $animateProvider#classNameFilter classNameFilter}. + * + * @param {Function=} filterFn - The filter function which will be used to filter all animations. + * If a falsy value is returned, no animation will be performed. The function will be called + * with the following arguments: + * - **node** `{DOMElement}` - The DOM element to be animated. + * - **event** `{String}` - The name of the animation event (e.g. `enter`, `leave`, `addClass` + * etc). + * - **options** `{Object}` - A collection of options/styles used for the animation. + * @return {Function} The current filter function or `null` if there is none set. + */ + this.customFilter = function(filterFn) { + if (arguments.length === 1) { + customFilter = isFunction(filterFn) ? filterFn : null; + } + + return customFilter; + }; + /** * @ngdoc method * @name $animateProvider#classNameFilter @@ -243,6 +289,11 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * When setting the `classNameFilter` value, animations will only be performed on elements * that successfully match the filter expression. This in turn can boost performance * for low-powered devices as well as applications containing a lot of structural operations. + * + * **Note:** If present, `classNameFilter` will be checked after + * {@link $animateProvider#customFilter customFilter}. If `customFilter` is present and returns + * false, `classNameFilter` will not be checked. + * * @param {RegExp=} expression The className expression which will be checked against all animations * @return {RegExp} The current CSS className expression value. If null then there is no expression value */ diff --git a/src/ngAnimate/animateQueue.js b/src/ngAnimate/animateQueue.js index 3dd080325a28..327cf0ad24b7 100644 --- a/src/ngAnimate/animateQueue.js +++ b/src/ngAnimate/animateQueue.js @@ -160,14 +160,17 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate var callbackRegistry = Object.create(null); - // remember that the classNameFilter is set during the provider/config - // stage therefore we can optimize here and setup a helper function + // remember that the `customFilter`/`classNameFilter` are set during the + // provider/config stage therefore we can optimize here and setup helper functions + var customFilter = $animateProvider.customFilter(); var classNameFilter = $animateProvider.classNameFilter(); - var isAnimatableClassName = !classNameFilter - ? function() { return true; } - : function(className) { - return classNameFilter.test(className); - }; + var returnTrue = function() { return true; }; + + var isAnimatableByFilter = customFilter || returnTrue; + var isAnimatableClassName = !classNameFilter ? returnTrue : function(node, options) { + var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' '); + return classNameFilter.test(className); + }; var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); @@ -345,16 +348,13 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate options.to = null; } - // there are situations where a directive issues an animation for - // a jqLite wrapper that contains only comment nodes... If this - // happens then there is no way we can perform an animation - if (!node) { - close(); - return runner; - } - - var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' '); - if (!isAnimatableClassName(className)) { + // If animations are hard-disabled for the whole application there is no need to continue. + // There are also situations where a directive issues an animation for a jqLite wrapper that + // contains only comment nodes. In this case, there is no way we can perform an animation. + if (!animationsEnabled || + !node || + !isAnimatableByFilter(node, event, initialOptions) || + !isAnimatableClassName(node, options)) { close(); return runner; } @@ -363,12 +363,11 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate var documentHidden = $$isDocumentHidden(); - // this is a hard disable of all animations for the application or on - // the element itself, therefore there is no need to continue further - // past this point if not enabled + // This is a hard disable of all animations the element itself, therefore there is no need to + // continue further past this point if not enabled // Animations are also disabled if the document is currently hidden (page is not visible // to the user), because browsers slow down or do not flush calls to requestAnimationFrame - var skipAnimations = !animationsEnabled || documentHidden || disabledElementsLookup.get(node); + var skipAnimations = documentHidden || disabledElementsLookup.get(node); var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {}; var hasExistingAnimation = !!existingAnimation.state; diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index ae40e568b2a3..e8c0131a8a16 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -307,6 +307,184 @@ describe('animations', function() { }); }); + it('should not try to match the `classNameFilter` RegExp if animations are globally disabled', + function() { + var regex = /foo/; + var regexTestSpy = spyOn(regex, 'test').and.callThrough(); + + module(function($animateProvider) { + $animateProvider.classNameFilter(regex); + }); + + inject(function($animate) { + $animate.addClass(element, 'foo'); + expect(regexTestSpy).toHaveBeenCalled(); + + regexTestSpy.calls.reset(); + $animate.enabled(false); + $animate.addClass(element, 'bar'); + expect(regexTestSpy).not.toHaveBeenCalled(); + + regexTestSpy.calls.reset(); + $animate.enabled(true); + $animate.addClass(element, 'baz'); + expect(regexTestSpy).toHaveBeenCalled(); + }); + } + ); + + describe('customFilter()', function() { + it('should be `null` by default', module(function($animateProvider) { + expect($animateProvider.customFilter()).toBeNull(); + })); + + it('should clear the `customFilter` if no function is passed', + module(function($animateProvider) { + $animateProvider.customFilter(angular.noop); + expect($animateProvider.customFilter()).toEqual(jasmine.any(Function)); + + $animateProvider.customFilter(null); + expect($animateProvider.customFilter()).toBeNull(); + + $animateProvider.customFilter(angular.noop); + expect($animateProvider.customFilter()).toEqual(jasmine.any(Function)); + + $animateProvider.customFilter({}); + expect($animateProvider.customFilter()).toBeNull(); + }) + ); + + it('should only perform animations for which the function returns a truthy value', + function() { + var animationsAllowed = false; + + module(function($animateProvider) { + $animateProvider.customFilter(function() { return animationsAllowed; }); + }); + + inject(function($animate, $rootScope) { + $animate.enter(element, parent); + $rootScope.$digest(); + expect(capturedAnimation).toBeNull(); + + $animate.leave(element, parent); + $rootScope.$digest(); + expect(capturedAnimation).toBeNull(); + + animationsAllowed = true; + + $animate.enter(element, parent); + $rootScope.$digest(); + expect(capturedAnimation).not.toBeNull(); + + capturedAnimation = null; + + $animate.leave(element, parent); + $rootScope.$digest(); + expect(capturedAnimation).not.toBeNull(); + }); + } + ); + + it('should only perform animations for which the function returns a truthy value (SVG)', + function() { + var animationsAllowed = false; + + module(function($animateProvider) { + $animateProvider.customFilter(function() { return animationsAllowed; }); + }); + + inject(function($animate, $compile, $rootScope) { + var svgElement = $compile('')($rootScope); + + $animate.enter(svgElement, parent); + $rootScope.$digest(); + expect(capturedAnimation).toBeNull(); + + $animate.leave(svgElement, parent); + $rootScope.$digest(); + expect(capturedAnimation).toBeNull(); + + animationsAllowed = true; + + $animate.enter(svgElement, parent); + $rootScope.$digest(); + expect(capturedAnimation).not.toBeNull(); + + capturedAnimation = null; + + $animate.leave(svgElement, parent); + $rootScope.$digest(); + expect(capturedAnimation).not.toBeNull(); + }); + } + ); + + it('should pass the DOM element, event name and options to the filter function', function() { + var filterFn = jasmine.createSpy('filterFn'); + var options = {}; + + module(function($animateProvider) { + $animateProvider.customFilter(filterFn); + }); + + inject(function($animate, $rootScope) { + $animate.enter(element, parent, null, options); + expect(filterFn).toHaveBeenCalledOnceWith(element[0], 'enter', options); + + filterFn.calls.reset(); + + $animate.leave(element); + expect(filterFn).toHaveBeenCalledOnceWith(element[0], 'leave', jasmine.any(Object)); + }); + }); + + it('should complete the DOM operation even if filtered out', function() { + module(function($animateProvider) { + $animateProvider.customFilter(function() { return false; }); + }); + + inject(function($animate, $rootScope) { + expect(element.parent()[0]).toBeUndefined(); + + $animate.enter(element, parent); + $rootScope.$digest(); + + expect(capturedAnimation).toBeNull(); + expect(element.parent()[0]).toBe(parent[0]); + + $animate.leave(element); + $rootScope.$digest(); + + expect(capturedAnimation).toBeNull(); + expect(element.parent()[0]).toBeUndefined(); + }); + }); + + it('should not execute the function if animations are globally disabled', function() { + var customFilterSpy = jasmine.createSpy('customFilterFn'); + + module(function($animateProvider) { + $animateProvider.customFilter(customFilterSpy); + }); + + inject(function($animate) { + $animate.addClass(element, 'foo'); + expect(customFilterSpy).toHaveBeenCalled(); + + customFilterSpy.calls.reset(); + $animate.enabled(false); + $animate.addClass(element, 'bar'); + expect(customFilterSpy).not.toHaveBeenCalled(); + + customFilterSpy.calls.reset(); + $animate.enabled(true); + $animate.addClass(element, 'baz'); + expect(customFilterSpy).toHaveBeenCalled(); + }); + }); + }); + describe('enabled()', function() { it('should work for all animations', inject(function($animate) {