From 0596fec3a96d3447faeb77ef6e943fe9c8289d7a Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Tue, 10 Jan 2017 19:46:08 +0200 Subject: [PATCH 1/6] docs(guide/animations): list missing animated directives (and other improvements) - List missing animation-aware directives. - Fix/Improve wording/formatting. - Fix typos. - Limit lines to 100 chars. --- docs/content/guide/animations.ngdoc | 416 ++++++++++++++-------------- 1 file changed, 215 insertions(+), 201 deletions(-) diff --git a/docs/content/guide/animations.ngdoc b/docs/content/guide/animations.ngdoc index 8499b9984bc1..5b6846a34038 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. 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. -## 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 **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) { @@ -276,16 +283,17 @@ 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. +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. -### 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 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 +302,42 @@ 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 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. ### 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 +348,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 +376,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 +388,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}. From 035997a162de74acf4f3af26f4697fab926886ac Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 5 Jan 2017 13:59:07 +0200 Subject: [PATCH 2/6] perf($animate): do not retrieve `className` unless `classNameFilter` is used --- src/ngAnimate/animateQueue.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ngAnimate/animateQueue.js b/src/ngAnimate/animateQueue.js index 3dd080325a28..92408ffffce5 100644 --- a/src/ngAnimate/animateQueue.js +++ b/src/ngAnimate/animateQueue.js @@ -165,7 +165,8 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate var classNameFilter = $animateProvider.classNameFilter(); var isAnimatableClassName = !classNameFilter ? function() { return true; } - : function(className) { + : function(node, options) { + var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' '); return classNameFilter.test(className); }; @@ -353,8 +354,7 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate return runner; } - var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' '); - if (!isAnimatableClassName(className)) { + if (!isAnimatableClassName(node, options)) { close(); return runner; } From 71c4d8c49f3d41a9505807b6045ec470990d3fb4 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Fri, 15 Jul 2016 13:37:22 +0300 Subject: [PATCH 3/6] feat($animate): add support for `customFilter` This commit adds a new `customFilter()` function on `$animateProvider` (similar to `classNameFilter()`), which can be used to filter animations (i.e. decide whether they are allowed or not), based on the return value of a custom filter function. 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. Fixes #14891 --- docs/content/guide/animations.ngdoc | 36 ++++++-- src/ng/animate.js | 48 +++++++++++ src/ngAnimate/animateQueue.js | 27 +++--- test/ngAnimate/animateSpec.js | 129 ++++++++++++++++++++++++++++ 4 files changed, 220 insertions(+), 20 deletions(-) diff --git a/docs/content/guide/animations.ngdoc b/docs/content/guide/animations.ngdoc index 5b6846a34038..f6b8eb368144 100644 --- a/docs/content/guide/animations.ngdoc +++ b/docs/content/guide/animations.ngdoc @@ -282,14 +282,37 @@ 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. +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#classNameFilter $animateProvider.classNameFilter()} +### 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. However, the filter +function has to be kept as lean as possible, since it will be executed for each animation. + +### During the config: {@link $animateProvider#classNameFilter $animateProvider.classNameFilter()} + +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 @@ -309,10 +332,11 @@ app.config(function($animateProvider) { } ``` -The `classNameFilter` approach generally gives 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()} diff --git a/src/ng/animate.js b/src/ng/animate.js index be650faee7e3..d5377ec0dd50 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,48 @@ 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:** + * The filtering function will be called for each animation, so try to keep it as lean as + * possible. Performing computationally expensive or time-consuming operations 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 +286,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 92408ffffce5..3f8f1d14779f 100644 --- a/src/ngAnimate/animateQueue.js +++ b/src/ngAnimate/animateQueue.js @@ -160,15 +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(node, options) { - var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' '); - 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); @@ -349,12 +351,9 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate // 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; - } - - if (!isAnimatableClassName(node, options)) { + if (!node || + !isAnimatableByFilter(node, event, initialOptions) || + !isAnimatableClassName(node, options)) { close(); return runner; } diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index ae40e568b2a3..8223a92a8e17 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -307,6 +307,135 @@ describe('animations', function() { }); }); + 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(); + }); + }); + }); + describe('enabled()', function() { it('should work for all animations', inject(function($animate) { From c3397d03ce273e03a5439620b92b468a499717e7 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 27 Apr 2017 14:24:56 +0300 Subject: [PATCH 4/6] fixup! feat($animate): add support for `customFilter` --- docs/content/guide/animations.ngdoc | 13 +++++++++++-- src/ng/animate.js | 9 ++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/content/guide/animations.ngdoc b/docs/content/guide/animations.ngdoc index f6b8eb368144..93fb46370b8f 100644 --- a/docs/content/guide/animations.ngdoc +++ b/docs/content/guide/animations.ngdoc @@ -307,8 +307,17 @@ app.config(function($animateProvider) { ``` 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. However, the filter -function has to be kept as lean as possible, since it will be executed for each animation. +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. +
### During the config: {@link $animateProvider#classNameFilter $animateProvider.classNameFilter()} diff --git a/src/ng/animate.js b/src/ng/animate.js index d5377ec0dd50..1f9bc9028cf0 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -250,9 +250,12 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * *
* **Best Practice:** - * The filtering function will be called for each animation, so try to keep it as lean as - * possible. Performing computationally expensive or time-consuming operations can make your - * animations sluggish. + * 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 From 237874b4944f115cd37c546f59a2d72c08c62903 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 27 Apr 2017 14:11:20 +0300 Subject: [PATCH 5/6] perf(animate): avoid unnecessary computations if animations are globally disabled --- src/ngAnimate/animateQueue.js | 16 ++++++------ test/ngAnimate/animateSpec.js | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/ngAnimate/animateQueue.js b/src/ngAnimate/animateQueue.js index 3f8f1d14779f..327cf0ad24b7 100644 --- a/src/ngAnimate/animateQueue.js +++ b/src/ngAnimate/animateQueue.js @@ -348,10 +348,11 @@ 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 || + // 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(); @@ -362,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 8223a92a8e17..e8c0131a8a16 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -307,6 +307,32 @@ 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(); @@ -434,6 +460,29 @@ describe('animations', function() { 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() { From 33822dd63a08d67511549acaec40dd76c9ea31ff Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Fri, 28 Apr 2017 12:28:08 +0300 Subject: [PATCH 6/6] fixup! docs(guide/animations): list missing animated directives (and other improvements) --- docs/content/guide/animations.ngdoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/content/guide/animations.ngdoc b/docs/content/guide/animations.ngdoc index 93fb46370b8f..7c9d33d3853b 100644 --- a/docs/content/guide/animations.ngdoc +++ b/docs/content/guide/animations.ngdoc @@ -171,17 +171,17 @@ 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 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.) +(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: