Want more updates, tutorials, and awesomeness in general? Sign up!

ng-animate First Look with AngularJS Wizard

Nacho Libre

The Setup

So my friend Matias, aka Year of Moo, Moo or MooMoo as I like to call him just dropped some animation functionality into AngularJS. I think this is fantastic and am super excited to see AngularJS get some love in the visual department. I dig that I can be functional AND fashionable!

There is still a lot of work to be done around ng-animate but it is a great start and so I wanted to build something “useful” to get our feet wet. In the spirit of transparency I freely admit that my example is whimsical and silly and so if you write me and are like “You forgot to do BLAH BLAH or it is going to blow up in the enterprise” I AM GOING CLOTHESLINE YOU!

With that said, we are going to build a wizard style dialog using AngularStrap and ng-animate. And beneath the dialog we find the nucleus.

Demo Code

Nacho Beginning

The Foundation

We are going to first build the wizard by itself without any of the special sauce and then I am going to show you how easy it is to add in the animations. So let’s get down to bizness!

Step 1: App Setup Plus AngularStrap

The first thing we need to do is get a modal on the page. I opted to use the incredibly useful AngularStrap which can be found here http://mgcrea.github.io/angular-strap/.

I am going to show you the HTML ‘skeleton’ so we are all on the same page. HAHAHAHAHAH! See what I did there?! I know for a fact that John C. Bland heard my high pitched cackle in his head as he read that very line.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html ng-app="App" ng-controller="AppCtrl">
<head>
    <title>AngularJS Taco Party</title>
   
    <link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.1/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" type="text/css" href="app.css" />
</head>
<body>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.4/angular.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular-strap/0.7.2/angular-strap.min.js"></script>
    <script src="app.js"></script>
</body>

Pretty straight forward stuff here. We are importing jQuery, AngularJS, Boostrap and AngularStrap with the Boostrap CSS at the top. We are also auto-bootstrapping AngularJS with the App module and defining a AppCtrl to go with it.

To make AngularStrap available to your app you simply need to inject it in as as submodule to your main application module like so.

1
2
angular.module('App', ['$strap.directives'])
.controller('AppCtrl', function($scope) { });

Step 2: AngularStrap Modal

We technically have an AngularJS application with AngularStrap working and so now we need to actually earn our keep. I really dig that AngularStrap makes it easy to attach the modal to the DOM dynamically via a template without cluttering up your HTML. I have written such a mechanism myself and I am glad no one else ever has to do it again!

So to show the modal, we are going to add a button to the page that looks like the markup below.

1
2
3
<button type="button" class="btn btn-large btn-primary ng-scope"
    bs-modal="'partials/wizard.html'"
    data-toggle="modal">I Dare You!</button>

There are two parts that I want to point out here. The first part is bs-modal=”‘partials/wizard.html'” which is the conduit for the Boostrap modal to gain access to the page and the attribute value of partials/wizard.html is the template that it populates the modal with. The second part is data-toggle=”modal” which is used to toggle visibility of the modal. Easy!

And in the partial we start with this basic HTML structure as established by Twitter Bootstrap.

1
2
3
4
5
6
7
8
9
<div class="modal-header">
    <!-- Header code goes here -->
</div>
<div class="modal-body">
    <!-- Body code goes here -->
</div> 
<div class="modal-footer">
    <!-- Footer code goes here -->
</div>

And now we have our modal!

Step 3: The Wizard Part One

We are now going to put the wiz in wizard. First we need to establish the stages available in the wizard and give users a way to step through them. We do this by creating $scope.steps with three steps defined in the array. We also need to keep track of the current step we are on and so we use $scope.step to do this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
angular.module('App', ['$strap.directives'])
.controller('AppCtrl', function($scope) {
  $scope.steps = ['one', 'two', 'three'];
  $scope.step = 0;

  $scope.isCurrentStep = function(step) {
    return $scope.step === step;
  };

  $scope.setCurrentStep = function(step) {
    $scope.step = step;
  };

  $scope.getCurrentStep = function() {
    return $scope.steps[$scope.step];
  };
});

I am going to elaborate what each of these functions do by anchoring it to the HTML that uses it. In the wizard we have a button group with three buttons. There is one button for each step of the wizard. When you click any of these buttons, it calls setCurrentStep with the step that the corresponds to the button. I am also dynamically setting the btn-primary class in the CSS with ng-class and binding it to the isCurrentStep function. I like binding to functions that perform a small unit of logic instead of putting it into my markup. Because of the way AngularJS handles two-way databinding, it is really easy to do this.

1
2
3
4
5
<div class="btn-group">
    <button class="btn" ng-class="{'btn-primary':isCurrentStep(0)}" ng-click="setCurrentStep(0)">Uno</button>
    <button class="btn" ng-class="{'btn-primary':isCurrentStep(1)}" ng-click="setCurrentStep(1)">Dos</button>
    <button class="btn" ng-class="{'btn-primary':isCurrentStep(2)}" ng-click="setCurrentStep(2)">Tres</button>
</div>

Just below the button group, we are going to start to flesh out the structure we want to build out the steps of our wizard in. I am binding ng-switch to the getCurrentStep method which returns the appropriate label for the switch to work off of. I defined $scope.steps as an array of strings for this very reason.

1
2
3
4
5
6
7
8
9
10
11
<div ng-switch="getCurrentStep()" class="slide-frame">
    <div ng-switch-when="one">
        <!-- STEP ONE -->
    </div>
    <div ng-switch-when="two">
        <!-- STEP TWO -->
    </div>
    <div ng-switch-when="three">
        <!-- STEP THREE -->
    </div>
</div>

Step 4: The Wizard Part Two

We have a way to jump directly to a state in the wizard via the nav buttons but let us create a way to iterate over the steps with next and previous buttons.

Let us start with the HTML and then we can dig into the JavaScript that supports it. We have two buttons that are used to call handlePrevious and handleNext via ng-click. The reason we are passing dismiss as a parameter to handleNext is that if we are at the last step we need a way to close the modal. If we are on the first step, we want to hide the previous button which is easy enough with ng-show and binding it to !isFirstStep(). And! If we are on the last step, we want to change the label of the next button to reflect that it is going to behave differently. Could we possibly bind the label to a function as well? Yep! You got it! getNextLabel makes a stunning debut!

1
2
3
4
<div class="modal-footer">
    <a class="btn" ng-click="handlePrevious()" ng-show="!isFirstStep()">Back</a>
    <a class="btn btn-primary" ng-click="handleNext(dismiss)">{{getNextLabel()}}</a>
</div>

I have called out a few methods that we need to cover in the controller. They are small and to the point and so I am going to briefly comment on them.

isFirstStep compares step to 0 and returns true if we are indeed on the first step.

isLastStep compares step to the length of the steps array minus 1 (to account for the zero ordinal shift) and returns true of we are on the last step.

getNextLabel uses isLastStep to determine if we are on the last step and returns either Submit or Next as the label.

handlePrevious uses isFirstStep to determine if we are on the first step and if we are not, subtracts 1 from step.

handleNext uses isLastStep to determine if we are on the last step. If we are not, it adds 1 to step. If we are, then we call dismiss to close the modal. In real life you would process the wizard data just before you called this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
angular.module('App', ['$strap.directives'])
.controller('AppCtrl', function($scope) {
    // Code omitted

    $scope.isFirstStep = function() {
        return $scope.step === 0;
    };

    $scope.isLastStep = function() {
        return $scope.step === ($scope.steps.length - 1);
    };

    $scope.getNextLabel = function() {
        return ($scope.isLastStep()) ? 'Submit' : 'Next';
    };

    $scope.handlePrevious = function() {
        $scope.step -= ($scope.isFirstStep()) ? 0 : 1;
    };

    $scope.handleNext = function(dismiss) {
        if($scope.isLastStep()) {
            dismiss();
        } else {
            $scope.step += 1;
        }
    };
});

And now we have a working wizard that we can step through! Okay! I admit we are missing the animations and the content but that is exactly what we are going to cover next.

Nacho Corn

Let’s Animate!

And now it is time to get to the punchline! ng-animate works on JavaScript or CSS animations and you can spin them up a couple ways. In this tutorial we are going to take the path of least resistance and use the ng-animate directive in conjunction with the ng-switch which has animation events built in to make it easy to control.

So how does this work? In a nutshell, you attach ng-animate to a directive that supports animations such as ng-switch and then give it a value that corresponds to some CSS styling we are going to get to in just a moment. You can see that I have done this via ng-animate=”slide”.

1
2
3
<div ng-switch="getCurrentStep()" ng-animate="'slide'" class="slide-frame">
    <!-- OMITTED -->
</div>

ng-animate works with a few methods such as enter, hide, leave, move and show and you define the setup and start state for each animation in the CSS.

The formula is as follows [animation-name]-[method]-[state-of-animation] so following this format we define slide as slide-enter-setup and slide-enter-start. So we have defined the animation on enter but does the format work the same for leave? Yep! We apply the same formula and we get slide-leave-setup and slide-leave-start. You will notice that I have one extra block of CSS where I define enter and leave setup. ng-animate allows you to define custom easing animations and so I like to get that out of the way way in one fell swoop.

1
2
3
4
5
6
.slide-enter-setup, .slide-leave-setup {
  -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
  -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
  -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
  transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.slide-enter-setup {
  position:absolute;
  left:0;
  top:-200px;
}

.slide-enter-start {
  top:0;
}

.slide-leave-setup {
  position:absolute;
  top:0;
}

.slide-leave-start {
  top:200px;
}

I have defined the slide animation to animate from -200px to 0px on enter and from 0px to 200px on leave.

Slide moves from top to bottom but let us create another animation that moves from left to right and call it wave.

1
2
3
4
5
6
.wave-enter-setup, .wave-leave-setup {
  -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
  -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
  -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
  transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.wave-enter-setup {
  position:absolute;
  left:-100%;
}

.wave-enter-start {
  left:0;
}

.wave-leave-setup {
  position:absolute;
  left:0;
}

.wave-leave-start {
  left:100%;
}

Take a moment and try to identify the pattern I talked about earlier. See it? Easy. With wave, we are simply moving from left to right 100% of the width. We could write these all day long now!

And to use wave instead of slide we just change ng-animate to ng-animate=”‘wave'”.

1
2
3
<div ng-switch="getCurrentStep()" ng-animate="'wave'" class="slide-frame">
    <!-- OMITTED -->
</div>

And now we have a wizard dialog that you can open and animate from step to step. Super awesome but we need to actually do something “useful” with it!

Nacho Fly

Pro Tip Alert! State and ng-switch

ng-switch is like a good neighbor and it cleans up after its children as it moves from one view to another. This is a good thing! But! It does cause problems when you need to preserve values from one view to another.

The solutions to this is pretty simple actually. You create a model object on the parent scope to use in the children. I have done this by declaring the $scope.wizard object on AppCtrl.

1
2
3
4
5
6
7
8
angular.module('App', ['$strap.directives'])
.controller('AppCtrl', function($scope) {
    $scope.steps = ['one', 'two', 'three'];
    $scope.stepIndex = 0;
    $scope.wizard = { tacos: 2 };
   
    // CODE OMITTED
});

From there you define your ng-models in the children views to reference the wizard object like so ng-model=”wizard.tacos”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div ng-switch-when="two">
    <fieldset>  
        <div class="control-group">  
            <label class="control-label" for="select01">How Many Tacos?</label>  
            <div class="controls">  
                <select id="select01" ng-model="wizard.tacos">  
                    <option>Unlimited</option>  
                    <option>2</option>  
                    <option>3</option>  
                    <option>4</option>  
                    <option>5</option>  
                </select>  
            </div>  
        </div>  
        <div class="control-group">  
            <label class="control-label" for="multiSelect">Toppings?</label>  
            <div class="controls">  
                <select multiple="multiple" id="multiSelect" ng-model="wizard.toppings">  
                    <option>Cheese</option>  
                    <option>Guacamole</option>  
                    <option>Mild Salsa</option>  
                    <option>Hot Salsa</option>  
                    <option>Cilantro</option>  
                </select>  
            </div>  
        </div>  
    </fieldset>  
</div>

Conclusion

We have covered a lot of ground and I have had a lot of fun building out the example project. This is just a first look at ng-animate but I think it is a great step in the right direction to kicking up our AngularJS apps a notch.

Big ups to MooMoo for all his great work and we are already kicking around some ideas on how to make it even better. Stay tuned!

Resources

Source Files: https://github.com/simpulton/angularjs-wizard

Year of Moo: http://www.yearofmoo.com/2013/04/animation-in-angularjs.html

AngularStrap: http://mgcrea.github.io/angular-strap/

AngularJS Docs: http://code.angularjs.org/1.1.4/docs/api/ng.directive:ngAnimate

Leave a Comment