This is the third part of Introducing Angular Multi-Step Wizard using UI-Router Series 3. You will learn how to build a multi-step wizard using the following technologies:
- Series 1 – UI-Router legacy version 0.x for Angular 1
- Part 1 – Create a SPA
- Part 2 – Create Features
- Series 2 – UI-Router version 1.0 with Angular 1.5+ component-based architecture
- Part 1 – Create a SPA
- Part 2 – Create Features
- Series 3 – UI-Router version 1.0 for Angular 2 and TypeScript
- Part 1 – Create a SPA
- Part 2 – Create Features with Validation
- Part 3 – Create Workflow Validation (This Post)
The source code for this tutorial series is published on GitHub. Demo application is hosted in Microsoft Azure.
Part 3: Create Workflow Validation in UI-Router 1.0 for Angular 2
In the previous tutorial, we created the features with validation using UI-Router v 1.0, Angular 2, and TypeScript 2.0+. In this tutorial, we will use the same technologies to implement workflow validation in the following sequence of features:
Personal
Work
Address
Result
Client-Side Technologies:
- Angular 2.4.0
- SystemJS 0.19.40
- RxJS 5.0.3
- UI-Router 1.0.0-beta.4 for Angular 2
- TypeScript 2.0.10
- Circular Bootstrap tabs Snippet by riliwanrabo
- Bootstrap
- Font Awesome
The application structure, styles, and patterns follow the recommendations outlined in the official Angular Style Guide. The application idea is inspired by Scotch.io’s tutorial.
I use the following guides as a reference to write this tutorial:
- Angular Developer Guide
- UI-Router for Angular (2+) – onEnter
- CanActivate analogue in ui-router-ng2 1.0.0?
Task 1. Create the Workflow Feature
The Workflow
feature contains the following files:
- workflow.service.ts: is a service responsible for controlling the wizard workflow
- workflow.model.ts: defines the steps in wizard
Task 2. Add shared service to the Workflow Feature
-
Add the workflow.service.ts file to the workflow folder.
-
Replace the code in this file with the following:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950import { Injectable } from '@angular/core';import { STEPS } from './workflow.model';@Injectable()export class WorkflowService {private workflow = [{ step: STEPS.personal, valid: false },{ step: STEPS.work, valid: false },{ step: STEPS.address, valid: false },{ step: STEPS.result, valid: false }];validateStep(step: string) {// If the state is found, set the valid field to truevar found = false;for (var i = 0; i < this.workflow.length && !found; i++) {if (this.workflow[i].step === step) {found = this.workflow[i].valid = true;}}}resetSteps() {// Reset all the steps in the Workflow to be invalidthis.workflow.forEach(element => {element.valid = false;});}getFirstInvalidStep(step: string) : string {// If all the previous steps are validated, return blank// Otherwise, return the first invalid stepvar found = false;var valid = true;var redirectToStep = '';for (var i = 0; i < this.workflow.length && !found && valid; i++) {let item = this.workflow[i];if (item.step === step) {found = true;redirectToStep = '';}else {valid = item.valid;redirectToStep = item.step}}return redirectToStep;}}The root module class named
AppModule
will import theWorkflowService
as a service. TheWorkflowService
controls the workflow of the wizard.
Task 3. Add data model to the Workflow Feature
-
Add the workflow.model.ts file to the workflow folder.
-
Replace the code in this file with the following:
123456export const STEPS = {personal: 'personal',work: 'work',address: 'address',result: 'result'}
Task 4. Register the new service in the root module
-
Open the app.module.js file in the app folder.
-
Update the file with the following highlighted code:
123456789101112131415161718192021222324252627282930313233343536373839import { NgModule } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';import { UIRouterModule } from "ui-router-ng2";import { FormsModule } from '@angular/forms';/* App Root */import { AppComponent } from './app.component';import { NavbarComponent } from './navbar/navbar.component';/* Feature Components */import { PersonalComponent } from './personal/personal.component';import { WorkComponent } from './work/work.component';import { AddressComponent } from './address/address.component';import { ResultComponent } from './result/result.component';/* App Router */import { UIRouterConfigFn } from "./app.router";import { appStates } from "./app.states";/* Shared Service */import { FormDataService } from './data/formData.service';import { WorkflowService } from './workflow/workflow.service';@NgModule({imports: [ BrowserModule,FormsModule,UIRouterModule.forRoot({states: appStates,useHash: true,config: UIRouterConfigFn})],providers: [{ provide: FormDataService, useClass: FormDataService },{ provide: WorkflowService, useClass: WorkflowService }],declarations: [ AppComponent, NavbarComponent, PersonalComponent, WorkComponent, AddressComponent, ResultComponent ],bootstrap: [ AppComponent ]})export class AppModule {} -
Line 22 and 34 register the
WorkflowService
with the injector in the root module so that the same instance of theWorkflowService
is available to all part of the application.
Task 5. Register a lifecycle hook with the routes
-
Open the app.states.js file in the app folder.
-
Update the file with the following highlighted code:
1234567891011121314151617181920212223242526272829import { PersonalComponent } from './personal/personal.component';import { WorkComponent } from './work/work.component';import { AddressComponent } from './address/address.component';import { ResultComponent } from './result/result.component';import { WorkflowService } from './workflow/workflow.service';export const appStates = [// 1st State{ name: 'personal', url: '/personal', component: PersonalComponent },// 2nd State:{ name: 'work', url: '/work', component: WorkComponent, onEnter: verifyWorkFlow },// 3rd State{ name: 'address', url: '/address', component: AddressComponent, onEnter: verifyWorkFlow },// 4th State{ name: 'result', url: '/result', component: ResultComponent, onEnter: verifyWorkFlow }];function verifyWorkFlow(transition, state) {console.log("Entered '" + state.name + "' state.");var $stateService = transition.router.stateService;var workflowService = transition.injector().get(WorkflowService);// If any of the previous steps is invalid, go back to the first invalid steplet firstState = workflowService.getFirstInvalidStep(state.name);if (firstState.length > 0) {console.log("Redirected to '" + firstState + "' state which it is the first invalid step.");return $stateService.target(firstState);};} -
Line 5 imports the
WorkflowService
. -
Lines 11 registers a
onEnter
hook to verify workflow when thework
state is being entered. -
Lines 13 registers a
onEnter
hook to verify workflow when theaddress
state is being entered. -
Lines 15 registers a
onEnter
hook to verify workflow when theresult
state is being entered. -
Lines 18 – 29 create a function called
verifyWorkFlow
to verify workflow. It uses theWorkflowService.getFirstInvalidStep
method to get the first invalid state. If any of the previous steps did not have the required information, it redirects to the first invalid state. Otherwise, it resumes the transition to load the specified component. We use theconsole.log
method to record the transition in the browser console. To see the result, activate the browser console with F12, and select Console in the menu.
Task 6. Inject the new service to the Data Feature
-
Open the formData.service.js file in the data folder.
-
Update the file with the following highlighted code:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293import { Injectable } from '@angular/core';import { FormData, Personal, Address } from './formData.model';import { WorkflowService } from '../workflow/workflow.service';import { STEPS } from '../workflow/workflow.model';@Injectable()export class FormDataService {private formData: FormData = new FormData();private isPersonalFormValid: boolean = false;private isWorkFormValid: boolean = false;private isAddressFormValid: boolean = false;constructor(private workflowService: WorkflowService) {}getPersonal(): Personal {// Return the Personal datavar personal: Personal = {firstName: this.formData.firstName,lastName: this.formData.lastName,email: this.formData.email};return personal;}setPersonal(data: Personal) {// Update the Personal data only when the Personal Form had been validated successfullythis.isPersonalFormValid = true;this.formData.firstName = data.firstName;this.formData.lastName = data.lastName;this.formData.email = data.email;// Validate Personal Step in Workflowthis.workflowService.validateStep(STEPS.personal);}getWork() : string {// Return the work typereturn this.formData.work;}setWork(data: string) {// Update the work type only when the Work Form had been validated successfullythis.isWorkFormValid = true;this.formData.work = data;// Validate Work Step in Workflowthis.workflowService.validateStep(STEPS.work);}getAddress() : Address {// Return the Address datavar address: Address = {street: this.formData.street,city: this.formData.city,state: this.formData.state,zip: this.formData.zip};return address;}setAddress(data: Address) {// Update the Address data only when the Address Form had been validated successfullythis.isAddressFormValid = true;this.formData.street = data.street;this.formData.city = data.city;this.formData.state = data.state;this.formData.zip = data.zip;// Validate Address Step in Workflowthis.workflowService.validateStep(STEPS.address);}getFormData(): FormData {// Return the entire Form Datareturn this.formData;}resetFormData(): FormData {// Reset the workflowthis.workflowService.resetSteps();// Return the form data after all this.* members had been resetthis.formData.clear();this.isPersonalFormValid = this.isWorkFormValid = this.isAddressFormValid = false;return this.formData;}isFormValid() {// Return true if all forms had been validated successfully; otherwise, return falsereturn this.isPersonalFormValid &&this.isWorkFormValid &&this.isAddressFormValid;}} -
Line 4 imports the
WorkflowService
. -
Line 5 imports the
STEPS
. -
Lines 15 – 16 inject the
WorkflowService
. -
Lines 34 – 35 validate the
Personal
step because thePersonal
form had been validated successfully. -
Lines 47 – 48 validate the
Work
step because theWork
form had been validated successfully. -
Lines 69 – 70 validate the
Address
step because theAddress
form had been validated successfully. -
Lines 79 – 80 reset the workflow by invalidating all steps.
Task 7. Build and run the application
Open the terminal window, enter the npm start
command to compile *.ts files to *.js files, start the server, launch the application in the browser.
Task 8. Test the Workflow feature
One of the ways to test the Workflow
feature:
- Launch the application in the browser
- To see the transition process, activate the browser console with F12, and select Console in the menu.
- There are two ways to activate the
Address
feature:- Click the circular
home
icon in the tab menu - In your browser address bar, enter http://localhost:3000/#/address, and then press return
- Click the circular
- The application will redirect to the
Personal
view which is the first invalid step of the current workflow.
You have reached the end of tutorial series 3 on how to create a Multi-Step Wizard using UI-Router v 1.0 for Angular 2.
The source code for this tutorial series is published on GitHub. Demo application is hosted in Microsoft Azure.
References
- The official Angular Style Guide
- Angular Developer Guide
- UI-Router for Angular (2+) – onEnter
- CanActivate analogue in ui-router-ng2 1.0.0?
- Riliwan Rabo’s Circular Bootstrap tabs Snippet
- Scotch.io’s AngularJS Multi-Step Form Using UI Router
Hi CATHY ,
Thank you for posting implementation of multi step wizard . It’s really helpful to many developers .
I had one question for you , as i observed from the implementation point of view ,
My question is : how to link child tab to previous and next buttons .
Child tab are like under the first tab there will be two or more tabs again .And when we click second tab ,second tab may have nested tabs again .
If we keep on clicking next button , next button flow should be like first tab and first tab’s child tabs and second tab ,second tab’s child tabs should be active .
Please share your suggestions/ideas .
Thanks