The Sheep-Pen of the Shaun

News

Shaun, the author of this blog is a semi-geek, clumsy developer, passionate speaker and incapable architect with about 10 years’ experience in .NET and JavaScript. He hopes to prove that software development is art rather than manufacturing. He's into cloud computing platform and technologies (Windows Azure, Amazon and Aliyun) and right now, Shaun is being attracted by JavaScript (Angular.js and Node.js) and he likes it.

Shaun is working at Worktile Inc. as the chief architect for overall design and develop worktile, a web-based collaboration and task management tool, and lesschat, a real-time communication aggregation tool.

Image Galleries

.NET

In my project I have a feature which needs user to specify a large amount of properties against an object. I though a wizard would be the best solution since it makes the user focus on part of the object in each steps. Then I checked the Internet to see if any existing Angular directives is OK to me. I found this, this and this. All of them are awesome but unfortunately none of them covers all my requirement. So I decided to create my own wizard, and below I would like to introduce how to use it.

You need to add "ui.bootstrap" as well since I'm using it to open bootstrap modal window.

Define Wizard in Controller

In the controller (in fact anywhere in your code), add parameter named "$wizard", which you can define a new instance of wizard, configure and add steps.

1: app.controller('HomeController', function($scope, $wizard) {

2:var wizard = $wizard

3: .config({

4: title: 'Wizard - by Shaun\'s Angular Toolkits',

5: size: 'lg',

6: shadow: true,

7: successing: function($data, $step, $isLastStep, callback) {

8:return callback(true);

9: }

10: });

11: });

Use "config" function to define overall layout and behavior of this wizard. It accepts an object as options with properties listed below.

1. title: Title of the wizard. Default is "Wizard".

2. size: Size of the wizard modal window, same as the option in UI-Bootstrap you can use 'lg', or 'sm'. Default is 'lg'.

3. shadow: Boolean value to determine whether wizard will use a shadow "div" convers on the step area when it's entering or leaving, prevent user click while the step is loading or validating. I will cover this feature later. Default is "true".

5. templateUrl: Specify the template URL of the wizard. "sx-wizard-tpls.js" will be used by default. Note that this is the template of wizard, not the steps.

6. successing: Function which will be invoked when user clicked "Finished" button. This can be used for final validation. The parameters are described as below. In default it will do nothing and close the wizard.

6.1 $data: Object passed into wizard.

6.2 $step: The step where user clicked "Finish".

6.3 $isLastStep: Indicates whether this is the last step.

6.4: callback: Callback function with a boolean parameter, indicates whether wizard can be closed (valid) or not (invalid).

Next, you may need to prepare data which is going to pass into wizard, and user will update it in each steps. For example, in the code below I created an object in $scope with two properties.

1: $scope.data = {

2: username: 'shaun',

3: email: 'jfarrio@gmail.com'

4: };

Then you can open wizard with 3 parameters.

1. data: Object which user will update through each steps.

2. success: A function invoked when user completed wizard, with the filled data as the parameter.

2.1:This method will be invoked only when it passed "successing" function in wizard configuration mentioned above.

2.2: Object in parameter is NOT the same one you passed in the first parameter. You can assign it back to the original object. This would be benefit when you don't want wizard to change your original data until user completed.

3. cancel: Function will be invoked when user dismissed the wizard. Usually you don't need to specify it.

1: $scope.launch = function() {

2: wizard.open(

3: $scope.data,

4:function(result) {

5: $scope.result = result;

6: },

7: window.angular.noop);

8: };

Add First Step

Use "wizard.addStep" to add a step into wizard before open it. You can chain this method with "config" as below.

1:var wizard = $wizard

2: .config({

3: ... ...

4: })

5: .addStep({

6: id: 'step-1-welcome',

7: title: 'Welcome',

8: templateUrl: 'steps/step-01-welcome.html'

9: });

I added a step with

1. id: Must be unique in a wizard instance.

2. title: Title of this step. I will use "id" if not specified.

3. templateUrl: Specify the template URL of the step layout. We can use "template" as inline template, too.

The template of this step would be like this.

1:<p>

2: This is the first step of the wizard.

3:</p>

4:<p>

5: Below is the initial object we will going to fill in the following steps.

6:</p>

7:

8:<divclass="panel panel-info">

9:<divclass="panel-heading">

10: Data

11:</div>

12:<divclass="panel-body">

13:<pre>{{$context.data | json}}</pre>

14:</div>

15:</div>

Now I can launch the wizard.

The initial data you passed though "wizard.config" can be retrieved and updated inside each steps, through the variant "$scope.$context.data". In fact, all properties under "$scope.$context" can be visited through all steps. So if you need to pass something in the whole wizard please add them into "$scope.$context".

Step Entering: Load Data

Sometimes we need to load some data when a step was entered. And in some cases we need to retrieve them through some external services via "$http". This means the loading operation might be asynchronous. Besides, we don't need load them every time this step was entered. For example, we don't need load them again if user clicked "Previous" button to navigate back.

"sx.wizard" handles this requirement by adding a function in "$scope.$context.behavior.entering" in step's controller. You can perform your logic to load data or any preparation when step is going to be shown. It contains two parameters.

1. options: Information related with this entering operation.

1.1 fromStepId: The step id which navigated from. It might be "undefined" if this is the first step and in the wizard when it just opened.

1.3: entered: Indicates whether this step had performed "entering". This is useful to prevent from load data again and again when navigated back and forth.

2. callback: Invoke this parameter-less function it indicate the entering operation had been finished. Don't forget to call it even though nothing to do.

In the code below I added another step where user can set more properties on "$scope.$context.data". I'm using "$timeout" to simulate asynchronous loading. I checked "options.entered" to make sure data will be loaded only the first time entered, and I called "callback()" even though "options.entered === true" to tell wizard entering is finished.

59:<spanng-show="basicInfoForm.country.$error.required">This field is required.</span>

60:</p>

61:</div>

62:</form>

Now let start this wizard you can click "Next" to navigate to this step.

When wizard is performing step's entering logic, there will be an icon pulsing to prevent user touch the step UI, since at this point the UI may contain some invalid or partial data which might cause some errors.

You can remove this feature by setting "shadow = false" in "wizard.config()". If so there will be no icon and user can touch step UI even during entering phase.

Also the navigation buttons will also be disabled except "Cancel".

Once the entering function finished (developer called "callback()") the icon will be disappeared, user can use this step and navigation buttons will be enabled.

Step Leaving: Validation

In this step I have some input fields where user can specify more properties against the initial data stored in "$scope.$context.data". When user leave this step we need to validate. This can be done by using another function in step's controller name "$scope.$context.behavior.leaving".

Similar as "$scope.$context.behavior.entering", this method will be invoked when a step is going to be left, with the parameters as below.

2. callback: Invoke this function it indicate the leaving operation had been finished, with a boolean parameter indicates whether this step can be left or not. You can put validation result as the parameter so user cannot move forward if any input were wrong. Don't forget to call it even move backward (options.forward === false).

Below is the code I put in this step for validation. I'm using "$timeout" to simulate asynchronous validation. Note that I checked "options.forward" so that the validation will only be invoked when moving forward. And note that I also called "callback(true)" even moving backward to make sure the step can be left.

"sx.wizard" shows icon as well when performing step leaving if you specified "shadow = true". Same as when step was being entered.

If the validation was failed you cannot leave this step.

But you can click "Previous" button even though errors in this step. This is because I just perform validation when "options.forward === true".

Customize Next and Finish Buttons

Sometimes we want to customize "Next" button and "Finish" button. For example, when user provide contact information in a wizard, she might need skip "additional information" step if checked "No additional information needed" box on in "basic information" step. Or, she might need to be able to click "Finish" button to save this contact if checked "No further information needed" on.

In "sx.wizard" you can configure which step will be navigated when user clicked "Next", as well as whether the "Finish" button will be shown, through two properties in "$scope.$context.navigation".

1. nextStepId: Return the step id when user clicked "Next" button.

2. showFinish: Indicates whether "Finish" button will be shown. Note that if this is the last step in wizard, "Finish" button will always be shown.

8:<pclass="help-block">Check this box on and press "Next" you will skip the next step.</p>

9:<pclass="help-block">Note the the "Previous" button is smart enough to back to the previous step you visited.</p>

10:</div>

11:</div>

12:</div>

13:

14:<divclass="panel panel-default">

15:<divclass="panel-body">

16:<p>You can specify whether the "Finish" button should be shown.</p>

17:<divclass="checkbox">

18:<label>

19:<inputtype="checkbox"ng-model="$context.navigation.showFinish"> Show finish button in this step.

20:</label>

21:<pclass="help-block">Check this box on you will see "Finish" button.</p>

22:<pclass="help-block">Wizard will show "Finish" button on its last step even though you spcified not to show.</p>

23:</div>

24:</div>

25:</div>

When user checked the second box on, "Finish" button will be shown.

Add More Navigation Buttons

The default navigation buttons (previous, next, finish and cancel) may not be enough in all cases. Developer may need provide more options to user. Sample example, in "basic information" step we might want allow user to input additional information by clicking "Next", allow user go to "relationship step" by clicking "Select Relations" button, allow user go to "attachments step" by clicking "Add Attachments" and allow user to save the contact right now by clicking "Finish".

2. stepFn: A function which return the step id you want to navigate when user clicked.

Below is the step that have three buttons added.

1:var wizard = $wizard

2: .config({

3: ... ...

4: })

5: .addStep({

6: ... ...

7: })

8: .addStep({

9: ... ...

10: }

11: .addStep({

12: ... ...

13: })

14: .addStep({

15: ... ...

16: })

17: .addStep({

18: id: 'step-5-customize-navigation',

19: title: 'Customize navigation buttions',

20: templateUrl: 'steps/step-05-custmize-nav.html',

21: controller: 'wizardStepCustmizeNavCtrl'

22: })

23: .addStep({

24: ... ...

25: });

The controller code was defined in a separated function as below. User will go to the first step when clicking the first button, go to the last step when clicking the second and the third one will lead user to step which she selected from UI.

1: app.controller('wizardStepCustmizeNavCtrl', function($scope) {

2: $scope.steps = [];

3:

4: window.angular.forEach($scope.$context.steps, function(step, id) {

5:if (id !== $scope.$context.currentStepId) {

6: $scope.steps.push(step);

7: }

8: });

9:

10: $scope.$context.navigation.buttons = [{

11: text: 'Go First',

12: stepFn: function() {

13:return $scope.steps[0].id;

14: }

15: }, {

16: text: 'Go Last',

17: stepFn: function() {

18:return $scope.steps[$scope.steps.length - 1].id;

19: }

20: }, {

21: text: 'Go Dynamic',

22: stepFn: function() {

23:return $scope.targetStepId;

24: }

25: }, ];

26: });

Below is the layout of this step.

1:<p>You can specify more buttons in the navigation in your step's controller.</p>

2:<p>Below we have 3 buttons</p>

3:<ul>

4:<li>The first one will navigate to the welcome step (first step).</li>

5:<li>The second one will navigate to the summary step (last step).</li>

6:<li>The thrid one is dynamical, it will navigate to the step you selected.</li>

7:</ul>

8:<p>Also note that the controller of this step was defined in a separeated file with the controller name specified.</p>

13:<optionng-repeat="step in steps"value="{{step.id}}">{{step.title}}</option>

14:</select>

15:<pclass="help-block">Select one step and click "Go Dynamic" button in navigation will lead you to that step.</p>

16:</div>

When you navigated to this step you will find three buttons on the left side of navigation. They will go to steps you defined in your code.

Step Template & Controller

You may noticed that in one of step I defined the template as inline string rather than the URL. "sx.wizard" supports define step template using inline HTML or URL. It will check if "step.template" was defined. If so it will use the value as template, otherwise it will try to load the content from the address in "step.templateUrl".

Besides you can specify step controller as an inline anonymous function, or the name of the controller defined in your angular module.

Summary

There are many implementation of angular wizard directives as I mentioned at the beginning of this post. But I don't think I reinvented the wheel. "sx.wizard" provides some new features.

1. Wizard is defined and launched from angular factory named "$wizard" which means you can use it in any where in your code, controllers, directives and services.

2. You specify template and controller for each steps separately, which is clear, and can be used in other wizards.

3. Handling step entering and leaving event where you can organize your data loading and validation code better. Both of them are designed for asynchronous operations.

4. Customize navigation buttons in each step. You can change which step will be navigated when user clicked "Next", you can add more buttons, and you can specify whether the "Finish" button should be shown in each step's controller.

5. Step template can be defined inline or through URL, controller can be inline or through the name.

"sx.wizard" is published under MIT license. So feel free to use it in your projects. Any issues, comments or suggestion please raise. Also I would like to dig into its source code to explain how I implemented in the future.

Comments

#re: Angular Directive: Wizard in bootstrap modalPosted by Lorena
on 4/20/2015 2:04 PM
The computer definetively has an unique way of processing the information and this is the hardest stuff for me to understand. I guess that sometimes I just get mad because I would have wished to have great skills in programming. Diana

#re: Angular Directive: Wizard in bootstrap modalPosted by Jack Tocken
on 6/12/2015 10:07 AM
I tried your example but I always get $wizard.config is not a function. Anything I missed? Thanks!

#re: Angular Directive: Wizard in bootstrap modalPosted by Shaun
on 6/12/2015 12:22 PM
Sorry about this. I updated the source code and renamed 'config' to '$new' due to a bugfix. But I didn't updated this post and the demo in plunker. Please use '$wizard.$new()', the parameter structure is the same.

#re: Angular Directive: Wizard in bootstrap modalPosted by Lenny
on 8/3/2015 8:51 AM
This is excellent! Thank you so much for this.

#re: Angular Directive: Wizard in bootstrap modalPosted by Steve
on 8/31/2015 6:05 AM
This is exactly what I needed. I'm just shaky on one part. What's the best way to get data from one step to the next? Let's say the user selects "blue" from a color list on step 1. The color will then be used to filter the choices allowed on step 2. How do I get the user's selection into that controller?

#re: Angular Directive: Wizard in bootstrap modalPosted by Steve
on 8/31/2015 6:13 AM
Oh, duh. Just add it to $context.data

#re: Angular Directive: Wizard in bootstrap modalPosted by Steve
on 8/31/2015 6:01 PM
I added the following code to the wizardStepCustmizeHavCtrl but its not being triggered on entering. Appreciate any help.

From your code I think you should append 'return callback()' after '$scope.enteredCount = 1' statement in the 'else' clause, so that wizard knows your entering operation had been done.

Make sure to call 'callback' in all exit paths in 'entering' and 'leaving' functions.

#re: Angular Directive: Wizard in bootstrap modalPosted by Steve
on 9/3/2015 6:44 PM
This module does exactly what I needed and does it well. Thanks!

What's the difference in general between $scope and $scope.context? On one hand, overriding $scope.context.entering in a controller only applies to that step, but on the other hand data added to $scope.context is available in all steps. Is there a philosophy here? Not that important as it does work.

#re: Angular Directive: Wizard in bootstrap modalPosted by Shaun Xu
on 9/6/2015 8:59 AM
@SteveWell that's a little bit dig into my implementation. Basically each step's controller has its own '$scope.$context', which means they are NOT shared. But '$scope.$context.data' is an exception. If you check the source code in line 276, the value of '$scope.$context.data' are referring from the same object named 'scope.$data', which is the scope of the whole directive. This is the reason you can put something inside '$scope.$context.data' and it will be available in all steps, but others are not.

Hope I answered your question.

#re: Angular Directive: Wizard in bootstrap modalPosted by Wei
on 2/26/2016 11:35 AM
Hi Shaun,I want to use radio to go different end step.is that possible to show finish and hide next button in a step which is not last one?

Showing "Finish" button is possible as I mentioned in the post above, just set "showFinish = true". But the visibility of "Next" button was controlled by the index of current step, which means it will always be shown if NOT the last step.

How can i navigate to the next step without going though the navigation buttons. I see that you're using 2 functions go and goById. Is there a way to access these functions?

Cheers,

#re: Angular Directive: Wizard in bootstrap modalPosted by Martin Omander
on 12/2/2016 9:37 AM
Looks like a great component! But when I run the demo in Plunker, I get this error message and the modal doesn't display: