Replacing jQuery.slideDown() with ngAnimate in AngularJS 1.1.5
Update 08/13/2013: The examples in this post are out of date and incorrect as of AngularJS 1.2.0-rc1 or later. The developers ripped out ngAnimate as a directive in favor of ngAnimate as a module and service. The world moves on and all things change.
Overview
I recently added an AngularJS example application to my Thywill Socket.IO-based framework, by way of providing an illustration of how to integrate Thywill with the modern breed of client-side Javascript application frameworks. The other example applications are jQuery-based and where animations are used they mostly run via jQuery functions such as slideDown().
The present unstable version of AngularJS (1.1.5 at the time of writing) offers a new ngAnimate directive. This provides some structure to the organization of CSS3 animations and acts in conjunction with ngRepeat, ngShow, and similar directives that change the DOM by adding, removing, showing, or hiding elements. So I thought I'd give it a try.
I'm calling my example application "Tabular", as it does little more than display a scrolling table of data. The data is intermittently pushed to the client by the server, one row at a time, and each new row is added to the top of the table. The objective here is to animate the addition of the new row such that it appears more or less the same as a call to slideDown() in jQuery.
The AngularJS Application Without Animation
But first the basics. Below is the outline of Tabular - it's about as simple as an AngularJS application can be. Starting with the HTML:
<!DOCTYPE html> <html lang="en"> <head> <title>Thywill: Tabular Application</title> <style type="text/css" media="all"> @import url("/tabular/css/client.css"); </style> <script type="text/javascript" src="/tabular/js/socket.io.js"></script> <script type="text/javascript" src="/tabular/js/modernizr.min.js"></script> <script type="text/javascript" src="/tabular/js/jquery.min.js"></script> <script type="text/javascript" src="/tabular/js/angular.min.js"></script> <script type="text/javascript" src="/tabular/js/thywill.js"></script> <script type="text/javascript" src="/tabular/js/thywillAppInterface.js"></script> <script type="text/javascript" src="/tabular/js/tabularThywillAppInterface.js"></script> <script type="text/javascript" src="/tabular/js/tabularAngularApp.js"></script> </head> <body data-ng-app="tabular" data-ng-view> <!-- The AngularJS application is going to populate the DOM from a loaded partial, so there is no HTML in the body. --> </body> </html>
Skipping over all of the preliminary Thywill Javascript and various other third party libraries, the AngularJS part of Tabular is defined in tabularAngularApp.js:
(function () { 'use strict'; // Obtain a reference to the Thywill ApplicationInterface for this // application. var applicationInterface = Thywill.tabularApplication; // Create the example application AngularJS module. var tabular = angular.module('tabular', []); // Use the config function to set up routes, or route singular in this case. tabular.config(function ($routeProvider) { $routeProvider .when('/', { controller: 'tabularMainController', templateUrl: '/tabular/partial/main.html' }) .otherwise({ redirectTo: '/' }); }); // Create the application controller. Only a single controller here to go // with the single route. tabular.controller('tabularMainController', ['$scope', function ($scope) { // To store the rows of data that are pushed to the client from the server. $scope.rows = []; // Listen on the Thywill ApplicationInterface instance for messages // arriving from the server. The only type of message in this application // is a row of data, so when one arrives add the data to the start of // the array. applicationInterface.on('received', function (message) { $scope.$apply(function (scope) { scope.rows.unshift(message.getData()); }); }); }]); })();
The partial associated with the only route in this application is as follows, essentially a list of rows:
<div class="tabular-wrapper"> <div class="title">Thywill: Tabular Application</div> <div class="table-wrapper"> <ul class="table"> <li class="row row-header"> <span>Alpha</span> <span>Beta</span> <span>Gamma</span> <span>Delta</span> <span>Epsilon</span> </li> <li class="row" data-ng-repeat="row in rows"> <span data-ng-repeat="cell in row">{{cell}}</span> </li> </ul> </div> </div>
The following CSS is used to style the unordered list as a table, with each list item as a row. I'm omitting the rest of the page CSS for brevity:
.table { display: table; float: left; margin: 0; padding: 0; width: 100%; } .row { display: table-row; height: 30px; line-height: 30px; list-style: none; } .row-header { background-color: #efebf5; font-weight: bold; } .row > span { border-top: 1px solid #cccccc; display: table-cell; text-align: center; vertical-align: middle; } .row:first-child > span { border-top: none; } .row.row-header > span { height: 40px; }
Adding Animation
To add the slideDown-like animation for new table rows, we first add the data-ng-animate attribute as follows. The value of "row" provided to the attribute will be used as a prefix for the name of classes that are applied to the row elements while they are animated:
<li class="row" data-ng-repeat="row in rows" data-ng-animate="'row'"> <span data-ng-repeat="cell in row">{{cell}}</span> </li>
We can then add a standard set of classes used for CSS3 transition animations. Since the prefix passed to the ngAnimate directive is "row", then the class names will be "row-enter", "row-enter-active", "row-leave", and "row-leave-active":
.row-enter, .row-leave { -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out; } .row.row-enter, .row.row-leave.row-leave-active { height: 0; line-height: 0; opacity: 0; } .row.row-leave, .row.row-enter.row-enter-active { height: 30px; line-height: 30px; opacity: 1; }
Note that height, line-height, and opacity are all being animated. You need all three to create the correct appearance of the element sliding down into existence. Also note the use of .row.row-enter rather than just .row-enter as is usually the case in other examples. Since .row specifies a height we need to use .row.row-enter in order to be sure that we override it regardless of the order in which the CSS classes were declared.
Further Notes
You can specify class names explicitly in the ngAnimate attribute if so desired:
<li class="row" data-ng-repeat="row in rows" data-ng-animate="{ enter: 'row-enter', leave: 'row-leave', move: 'row-move' }"> <span data-ng-repeat="cell in row">{{cell}}</span> </li>
I didn't use the "row-move" and "row-move-active" classes in my example application because they apply to elements within the list moving from one position to another, which doesn't happen in this case. The principle is the same if you do have to use them: just define the necessary classes with CSS transitions and off you go.