|
| 1 | +@ngdoc overview |
| 2 | +@name Tutorial: 12 - Applying Animations |
| 3 | +@description |
| 4 | + |
| 5 | +<ul doc-tutorial-nav="12"></ul> |
| 6 | + |
| 7 | + |
| 8 | +In this final step, we will enhance our phonecat web application by attaching CSS and JavaScript |
| 9 | +animations on top of the template code we created before. |
| 10 | + |
| 11 | + |
| 12 | +<div doc-tutorial-reset="12"></div> |
| 13 | + |
| 14 | + |
| 15 | +Now that everything is set in place for a fully functional web application, we can attach CSS and JavaScript |
| 16 | +animations to common directives that are used to render our application. AngularJS comes bundled with an |
| 17 | +additional JavaScript file called `angular-animate.js` which, when included into the website and set as |
| 18 | +a dependency with the application module, will enable animations throughout the application. |
| 19 | + |
| 20 | +Common `ng` directives automatically trigger hooks for animations to tap into. When an animation is found |
| 21 | +then the animation will run in between the standard DOM operation that is being issued on the element at |
| 22 | +the given time (e.g. inserting and removing nodes on ngRepeat or adding and removing classes on ngClass). |
| 23 | + |
| 24 | +The most important changes are listed below. You can see the full diff on |
| 25 | +{@link https://github.com/angular/angular-phonecat/compare/step-11...step-12 GitHub}: |
| 26 | + |
| 27 | + |
| 28 | +## How Animations work with `ngAnimate` |
| 29 | + |
| 30 | +To get an idea of how animations work with AngularJS, please read the |
| 31 | +{@link guide/animations AngularJS Animation Guide} first. |
| 32 | + |
| 33 | + |
| 34 | +## Template |
| 35 | + |
| 36 | +The changes required within the HTML template code is to link the asset files which define the animations as well |
| 37 | +as the `angular-animate.js` file. The animation module, known as `ngAnimate`, is defined within |
| 38 | +`angular-animate.js` and contains the code necessary to make your application become animation aware. |
| 39 | + |
| 40 | +Here's what needs to changed in the index file: |
| 41 | + |
| 42 | +__`app/index.html`.__ |
| 43 | +<pre> |
| 44 | +... |
| 45 | + <!-- jQuery is used for JavaScript animations (include this before angular.js) --> |
| 46 | + <script src="http://code.jquery.com/jquery-2.0.3.min.js"></script> |
| 47 | + |
| 48 | + <!-- required module to enable animation support in AngularJS --> |
| 49 | + <script src="lib/angular/angular-animate.js"></script> |
| 50 | + |
| 51 | + <!-- for JavaScript Animations --> |
| 52 | + <script src="js/animations.js"></script> |
| 53 | + |
| 54 | + <!-- for CSS Transitions and/or Keyframe Animations --> |
| 55 | + <link rel="stylesheet" href="css/animations.css"> |
| 56 | +... |
| 57 | +</pre> |
| 58 | + |
| 59 | +Animations can now be created within the CSS code (`animations.css`) as well as the JavaScript code (`animations.js`). |
| 60 | +But before we start, let's create a new module which uses the ngAnimate module as a dependency just like we did before |
| 61 | +with `ngResource`. |
| 62 | + |
| 63 | +## Module & Animations |
| 64 | + |
| 65 | +__`app/js/animations.js`.__ |
| 66 | +<pre> |
| 67 | +angular.module('phonecatAnimations', ['ngAnimate']). |
| 68 | + // ... |
| 69 | + // this module will later be used to define animations |
| 70 | + // ... |
| 71 | +</pre> |
| 72 | + |
| 73 | +And now let's attach this module to our application module... |
| 74 | + |
| 75 | +__`app/js/app.js`.__ |
| 76 | +<pre> |
| 77 | +// ... |
| 78 | +angular.module('phonecat', [ |
| 79 | + 'ngRoute', |
| 80 | + |
| 81 | + 'phonecatAnimations', |
| 82 | + 'phonecatControllers', |
| 83 | + 'phonecatFilters', |
| 84 | + 'phonecatServices', |
| 85 | +]). |
| 86 | +// ... |
| 87 | +</pre> |
| 88 | + |
| 89 | +Now, the phonecat module is animation aware. Let's make some animations! |
| 90 | + |
| 91 | + |
| 92 | +## Animating ngRepeat with CSS Transition Animations |
| 93 | + |
| 94 | +We'll start off by adding CSS transition animations to our `ngRepeat` directive present on the `phone-list.html` page. |
| 95 | +First let's add an extra CSS class to our repeated element so that we can hook into it with our CSS animation code. |
| 96 | + |
| 97 | +__`app/partials/phone-list.html`.__ |
| 98 | +<pre> |
| 99 | +<!-- |
| 100 | + Let's change the repeater HTML to include a new CSS class |
| 101 | + which we will later use for animations: |
| 102 | +--> |
| 103 | +<ul class="phones"> |
| 104 | + <li ng-repeat="phone in phones | filter:query | orderBy:orderProp" |
| 105 | + class="thumbnail phone-listing"> |
| 106 | + <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a> |
| 107 | + <a href="#/phones/{{phone.id}}">{{phone.name}}</a> |
| 108 | + <p>{{phone.snippet}}</p> |
| 109 | + </li> |
| 110 | +</ul> |
| 111 | + |
| 112 | +</pre> |
| 113 | + |
| 114 | +Notice how we added the `phone-listing` CSS class? This is all we need in our HTML code to get animations working. |
| 115 | + |
| 116 | +Now for the actual CSS transition animation code: |
| 117 | + |
| 118 | +__`app/css/animations.css`__ |
| 119 | +<pre> |
| 120 | +.phone-listing.ng-enter, |
| 121 | +.phone-listing.ng-leave, |
| 122 | +.phone-listing.ng-move { |
| 123 | + -webkit-transition: 0.5s linear all; |
| 124 | + -moz-transition: 0.5s linear all; |
| 125 | + -o-transition: 0.5s linear all; |
| 126 | + transition: 0.5s linear all; |
| 127 | +} |
| 128 | + |
| 129 | +.phone-listing.ng-enter, |
| 130 | +.phone-listing.ng-move { |
| 131 | + opacity: 0; |
| 132 | + height: 0; |
| 133 | + overflow: hidden; |
| 134 | +} |
| 135 | + |
| 136 | +.phone-listing.ng-move.ng-move-active, |
| 137 | +.phone-listing.ng-enter.ng-enter-active { |
| 138 | + opacity: 1; |
| 139 | + height: 120px; |
| 140 | +} |
| 141 | + |
| 142 | +.phone-listing.ng-leave { |
| 143 | + opacity: 1; |
| 144 | + overflow: hidden; |
| 145 | +} |
| 146 | + |
| 147 | +.phone-listing.ng-leave.ng-leave-active { |
| 148 | + opacity: 0; |
| 149 | + height: 0; |
| 150 | + padding-top: 0; |
| 151 | + padding-bottom: 0; |
| 152 | +} |
| 153 | +</pre> |
| 154 | + |
| 155 | +As you can see our `phone-listing` CSS class is combined together with the animation hooks that occur when items are |
| 156 | +inserted info and removed from the list: |
| 157 | + |
| 158 | + * The `ng-enter` class is applied to the element when a new phone is added to the list and rendered on the page. |
| 159 | + * The `ng-move` class is applied when items are moved around in the list. |
| 160 | + * The `ng-leave` class is applied when they're removed from the list. |
| 161 | + |
| 162 | +The phone listing items are added and removed depending on the data passed to the `ng-repeat` attribute. |
| 163 | +For example, if the filter data changes the items will be animated in and out of the repeat list. |
| 164 | + |
| 165 | +Something important to note is that when an animation occurs, two sets of CSS classes |
| 166 | +are added to the element: |
| 167 | + |
| 168 | + 1. a "starting" class that represents the style at the beginning of the animation |
| 169 | + 2. an "active" class that represents the style at the end of the animation |
| 170 | + |
| 171 | +The name of the starting class is the name of event that is fired (like `enter`, `move` or `leave`) prefixed with |
| 172 | +`ng-`. So an `enter` event will result in a class called `ng-enter`. |
| 173 | + |
| 174 | +The active class name is the same as the starting class's but with an `-active` suffix. |
| 175 | +This two-class CSS naming convention allows the developer to craft an animation, beginning to end. |
| 176 | + |
| 177 | +In our example above, elements expand from a height of **0** to **120 pixels** when items are added or moved, |
| 178 | +around and collapsing the items before removing them from the list. |
| 179 | +There's also a nice fade-in and fade-out effect that also occurs at the same time. All of this is handled |
| 180 | +by the CSS transition declarations at the top of the example code above. |
| 181 | + |
| 182 | +Although most modern browsers have good support for {@link http://caniuse.com/#feat=css-transitions CSS transitions} |
| 183 | +and {@link http://caniuse.com/#feat=css-animation CSS animations}, IE9 and earlier do not. |
| 184 | +If you want animations that are backwards-compatible with older browsers, consider using JavaScript-based animations, |
| 185 | +which are described in detail below. |
| 186 | + |
| 187 | + |
| 188 | +## Animating `ngView` with CSS Keyframe Animations |
| 189 | + |
| 190 | +Next let's add an animation for transitions between route changes in `ngView`. |
| 191 | + |
| 192 | +To start, let's add a new CSS class to our HTML like we did in the example above. |
| 193 | +This time, instead of the `ng-repeat` element, let's add it to the element containing the ng-view directive. |
| 194 | +In order to do this, we'll have to make some small changes to the HTML code so that we can have more control over our |
| 195 | +animations between view changes. |
| 196 | + |
| 197 | +__`app/partials/phone-list.html`.__ |
| 198 | +<pre> |
| 199 | +<div class="view-container"> |
| 200 | + <div ng-view class="view-frame"></div> |
| 201 | +</div> |
| 202 | +</pre> |
| 203 | + |
| 204 | +With this change, the `ng-view` directive is nested inside a parent element with |
| 205 | +a `view-container` CSS class. This class adds a `position: relative` style so that the positioning of the `ng-view` |
| 206 | +is relative to this parent as it animates transitions. |
| 207 | + |
| 208 | +With this in place, let's add the CSS for this transition animation to our `animations.css` file: |
| 209 | + |
| 210 | +__`app/css/animations.css`.__ |
| 211 | +<pre> |
| 212 | +.view-container { |
| 213 | + position: relative; |
| 214 | +} |
| 215 | + |
| 216 | +.view-frame.ng-enter, .view-frame.ng-leave { |
| 217 | + background: white; |
| 218 | + position: absolute; |
| 219 | + top: 0; |
| 220 | + left: 0; |
| 221 | + right: 0; |
| 222 | +} |
| 223 | + |
| 224 | +.view-frame.ng-enter { |
| 225 | + -webkit-animation: 0.5s fade-in; |
| 226 | + -moz-animation: 0.5s fade-in; |
| 227 | + -o-animation: 0.5s fade-in; |
| 228 | + animation: 0.5s fade-in; |
| 229 | + z-index: 100; |
| 230 | +} |
| 231 | + |
| 232 | +.view-frame.ng-leave { |
| 233 | + -webkit-animation: 0.5s fade-out; |
| 234 | + -moz-animation: 0.5s fade-out; |
| 235 | + -o-animation: 0.5s fade-out; |
| 236 | + animation: 0.5s fade-out; |
| 237 | + z-index:99; |
| 238 | +} |
| 239 | + |
| 240 | +@keyframes fade-in { |
| 241 | + from { opacity: 0; } |
| 242 | + to { opacity: 1; } |
| 243 | +} |
| 244 | +@-moz-keyframes fade-in { |
| 245 | + from { opacity: 0; } |
| 246 | + to { opacity: 1; } |
| 247 | +} |
| 248 | +@-webkit-keyframes fade-in { |
| 249 | + from { opacity: 0; } |
| 250 | + to { opacity: 1; } |
| 251 | +} |
| 252 | + |
| 253 | +@keyframes fade-out { |
| 254 | + from { opacity: 1; } |
| 255 | + to { opacity: 0; } |
| 256 | +} |
| 257 | +@-moz-keyframes fade-out { |
| 258 | + from { opacity: 1; } |
| 259 | + to { opacity: 0; } |
| 260 | +} |
| 261 | +@-webkit-keyframes fade-out { |
| 262 | + from { opacity: 1; } |
| 263 | + to { opacity: 0; } |
| 264 | +} |
| 265 | + |
| 266 | +/* don't forget about the vendor-prefixes! */ |
| 267 | +</pre> |
| 268 | + |
| 269 | +Nothing crazy here! Just a simple fade in and fade out effect between pages. The only out of the ordinary thing |
| 270 | +here is that we're using absolute positioning to position the next page (identified via `ng-enter`) on top of the |
| 271 | +previous page (the one that has the `ng-leave` class) while performing a cross fade animation in between. So |
| 272 | +as the previous page is just about to be removed, it fades out while the new page fades in right on top of it. |
| 273 | +Once the leave animation is over then element is removed and once the enter animation is complete then the |
| 274 | +`ng-enter` and `ng-enter-active` CSS classes are removed from the element rendering it to be position itself |
| 275 | +with its default CSS code (so no more absolute positioning once the animation is over). This works fluidly so |
| 276 | +that pages flow naturally between route changes without anything jumping around. |
| 277 | + |
| 278 | +The CSS classes applied (the start and end classes) are much the same as with `ng-repeat`. Each time a new page is |
| 279 | +loaded the ng-view directive will create a copy of itself, download the template and append the contents. This |
| 280 | +ensures that all views are contained within a single HTML element which allows for easy animation control. |
| 281 | + |
| 282 | +For more on CSS animations, see the |
| 283 | +{@link http://docs.webplatform.org/wiki/css/properties/animations Web Platform documentation}. |
| 284 | + |
| 285 | + |
| 286 | +## Animating `ngClass` with JavaScript |
| 287 | + |
| 288 | +Let's add another animation to our application. Switching to our `phone-detail.html` page, |
| 289 | +we see that we have a nice thumbnail swapper. By clicking on the thumbnails listed on the page, |
| 290 | +the profile phone image changes. But how can we change this around to add animations? |
| 291 | + |
| 292 | +Lets think about it first, |
| 293 | +basically when you click on a thumbnail image, you're changing the state of the profile image to reflect the newly |
| 294 | +selected thumbnail image. |
| 295 | +The best way to specify state changes within HTML is to use classes. |
| 296 | +Much like before, how we used a CSS class to specify |
| 297 | +an animation, this time the animation will occur whenever the CSS class itself changes. |
| 298 | + |
| 299 | +Whenever a new phone thumbnail is selected, the state changes and the `.active` CSS class is added to the matching |
| 300 | +profile image and the animation plays. |
| 301 | + |
| 302 | +Let's get started and tweak our HTML code on the `phone-detail.html` page first: |
| 303 | + |
| 304 | +__`app/partials/phone-detail.html`.__ |
| 305 | +<pre> |
| 306 | +<!-- We're only changing the top of the file --> |
| 307 | +<div class="phone-images"> |
| 308 | + <img ng-src="{{img}}" |
| 309 | + class="phone" |
| 310 | + ng-repeat="img in phone.images" |
| 311 | + ng-class="{active:mainImageUrl==img}"> |
| 312 | +</div> |
| 313 | + |
| 314 | +<h1>{{phone.name}}</h1> |
| 315 | + |
| 316 | +<p>{{phone.description}}</p> |
| 317 | + |
| 318 | +<ul class="phone-thumbs"> |
| 319 | + <li ng-repeat="img in phone.images"> |
| 320 | + <img ng-src="{{img}}" ng-mouseenter="setImage(img)"> |
| 321 | + </li> |
| 322 | +</ul> |
| 323 | +</pre> |
| 324 | + |
| 325 | +Just like with the thumbnails, we're using a repeater to display **all** the profile images as a list, however we're |
| 326 | +not animating any repeat-related animations. Instead, we're keeping our eye on the ng-class directive since whenever |
| 327 | +the `active` class is true then it will be applied to the element and will render as visible. Otherwise, the profile image |
| 328 | +is hidden. In our case, there is always one element that has the active class, and, therefore, there will always |
| 329 | +be one phone profile image visible on screen at all times. |
| 330 | + |
| 331 | +When the active class is added to the element, the `active-add` and the `active-add-active` classes are added just before |
| 332 | +to signal AngularJS to fire off an animation. When removed, the `active-remove` and the `active-remove-active` classes |
| 333 | +are applied to the element which in turn trigger another animation. |
| 334 | + |
| 335 | +You may be thinking that we're just going to create another CSS-enabled animation. |
| 336 | +Although we could do that, let's take the opportunity to learn how to create JavaScript-enabled animations with the `animation()` module method. |
| 337 | + |
| 338 | +__`app/js/animations.js`.__ |
| 339 | +<pre> |
| 340 | +angular.module('phonecatAnimations', ['ngAnimate']) |
| 341 | + |
| 342 | + .animation('.phone', function() { |
| 343 | + return { |
| 344 | + addClass : function(element, className, done) { |
| 345 | + if(className != 'active') { |
| 346 | + return; |
| 347 | + } |
| 348 | + element.css({ |
| 349 | + position: 'absolute', |
| 350 | + top: 500, |
| 351 | + left: 0, |
| 352 | + display: 'block' |
| 353 | + }); |
| 354 | + jQuery(element).animate({ |
| 355 | + top: 0 |
| 356 | + }, done); |
| 357 | + |
| 358 | + return function(cancel) { |
| 359 | + if(cancel) element.stop(); |
| 360 | + }; |
| 361 | + }, |
| 362 | + removeClass : function(element, className, done) { |
| 363 | + if(className != 'active') return; |
| 364 | + element.css({ |
| 365 | + position: 'absolute', |
| 366 | + left: 0, |
| 367 | + top: 0 |
| 368 | + }); |
| 369 | + jQuery(element).animate({ |
| 370 | + top: -500 |
| 371 | + }, done); |
| 372 | + |
| 373 | + return function(cancel) { |
| 374 | + if(cancel) element.stop(); |
| 375 | + }; |
| 376 | + } |
| 377 | + }; |
| 378 | + }); |
| 379 | +</pre> |
| 380 | + |
| 381 | +Note that we're using {@link http://jquery.com/ jQuery} to implement the animation. jQuery |
| 382 | +isn't required to do JavaScript animations with AngularJS, but we're going to use it because writing |
| 383 | +your own JavaScript animation library is beyond the scope of this tutorial. For more on |
| 384 | +`jQuery.animate`, see the {@link http://api.jquery.com/animate/ jQuery documentation}. |
| 385 | + |
| 386 | +<div class="alert alert-error"> |
| 387 | + <h4>Important:</h4> |
| 388 | + Be sure to use jQuery version `1.10.2`. AngularJS does not yet support jQuery `2.x`. |
| 389 | +</div> |
| 390 | + |
| 391 | +The `addClass` and `removeClass` callback functions are called whenever an a class is added or removed |
| 392 | +on the element that contains the class we registered, which is in this case `.phone`. When the `.active` |
| 393 | +class is added to the element (via the `ng-class` directive) the `addClass` JavaScript callback will |
| 394 | +be fired with `element` passed in as a parameter to that callback. The last parameter passed in is the |
| 395 | +`done` callback function. The purpose of `done` is so you can let Angular know when the JavaScript |
| 396 | +animation has ended by calling it. |
| 397 | + |
| 398 | +The `removeClass` callback works the same way, but instead gets triggered when a class is removed |
| 399 | +from the element. |
| 400 | + |
| 401 | +Within your JavaScript callback, you create the animation by manipulating the DOM. In the code above, |
| 402 | +that's what the `element.css()` and the `element.animate()` are doing. The callback positions the next |
| 403 | +element with an offset of `500 pixels` and animates both the previous and the new items together by |
| 404 | +shifting each item up `500 pixels`. This results in a conveyor-belt like animation. After the `animate` |
| 405 | +function does its business, it calls `done`. |
| 406 | + |
| 407 | +Notice that `addClass` and `removeClass` each return a function. This is an **optional** function that's |
| 408 | +called when the animation is cancelled (when another animation takes place on the same element) |
| 409 | +as well as when the animation has completed. A boolean parameter is passed into the function which |
| 410 | +lets the developer know if the animation was cancelled or not. This function can be used to |
| 411 | +do any cleanup necessary for when the animation finishes. |
| 412 | + |
| 413 | + |
| 414 | +# Summary |
| 415 | + |
| 416 | +There you have it! Animations are in place. Hopefully this has shown you how you can improve |
| 417 | +your AngularJS web application to have an awesome layer of animated interactivity. |
| 418 | + |
| 419 | +<ul doc-tutorial-nav="12"></ul> |
0 commit comments