Replacing jQuery.slideDown() with ngAnimate in AngularJS 1.2.0

September 1st, 2013 Permalink

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().

At the time I was using the unstable version 1.1.5 of AngularJS, which offered a new ngAnimate directive to provide some structure to the organization of CSS3 animations. So I wrote a post on the subject. I should have paid more attention to the AngularJS code in development, however, as ngAnimate had a short life span as a directive. By version 1.2.0-rc1 it morphed into a module and service combination: while the underlying CSS3 is more or less the same, the syntax for using it is quite different, more implicit than explicit. So here I get to rewrite my not-so-old post that is now embarrassingly behind the times. Such is life in the fast lane.

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;
  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;
  width: 20%;
}
.row:first-child > span {
  border-top: none;
}
.row.row-header > span {
  line-height: 40px;
}

Adding Animation

Animation is added to an application by specifying a dependency on the ngAnimate module. Every DOM element associated with animation-friendly directives in the application partials will be decorated with animation-related classes when the DOM is changed by the addition, removal, or movement of elements. It's then up to you define those classes for the places you want animation to occur. Here is the altered module definition for Tabular:

  // Note that as of 1.2.0-rc1 we have to specify a dependency on ngRoute
  // as well, as that has been broken out into its own module.
  var tabular = angular.module('tabular', ['ngRoute', 'ngAnimate']);

Don't forget to include angular-route.js and angular-animate.js in the manifest of Javascript files to load. AngularJS is becoming increasingly modular these days.

  <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/angular-route.min.js"></script>
  <script type="text/javascript" src="/tabular/js/angular-animate.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>

With the module dependency added we can now add the definitions for a standard set of classes used for CSS3 transition animations. The relevant animation class names for ngRepeat are "ng-enter", "ng-enter-active", "ng-leave", and "ng-leave-active". We combine those with the element class of "row" as follows:

.row.ng-enter, .row.ng-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.ng-enter, .row.ng-leave.ng-leave-active {
  line-height: 0;
  opacity: 0;
}
.row.ng-leave, .row.ng-enter.ng-enter-active {
  line-height: 30px;
  opacity: 1;
}

Line-height is a fairly arbitrary choice for the property to animate: max-height and height also work for WebKit, but you might run into issues depending on how you are defining the default CSS for the animated elements. Opacity is mandatory - the effect doesn't look anywhere near as good without it.

Further Notes

As you can see this is fairly straightforward. The module and service approach for ngAnimate makes it a much easier undertaking to add animations to an existing project in comparison to the directive model.

Annoyingly I have not been able to make this example work properly in recent versions of Firefox. It certainly should animate the line-height, per the CSS3 specs, but in practice it isn't having any of it. Max-height and height similarly fail. This is something to look into when I have more time to devote to the issue, but it appears that it might be something to do with animations overlapping or curtailing one another.

I didn't use the "ng-move" and "ng-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.