A typical application of smooth transitions is navigation between pages of your webapp.

In this article we will implement a typical master/detail movie search application that applies transitions between pages:

Setup

This tutorial uses AngularJS and UI Router, the most popular navigation framework to this date (so that you can easily transpose the technique to your own webapp).

The first step is to declare your pages:

  • the main structure page, that define a global structure (header, menu, etc.), including a "content" area
  • contents pages, that will be dynamically inserted in the content area, depending on the navigation state.

Let's start defining those latter "contents" pages. First, the search template (Page1.html) is just a simple <form> whose submit button will trigger a navigation toward the search results (page.2 router state):

  <div id="page1">
  <h1>Search movie</h1>
  <form name="searchForm">
    <label for="searchInput">Title</label>
    <input id="searchInput" type="text" ng-model="searchText" required>
    <input type="submit" value="Fetch" ui-sref="page.2({q:searchText})">
  </form>
  <div ng-show="searching"><br>Searching for {{searchText}}...</div><br>
  <a ng-if="results" ui-sref="page.2">&gt; See latest results</a>
</div>

Note the ui-sref (UI State Reference) attribute in the submit input, which is a UI Router directive that will trigger a navigation toward the specified state (page.2 in this case, with the value of searchText as the q parameter).

then the results list template (Page2.html) which iterates on each movie in results and provide a link to its details (page.3 router state):

  <div id="page2">
  <h1>Movies with "{{searchText}}\"</h1>
  <a ui-sref="page.1">&lt; back</a>  // Link back to first search page
  <div ng-show="searching"><br>Searching for {{searchText}}...</div>
  <ul>
    <li ng-repeat="movie in results">
      <a ui-sref="page.3({movieIndex:$index})">{{movie.title}}</a>
    </li>
  </ul>
</div>

and finally the movie details template (Page3.html) that displays every detail about a given movie. This one is very simple, as movie display details are encapsulated as a movie attribute directive here (which takes itself as base-url parameter):

  <div id="page3">
  <div movie="movie" base-url="{{baseUrl}}"></div>
  <a ui-sref="page.2">&lt; Results</a>
</div>

Now we're going to set up the main HTML structure that will display those contents page:

  <html ng-app="scrollDemo">
  <body>
    <header>This is a menu bar 
      <nav>
        <a ui-sref="page.1">Search</a>
        <a ui-sref="page.2">Results</a>
        <a ui-sref="page.3">Detail</a>
      </nav>
    </header>
    <div class="content">
      <div class="scroll">
        <ui-view></ui-view>
      </div>
    </div>
  </body>
</html>

Here you can see two types of special tagging for UI Router:

  • an ui-view tag which is a placeholder for the dynamic views contents. So, depending on the navigation state, that tag will be replaced by some content page template or another (for the record, note that you can also use an <div ui-view> attribute notation, and even add names to your views if you have multiple ones) ;
  • the ui-sref attributes that you already saw in contents pages, that will take care of hyperlinks to the various content pages.

Now let's define the meaning of those states with their associated templates and URL changes:

  angular.module('scrollDemo', ['ui.router'])
.config(function($stateProvider) {
  $stateProvider
    .state('page.1', {
      url: '/page1',
      templateUrl: '/Page1.html'
    })
    .state('page.2', {
      url: '/page2',
      templateUrl: '/Page2.html'
    })
    .state('page.3', {
      url: '/page3',
      templateUrl: '/Page3.html'
    });
})

At this point you can now check that your contents are actually loaded into the structure when you type one URL or another.

But at startup nothing appears. That's because we have to set a state to display at startup (Angular's run()) to fix this, using the UI Router $state service:

  angular.module('scrollDemo')
  .run(function($state, $rootScope) {
    $state.go('page.1');  
  });

Animating

Navigation is working fine now, but still without animations between pages. To do so, we will leverage the AngularJS ngAnimate API:

  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.17/angular-animate.min.js"></script>

  angular.module('scrollDemo', ['ui.router', 'ngAnimate']);

Basically, the AngularJS animate API provides hooks for adding and removing CSS classes when entering or leaving a state. Those hooks are leveraged by standard angular directives such as ng-view but also the UI Router view change events. More specifically, as the Angular 1.3 animation services states:

  • a ng-enter class will be added on "entering" elements
  • a ng-enter-active class will be added on "entered" elements
  • a ng-leave class will be added on "leaving" elements
  • a ng-leave-active will be added on "left" elements

That allows us to define variations of the translated state of each part.

Sliding transition

Let's use the method we recommended for smooth transitions:

  .scrolled {
  transition: transform $duration ease;
  transform: translate3d(0, 0, 0);
  &.ng-enter {
    transform: translate3d(100%, 0, 0);  // Move to the right (up to 100% of width)
  }
  &.ng-enter-active {
    transform: translate3d(0, 0, 0);
  }
  &.ng-leave {
    transform: translate3d(0, 0, 0);
  }
  &.ng-leave-active {
    transform: translate3d(-100%, 0, 0);  
  }
}

We should also define the transition in the opposite direction, when going back to a previous page. We will implement that through an additional backward class applied on the scrolled view:

  .backward {
  .scrolled {
    &.ng-enter {
        transform: translate3d(-100%, 0, 0);
    }
    &.ng-enter-active {
        transform: translate3d(0, 0, 0);
    }
    &.ng-leave {
        transform: translate3d(0, 0, 0);
    }
    &.ng-leave-active {
        transform: translate3d(100%, 0, 0);
    }
  }
}

Modal transition

Note that you're not constrained to a sliding transition. This is a nice SoC to allow to attach any kind of transition to a navigation state change, only through different CSS rules.

So here is another kind of "Android modal" transition, combining both opacity and scaling. We will apply this one to the third movie details page, that will hold an additional modal class:

  .modal {
  transition: opacity 1s ease, transform 1s ease;
  opacity: 1;
  &.ng-enter {
    opacity: 0;
    transform: scale(0.95) translate3d(0, 0.5em, 0);
  }
  &.ng-enter-active {
    opacity: 1;
    transform: scale(1) translate3d(0, 0, 0);
  }
  &.ng-leave {
    opacity: 1;
  }
  &.ng-leave-active {
     opacity: 0;
  }
}
.backward {
  .modal {
    transition: opacity 1s ease, transform 1s ease;
    opacity: 1;
    &.ng-enter {
      opacity: 0;
      transform: scale(1) translate3d(0, 0, 0);
    }
    &.ng-enter-active {
      opacity: 1;
      transform: scale(1) translate3d(0, 0, 0);
    }         
    &.ng-leave {
      opacity: 1;
      transform: scale(1) translate3d(0, 0, 0);
    }
    &.ng-leave-active {
      opacity: 0;
      transform: scale(0.95) translate3d(0, 0.5em, 0);
    }
  }
}

Loading the data

Having smooth transitions is fine, but even such transitions may be impacted by a parallel data loading, typically through an XHR request. To achieve such loading, we have the following options:

  • loading during the transition: theorically this should be the ideal option, as using the transition time to make the data loading time more acceptable. However, depending on the device performance and context, this will cause the transition to pause when the XHR request is being performed. Beware that, while not seeing such a slowdown in a browser, you will actually see it once embedded in a native WebView only. For this reason, this is an option we want to rule out ;
  • loading before the transition: this will warrant a smooth transition once loading is done, but can be detrimental for the UX, as the user will visually feel that its submission is not immediately taken in account. For this reason, this will not be our preferred option ;
  • loading after the transition: this optimisic UI approach will warrant a smooth transition before loading is done, that is, an immediate visual reaction to user submission (provided you also implemented fast click). But we have to make sure that the loading will not start before the actual end of the transition.

In each method, described below, we will ask a common movieFetch function to retrieve the data to display, and provide it as a $q promise. Additionally, a searching variable is set and unset to notify the user of the searching state:

  $scope.movieFetch = function(movieTitle, page) {
  return $q(function(resolve) {
    $scope.searching = true; 
    $http.jsonp('https://api.themoviedb.org/3/search/movie?query=' + movieTitle + '&callback=JSON\_CALLBACK')
      .success(function(data) {
        $scope.searching = false;
        resolve(angular.fromJson(data).results);
      })
    });
  };

Fetch before

This implies to resolve a dependency that will be injected in the target view controller:

  $stateProvider
  .state('page.2', {
    url: '/page2?:q',
    controller: function($scope, resolvedResults) {
      $scope.results = resolvedResults;
    },
    resolve: {
      resolvedResults: function($stateParams) {
        return movieFetch($stateParams.q);
      }
    }
  });

Fetch after

This implies to trigger the load at the execution of the target state controller. However, this is not enough if want to avoid any concurrent XHR to slow down your transition: you have to explicitly wait for the end of the transition, through listening for the transitionend event:

  $stateProvider
  .state('page.2', {
    url: '/page2?:q',
    controller: function($scope, $element, $stateParams) {
      $element[0].addEventListener('transitionend', function(event) {  
        movieFetch($stateParams.q).then(function(data) {
          $scope.results = data;
        });
      }, false);
    }
  });


59,824 1 23