Question

I was given what i thought was a nearly impossible task by my product owner to create some animations between 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."

So to outline it a bit better. As I change between panels of a collapse widget I want to change the state and update the URL as well.

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

Was it helpful?

Solution

Ok... so back to the drawing board I came up with this cool approach and though I might share it with anyone interested.

First off lets set up the states that we need to transition. I will only show this with two collapse panels instead of the three I had... Its gonna be alot of code anyways but a solution worth sharing.

Routing app.js

        .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'
                }
            }
        })

HTML order-checkout-root.html

<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>
    </section>
</div>

closed-state.html

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

order-confirmation-partial.html

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>

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

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

kx-collapse-toggler

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

collapseTogglerDirective.js

'use strict';

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

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

        var linker = function(scope, element) {

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

            //Expand the panel on directive instantiation
            $timeout(function() {
                $(element).collapse('show');
            }, 300);


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

                //Collapse the panel
                $(element).collapse('hide');

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

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

        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 isnt too much to follow, but if you do the potential it has for other kinds of animation is pretty great.

I am working on putting together a plunker so its possible to see the animation.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top