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-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) {