Ui-Router State Transition Animations & Bootstrap

I was given what I thought was a nearly impossible task by my product owner - create an animation when changing states. I began using ngAnimate and came up with what I thought was a pretty cool solution to his problem - wrong.

"This isn't what I had in mind he told me. As I change between panels of a collapse widget I want to change the state and update the URL as well."

Me - "Oh boy..."

So how else can I animate state transitions using an accordion like approach or in bootstrap terminology - the collapse widget?

Back to the drawing board...

I came up with what I thought was a really cool way to animate transitions using ui-router and bootstrap.

First off lets set up the states that we need transitioning. I will only show this with two collapse panels, but in theory there can be as many as needed.


    .state('home.checkout', {
            url: 'checkout',
            views: {
                '@home': {
                    templateUrl: 'views/partials/generic/checkout-process/order-checkout-root.html'

        .state('home.checkout.shoppingcart', {
            url: '^/shoppingcart',
            views: {
                'shopping-cart@home.checkout': {
                    templateUrl: 'views/partials/generic/checkout-process/shoppingcart/shopping-cart-partial.html',
                    controller: 'ShoppingCartController'
                'order-confirmation@home.checkout' : {
                    templateUrl: 'views/partials/generic/checkout-process/closed-state.html',
                    controller: function($scope) {
                        $scope.page = {name: 'Order Confirmation'};
                        $scope.state = {name: 'home.checkout.confirm'};

        .state('home.checkout.confirm', {
            url: '/confirmation',
            views: {
                'shopping-cart@home.checkout': {
                    templateUrl: 'views/partials/generic/checkout-process/closed-state.html',
                    controller: function($scope) {
                        $scope.page = {name: 'Shopping Cart'};
                        $scope.state = {name: 'home.checkout.shoppingcart'};
                'order-confirmation@home.checkout': {
                    templateUrl: 'views/partials/generic/checkout-process/confirmation/order-confirmation-partial.html',
                    controller: 'OrderConfirmationController'



    <div class="row checkout-process">
        <section class="col-sm-8 col-md-8 col-lg-8 panel-group" id="accordion">
            <div class="shopping-cart panel panel-default" ui-view="shopping-cart" autoscroll="false"></div>
            <div class="order-confirmation panel panel-default" ui-view="order-confirmation" autoscroll="false"></div>


    <article class="col-sm-12 col-md-12 col-lg-12 panel-heading closed-state">
        <h4 class="panel-title">
            <a ui-sref="{{state.name}}">


I will only include this one and not the other partial as its the same idea.

    <div class="order-confirmation-page row">
        <div class="panel-heading">
            <h4 class="panel-title">Order Confirmation</h4>

        <div class="panel-collapse collapse" pttp-collapse-toggler data-toggle="collapse">
            <div class="panel-body">
                <!--Code for the collapse body goes here-->

Whats important from this last partial is to note the inclusion of the directive


This is where we do our work and the most interesting part of the code


    'use strict';

    angular.module('PTTP.Directives.CollapseToggler', [])

        .directive('pttpCollapseToggler', function ($rootScope, $state, $q, $timeout) {

            var linker = function(scope, element) {

                    collapse = $q.defer(),
                    changeEventStarted = false

                //Expand the panel on directive instantiation
                $timeout(function() {
                }, 300);

                scope.$on('$stateChangeStart', function(event, toState) {
                    //Check to make sure we arent in the middle of a $stateChangeEvent
                    if(changeEventStarted) {
                    //Stop the state transition

                    //Collapse the panel

                    //Wait for the panel to collapse completely
                    collapse.promise.then(function() {
                        changeEventStarted = true;
                        //Then transiton the state

                //Event listener for the collapse completed
                $(element).on('hidden.bs.collapse', function() {

            return {
                restrict: 'A',
                link: linker


In short what we do here is:

  1. Setup a promise to know when we can transition again.
  2. Intercept the $stateChangeStart event and stop it from happening.
  3. Then we collapse the panel we are interested in
  4. When the collapse is finished bootstrap fires an event saying I am done collapsing which we listen for and in turn resolve the promise
  5. When the promise is resolved we can safely transition to the next state

I hope that this isn't too much to follow, but if you do the potential it has for other kinds of animation is pretty sweet.