The Setup
My original intention was to revisit and update the ng-animate First Look with AngularJS Wizard post; but by the time I was finished, I had an almost entirely different project on my hands. Since the original post, the ngAnimate API has pretty much entirely changed. Also, the new “controller as” syntax (which I have learned to like quite a bit) was introduced. Along with updating to the latest AngularJS libraries and conventions, I replaced the existing modal with the ui-bootstrap modal since that is what I use almost exclusively now.
So welcome to an entirely new version of an old blog post! Check out the demo and grab the code. My life is good!
Demo CodeThe Source Files
The wizard is primarily built on top of Bootstrap and AngularJS as you can see in the code below. We will put our animations in the app.css file and the application functionality in app.js.
<!DOCTYPE html> <html ng-app="App" ng-controller="AppCtrl as app"> <head> <title>AngularJS Taco Party</title> <pre><code> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.1/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="app.css"/> </head> <body> <!-- ... --> <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.8/angular.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.8/angular-animate.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.1/js/bootstrap.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.12.0/ui-bootstrap-tpls.min.js"></script> <script src="app.js"></script> </body> </code></pre> </html>
Once we include the ngAnimate and ui.bootstrap submodules in our application, the foundation for our wizard will be complete and we can start building on it.
angular.module('App', ['ngAnimate', 'ui.bootstrap']) .controller('AppCtrl', function ($modal) { });
We will iterate through the steps to build the wizard in a somewhat circular fashion, starting with showing the wizard and ending with closing the wizard and displaying the results.
Show the Modal
The instantiation of a bootstrap modal is handled by the $modal service so we need to inject that into our AppCtrl. The creation of the modal is handled by calling $modal.open(config) and passing in a configuration option for the new modal. This is surprisingly similar to creating a new route in that the we define values for templateUrl, controller and controllerAs properties.
angular.module('App', ['ngAnimate', 'ui.bootstrap']) .controller('AppCtrl', function ($modal) { var app = this; <pre><code> app.open = function () { var modalInstance = $modal.open({ templateUrl: 'partials/wizard.html', controller: 'ModalCtrl', controllerAs: 'modal' }); }; }) </code></pre>
We are also storing a reference to the newly minted modal, modalInstance, for us to use when we handle the result of closing or dismissing the modal.
Navigate the Steps
The $modal service takes the modal template and the appropriate controller, compiles the two together, and adds the result to the DOM. Now that we have the wizard on the page, the next bit of functionality we will add is the ability to navigate through the steps of the wizard. Our wizard will have three steps which we are defining in the modal.steps array as [‘one’, ‘two’, ‘three’]. We will track the index of the current step with the modal.step property. I could have made this more concise by tracking the steps entirely by index, but I like using labels for steps so you can better describe the step you are on.
We are also going to create an object to take the user input and store it on the modal.wizard property. Then we are going to pre-populate the tacos property to 2 since that is the polite thing to do. Enough to appear sensible but not so many that we go broke! We are doing this so that we have a persistent mechanism to store the user input as they work through the wizard. We are using ngSwitch to change from one step to another and, as you would expect from any good component, it does a very good job of cleaning up after itself. The first iteration I did of this, I was not able to persist the user’s input because it kept getting wiped out when I went to the next step. The trick is to promote the data structure to be one level above the actual ngSwitch statements.
.controller('ModalCtrl', function ($modalInstance) { var modal = this; <pre><code>modal.steps = ['one', 'two', 'three']; modal.step = 0; modal.wizard = {tacos: 2}; modal.isCurrentStep = function (step) { return modal.step === step; }; modal.setCurrentStep = function (step) { modal.step = step; }; modal.getCurrentStep = function () { return modal.steps[modal.step]; }; </code></pre> });
We also have three convenience methods that provide the right levers and switches for controlling the view. The first method is isCurrentStep(step), which tells us if a step is the step we are on currently; this is good for things like ngClass. The second method is setCurrentStep(step), which is a simple setter for setting the current step. And finally, we have getCurrentStep(), which returns us the label of the step we are on.
<div class="btn-group"> <button class="btn" ng-class="{'btn-primary':modal.isCurrentStep(0)}" ng-click="modal.setCurrentStep(0)">Uno</button> <button class="btn" ng-class="{'btn-primary':modal.isCurrentStep(1)}" ng-click="modal.setCurrentStep(1)">Dos</button> <button class="btn" ng-class="{'btn-primary':modal.isCurrentStep(2)}" ng-click="modal.setCurrentStep(2)">Tres</button> </div>
We will use modal.isCurrentStep(step) to apply a btn-primary class to our button group to visually indicate the active step we are on. We will also attach modal.setCurrentStep(step) to ngClick to set the current step.
We are using ngSwitch to control which div is shown based on the return value of modal.getCurrentStep(). When modal.getCurrentStep() returns two for instance, then ng-switch-when=”two” is triggered and that div is shown while the other divs are hidden.
<div ng-switch="modal.getCurrentStep()" class="slide-frame"> <div ng-switch-when="one"></div> <div ng-switch-when="two"></div> <div ng-switch-when="three"></div> </div>
I am not going to dig into the actual contents of each step of the wizard as it is nothing more than a basic AngularJS form. Definitely check out the AngularJS documentation if you have any questions about how that piece is working.
We have covered the button bar on the top which allows us to jump to any point in the wizard, but we need to add in next and previous buttons so we can step through the wizard in a sequential fashion. In the modal footer, we will add two buttons with a to handle previous and next actions. Using ng-show=”!modal.isFirstStep()”, we will toggle the visibility of the previous button based on whether or not we are on the first step. We will do something similar with the label of the next button via modal.getNextLabel(), because we want to actually close the modal when we are on the last step and we want to reflect the nature of that interaction. And finally, we have attached modal.handlePrevious() and modal.handleNext() method calls to the previous and next buttons, respectively.
<div class="modal-footer"> <button class="btn btn-default" ng-click="modal.handlePrevious()" ng-show="!modal.isFirstStep()">Back</button> <button class="btn btn-primary" ng-click="modal.handleNext()">{{modal.getNextLabel()}}</button> </div>
In the ModalCtrl, we will examine the methods described in the view above. If the intent is clear without going further, I will have considered that a small win. We are going to track whether or not we are on the first or last step with modal.isFirstStep() and modal.isLastStep(). By comparing modal.step to 0, we can know if we are on the first step and by comparing modal.step to the total number of steps i.e. modal.steps.length – 1, we can know if we are on the last step.
modal.isFirstStep = function () { return modal.step === 0; }; modal.isLastStep = function () { return modal.step === (modal.steps.length - 1); }; modal.getNextLabel = function () { return (modal.isLastStep()) ? 'Submit' : 'Next'; }; modal.handlePrevious = function () { modal.step -= (modal.isFirstStep()) ? 0 : 1; }; modal.handleNext = function () { if (modal.isLastStep()) { $modalInstance.close(modal.wizard); } else { modal.step += 1; } };
We can build on those two simple methods by fleshing out modal.handlePrevious(). If we are on the first step then we cannot go back any further, so we will keep modal.step at 0. We will also finish modal.getNextLabel() by returning ‘Submit’ if we are on the last step and ‘Next’ if we are not.
Things get interesting with modal.handleNext() in that we will increment modal.step by one if we are not on the last step; but we will actually close the modal and submit the contents of modal.wizard if we are. We are able to manage interactions with the actual modal instance through the $modalInstance service, which we have injected into ModalCtrl. By calling $modalInstance.close(modal.wizard), we are sending the contents of modal.wizard back to the origination point of the modal for processing.
Dismiss the Modal
We can also close the modal by dismissing it which is handled differently by the parent controller than closing the modal. We will get into how to handle these two actions in the AppCtrl in just a moment; but for now, we just need to know that we dismiss the modal by calling $modalInstance.dismiss(reason).
modal.dismiss = function(reason) { $modalInstance.dismiss(reason); };
We will wrap the $modalInstance call in the modal.dismiss(reason) method so that we can keep our view from knowing anything about $modalInstance. In the modal header, we then call ng-click=”modal.dismiss(‘No bueno!’)” from ngClick to dismiss the modal.
<div class="modal-header"> <button type="button" class="close" ng-click="modal.dismiss('No bueno!')" aria-hidden="true">×</button> <h3>Taco Party!!!!!</h3> </div>
Animate the Steps
So far we have been entirely focused on the technical details of setting up the wizard, so I think it is time to spread our wings and fly with the spirit of the eagle! We are going to achieve this super special place of power by adding in a CSS animation using ngAnimate.
Animations in ngAnimate follow a fairly conventional format that is pretty easy to leverage once you understand how they are defined. In CSS, they basically follow a [class-name].[event-name].[state-name]. We are going to start with a base class of wave and construct our animations off of that.
We are only going to touch the surface of how to do one type of animation in AngularJS. I highly recommend reading Remastered Animation in AngularJS 1.2 by my BFF Matias Niemela for a more in-depth explanation. He wrote ngAnimate so it is safe to say that he knows things!
The first thing we need to do is to define the nature of the transition for adding and removing the wave class. Following the convention I described above, we will define a style for .wave.ng-enter and .wave.ng-leave since wave is the base style and ng-leave and ng-enter are the events that are triggered. From here, we are defining our transition to animate all properties using the cubic-bezier tween we defined at 0.5 seconds.
.wave.ng-enter, .wave.ng-leave { -webkit-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; }
First, we will define the transition for when an element is activated via ngSwitch. The base class is wave, the event is ng-enter and the state when it is actively applied aka the finished state is ng-enter-active. When a new step is activated, we want it to slide in from the left, so we start out by setting the left property to -100% and finish at 0.
.wave.ng-enter { position: absolute; left:-100%; } .wave.ng-enter.ng-enter-active { left:0; }
We will do the opposite for the step that is being left by harnessing the ng-leave event and starting at left:0 and and finishing with left: 100%.
.wave.ng-leave { position: absolute; left:0; } .wave.ng-leave.ng-leave-active { left:100%; }
The beauty of ngAnimate is that attaching animations is as easy as attaching a class to our view. In the curious case of our wizard, it goes no further than adding class=”wave” to our three ngSwitch divs.
<div ng-switch="modal.getCurrentStep()" class="slide-frame"> <div ng-switch-when="one" class="wave"></div> <div ng-switch-when="two" class="wave"></div> <div ng-switch-when="three" class="wave"></div> </div>
Handling the Response
And now we are full circle and it is time to hand off the results of our wizard to the main application. When we stored the reference of our modal as modalInstance, it came with a result property that is actually a promise and is resolved when the modal is closed or dismissed. The distinction between $modalInstance.close(data) and $modalInstance.dismiss(reason) is that the close handler is treated as the success function and the dismiss handler is treated as the error function.
angular.module('App', ['ngAnimate', 'ui.bootstrap']) .controller('AppCtrl', function ($modal) { var app = this; <pre><code> app.closeAlert = function () { app.reason = null; }; app.open = function () { var modalInstance = $modal.open({ templateUrl: 'partials/wizard.html', controller: 'ModalCtrl', controllerAs: 'modal' }); modalInstance.result .then(function (data) { app.closeAlert(); app.summary = data; }, function (reason) { app.reason = reason; }); }; }) </code></pre>
In the case of a dismissal, we are setting the reason to app.reason, which is tied to an alert in the view. This is slightly gratuitous but hey! That is how I roll! We then dismiss the alert by calling app.closeAlert().
<div class="alert-container"> <alert ng-if="app.reason" type="info" close="app.closeAlert()">{{app.reason}}</alert> </div>
In the case of submitting the wizard, we close the alert in case it is showing and then set app.summary to data, which is the result of the wizard.
<div ng-if="app.summary"> <hr/> <h3>Summary for {{app.summary.firstName}} {{app.summary.lastName}}</h3> <strong>Coming: </strong>{{app.summary.coming}} <strong>Tacos: </strong>{{app.summary.tacos}} <strong>Toppings:</strong> <div class="badge badge-success pull-left" ng-repeat="topping in app.summary.toppings">{{topping}} </div> </div>
We can then bind to app.summary in the view to show the results of the wizard. These last two items are more for illustration purposes, but I believe by now we have gotten the point.
Review
Let us take a moment to review what we have learned.
- We need to include the ngAnimate and ui.bootstrap submodules to make our wizard work.
- We instantiate a new modal buy calling open on the $modal service.
- The object returned by calling $modal.open has a property called result, which is a promise we use to handle the result of closing or dismissing the modal.
- We can use ngSwitch to toggle the steps within the wizard. We could also use ngShow, ngHide or ngIf to accomplish the same thing.
- By following the convention of [class-name].[event-name].[state-name], we are able to define the animations for a step animating in and out.
Resources
Remastered Animation in AngularJS 1.2
Demo Code
I couldn’t help but hear in my head “But Ramsey is not dancing, he does not dance at the partyyyy”. LOL. Nice post. How would you implement this with animate.css?
great stuff!