Shaun Xu

The Sheep-Pen of the Shaun


News

logo

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

Shaun is working at IGT Technology Development (Beijing) Co., Ltd. as the architect and product manager responsible for design and develop the next generation, web-based casino management system.

MVP

My Stats

  • Posts - 108
  • Comments - 385
  • Trackbacks - 0

Tag Cloud


Recent Comments


Recent Posts


Article Categories


Archives


Image Galleries


.NET



I started to use Geekwithblogs (a.k.a. GWB) since 2010, based on one of my friend's recommendation. I've to say during the past 5+ years I was really enjoying blogging and had published 107 posts with 380 comments. GWB provided an awesome platform where I can share my experience and discuss with a lot of talents.

 

But since last month I found my blog look strange. On May 29th I found all my categories are lost. And when I tried to create a new category it still cannot be saved. This means all my well-categorized 107 posts are messed up.

image

Several days later I found my gallery was emptied in admin page, too, even though I can access images stored there.

image

 

Well I think this is not a big issue. Maybe GWB was updating, or maybe my site was hacked. So when I found the issue on May 29th I tried to contact Jeff Julian, the staff of GWB who helped me to map blog.shaunxu.me to my blog before. But no response till now.

Then I tried to find any channels to the team of GWB, but no luck. There seems no entry or link on geekswithblogs.net, or in admin page mentions how to contact them. Finally I tried to use the "Suggest" link on geekswithblogs.net and posted an item, but still no reply till now.

image

 

Today I suddenly found my blog theme was changed. After resumed the theme I think this might be the only way to report my problem, which is to publish a post. Sorry if I border you but I really want to check what's going on with GWB? Is there anyone who is still maintaining this site?

 

Hope anyone can help me,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.


Today when I upgraded my application from Angular.js 1.3.9 to the latest 1.4, I got some bugs. After investigated a bit I found they are related with date properties and "angular.merge" function, which was included in 1.4.

In the official document, "angular.merge" is

Deeply extends the destination object "dst" by copying own enumerable properties from the "src" object(s) to "dst".

It also mentioned the different with "angular.extend", which had been included in previous versions.

Unlike "extend()", "merge()" recursively descends into object properties of source objects, performing a deep copy.

 

Let's have a look on a very simple example. In code below I have a source object and custom object defined in scope. And I used "angular.extend" to copy custom object into source.

   1: $scope.source = {
   2:   name: 'Shaun',
   3:   age: 35,
   4:   birthDate: new Date(1997, 5, 27),
   5:   skills: {
   6:     dotNet: {
   7:       level: 'expert',
   8:       years: 10
   9:     },
  10:     javaScript: {
  11:       level: 'newbie',
  12:       years: 2
  13:     }
  14:   },
  15:   mvp: [
  16:     2011,
  17:     2012,
  18:     2013,
  19:     2014,
  20:     2015
  21:   ]
  22: };
  23:  
  24: $scope.custom = {
  25:   name: 'Ziyan',
  26:   age: 35,
  27:   skills: {
  28:     dotNet: {
  29:       level: 'hero', 
  30:       years: 100,
  31:       active: true
  32:     },
  33:   },
  34:   mvp: [
  35:     2016
  36:   ]
  37: };
  38:  
  39: $scope.extend = angular.extend({}, $scope.source, $scope.custom);

From the result we can see, since "angular.extend" performs shallow copy, primitive value property such as "name", "age" and "bitrhDate" are merged. But since "skills" and "mvp" are object and array, "angular.extend" will just copy the entire value, rather than their members.

image

 

Now let's using "angular.merge".

   1: $scope.merge = angular.merge({}, $scope.source, $scope.custom);

Now we can see, when using "angular.merge", it will copy object properties recursively.

image

 

Everything looks great till now. But someone may find when using "angular.merge", one of the property, "birthDate" was been set as am empty object.

image

If we deep into the source code of Angular.js we will find, both "angular.extend" and "angular.merge" are invoking an internal function named "baseExtend". It merges objects to destination object, with a flag parameter indicates whether it's a deep merge or not.

Inside this function, it loops each enumerable properties, try to copy the value to destination object. If the source property is an object and need to be deeply copied, Angular.js will create an empty object in destination and perform this function against this property recursively.

   1: function baseExtend(dst, objs, deep) {
   2:   var h = dst.$$hashKey;
   3:  
   4:   for (var i = 0, ii = objs.length; i < ii; ++i) {
   5:     var obj = objs[i];
   6:     if (!isObject(obj) && !isFunction(obj)) continue;
   7:     var keys = Object.keys(obj);
   8:     for (var j = 0, jj = keys.length; j < jj; j++) {
   9:       var key = keys[j];
  10:       var src = obj[key];
  11:  
  12:       if (deep && isObject(src)) {
  13:         if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
  14:         baseExtend(dst[key], [src], true);
  15:       } else {
  16:         dst[key] = src;
  17:       }
  18:     }
  19:   }
  20:  
  21:   setHashKey(dst, h);
  22:   return dst;
  23: }

It works in almost all cases but Date. If we have a Date property defined, for example "birthDate", it will check if this property is an object by using "angular.isObject" and it will return "true". So it will create a property named "birthDate" in destination with an empty object, and invoke "baseExtend" against "birthDate". But since "birthDate" is a Date which is no enumerable property, so it will not assign any data. This is the reason we found in result, "birthDate" property is empty.

If I copied Angular.js "baseExtend" function to my scope, and changed the code as below, which will perform simple copy when the property is Date, it will work.

   1: $scope.$baseExtend = function (dst, objs, deep) {
   2:   for (var i = 0, ii = objs.length; i < ii; ++i) {
   3:     var obj = objs[i];
   4:     if (!angular.isObject(obj) && !angular.isFunction(obj)) {
   5:       console.log('[' + obj + '] = (skip)');
   6:       continue;
   7:     }
   8:     var keys = Object.keys(obj);
   9:     for (var j = 0, jj = keys.length; j < jj; j++) {
  10:       var key = keys[j];
  11:       var src = obj[key];
  12:       // perform deep copy if
  13:       // 1. spcified by user
  14:       // 2. source property is an object
  15:       // 3. source property is NOT a date
  16:       if (deep && angular.isObject(src) && !angular.isDate(src)) {
  17:         if (!angular.isObject(dst[key])) {
  18:           console.log('[' + key + '] = (Try copy an object to an non-object, create an empty for deep copy.)');
  19:           dst[key] = angular.isArray(src) ? [] : {};
  20:         }
  21:         $scope.$baseExtend(dst[key], [src], true);
  22:       } 
  23:       else {
  24:         dst[key] = src;
  25:         
  26:         console.log('[' + key + '] = ' + src);
  27:       }
  28:     }
  29:   }
  30:   return dst;
  31: };
  32:  
  33: $scope.sample = $scope.$baseExtend({}, [$scope.source, $scope.custom], true);

This is the result.

image

 

Summary

Upgrade framework to a new version is always be an adventure. We need to perform a lot of regression tests to make sure it will not break anything. This problem was found when I perform E2E tests.

I think this is bug in Angular.js and I had posted an issue. But before their verification and response, I think you should pay more attention when using "angular.merge" function in your application.

 

Hope this helps,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.


Directive is very powerful in Angular.js. I would like call it "User Control for Web". When I'm writing sx.wizard, sx.highlight and sx.tabs, I was using directives. And in directive, one of the most important concept is isolated scope.

 

Shared Scope

Let's start with an example. Assuming I have an Angular.js application. I have a controller contains an object, and display the content in the page. The code is very simple.

   1: <body ng-app="Demo" >
   2:   <div class="panel panel-info" ng-controller="HomeCtrl">
   3:     <div class="panel-heading">Home Controller</div>
   4:     <div class="panel-body">
   5:         <pre>{{person | json}}</pre>
   6:  
   7:         <hr>
   8:     </div>
   9:   </div>
  10:  
  11:   <script>
   1:  
   2:     (function() {
   3:       var app = angular.module('Demo', []);
   4:  
   5:       app.controller('HomeCtrl', function($scope) {
   6:         $scope.person = {
   7:           name: 'Shaun',
   8:           age: 36,
   9:           Company: 'IGT'
  10:         };
  11:       });
  12:     }());
  13:   
</script>
  12: </body>

And below is what the page looks like.

image

Now let create a very simple directive which will show the object from the parent controller.

   1: app.directive('sxIsolatedScopeDemo', function () {
   2:   return {
   3:     templateUrl: '/tpl.html'
   4:   };
   5: });

Below is the template content of this directive.

   1: <pre>{{person | json}}</pre>
   2: <div class="form-horizontal">
   3:   <div class="form-group">
   4:     <label class="col-sm-2 control-label">Name</label>
   5:     <div class="col-sm-10">
   6:       <input type="text" class="form-control" placeholder="Name" ng-model="person.name">
   7:     </div>
   8:   </div>
   9:   <div class="form-group">
  10:     <label class="col-sm-2 control-label">Age</label>
  11:     <div class="col-sm-10">
  12:       <input type="number" class="form-control" placeholder="Age" ng-model="person.age">
  13:     </div>
  14:   </div>
  15:   <div class="form-group">
  16:     <label class="col-sm-2 control-label">Company</label>
  17:     <div class="col-sm-10">
  18:       <input type="text" class="form-control" placeholder="Company" ng-model="person.company">
  19:     </div>
  20:   </div>
  21: </div>

And now let's insert this directive into the controller we defined previously.

   1: <div class="panel panel-info" ng-controller="HomeCtrl">
   2:   <div class="panel-heading">Home Controller</div>
   3:   <div class="panel-body">
   4:       <pre>{{person | json}}</pre>
   5:  
   6:       <hr>
   7:       
   8:       <div class="panel panel-success">
   9:         <div class="panel-heading">Directive (Not Isolated Scope)</div>
  10:         <div class="panel-body">
  11:             <div sx-isolated-scope-demo></div>
  12:         </div>
  13:       </div>
  14:   </div>
  15: </div>

Well below is what the page looks like now. As you can see we can use variants defined in the parent scope of our directive. And when I changed the object content, parent scope also be changed as well.

image

This is OK for our demo, but there's a big problem. Our directive must be located in a controller where its scope contains a variant named "person". Also, anything changed against "person" inside the directive will affect the one in parent scope. As what you've seen in the screenshot above, "person" in both parent scope and directive scope are all changed when I specified new value. And even worst, if we defined some new variants in directive's scope, it will affect the scope in parent scope, which is definitively not what we want.

 

Isolated Scope

It's very simple to create an isolated scope in a directive. Below is what my directive with an isolated scope.

   1: app.directive('sxIsolatedScopeDemo', function () {
   2:   return {
   3:     scope: {},
   4:     templateUrl: '/tpl.html'
   5:   };
   6: });

Below is what it looks like now. Note that when we change something and add new variant inside the directive scope, it will NOT affect the parent scope.

ezgif.com-optimize (1)

 

Share Values in Isolated Scope

In order to make directive to be able to touch values defined in the parent scope we can specify in its scope options. Angular.js provides three ways: specify a variant, an expression, and a function.

We use "=" with attribute name to define a variant scope binding. Below I told my directive, there's an attribute named "the-person" defined in its HTML element, and the value of this attribute is the variant name defined in parent scope, which will be bind into the variant "me" inside the directive's scope.

   1: app.directive('sxIsolatedScopeDemo', function () {
   2:   return {
   3:     scope: {
   4:       me: '=thePerson'
   5:     },
   6:     templateUrl: '/tpl.html'
   7:   };
   8: });

And in the HTML part we added "the-person" attribute and point it to "person", which is the value we want to share from the parent scope.

   1: <div class="panel panel-success">
   2:   <div class="panel-heading">Directive</div>
   3:   <div class="panel-body">
   4:       <div sx-isolated-scope-demo the-person="person"></div>
   5:   </div>
   6: </div>

Also changed a little in the directive template as below.

   1: <pre>{{me | json}}</pre>
   2: <pre>$scope.foo = {{foo}}</pre>
   3:  
   4: <div class="form-horizontal">
   5:   <div class="form-group">
   6:     <label class="col-sm-2 control-label">Name</label>
   7:     <div class="col-sm-10">
   8:       <input type="text" class="form-control" placeholder="Name" ng-model="me.name">
   9:     </div>
  10:   </div>
  11:   <div class="form-group">
  12:     <label class="col-sm-2 control-label">Age</label>
  13:     <div class="col-sm-10">
  14:       <input type="number" class="form-control" placeholder="Age" ng-model="me.age">
  15:     </div>
  16:   </div>
  17:   <div class="form-group">
  18:     <label class="col-sm-2 control-label">Company</label>
  19:     <div class="col-sm-10">
  20:       <input type="text" class="form-control" placeholder="Company" ng-model="me.company">
  21:     </div>
  22:   </div>
  23:   <div class="form-group">
  24:     <label class="col-sm-2 control-label">Foo</label>
  25:     <div class="col-sm-10">
  26:       <input type="text" class="form-control" placeholder="" ng-model="foo">
  27:     </div>
  28:   </div>
  29: </div>

Now we can see that the person information was passed into the directive in "scope.me". When we change the value inside this variant it will affect the parent scope. But if we add something new in directive's scope, for example "foo", it will NOT change the parent scope.

ezgif.com-optimize (2)

Another way to share value between parent scope and directive is to use "@" + expression. In this case, Angular.js will execute the expression defined in the attribute and set the result into the scope in directive. For example, if I have a scope property named "parentFoo" defined in parent scope and I'd like my directive scope to get its value I can use the code below.

   1: app.directive('sxIsolatedScopeDemo', function () {
   2:   return {
   3:     scope: {
   4:       me: '=thePerson',
   5:       foo: '@theFoo'
   6:     },
   7:     templateUrl: '/tpl.html'
   8:   };
   9: });

Changed the directive HTML element to bind it with an expression pointing to the "parentFoo".

   1: <div class="panel panel-success">
   2:   <div class="panel-heading">Directive</div>
   3:   <div class="panel-body">
   4:       <div sx-isolated-scope-demo the-person="person" the-foo="{{parentFoo}}"></div>
   5:   </div>
   6: </div>

Below is the result. Note that when using expression binding all changes in directive scope will NOT affect to the parent scope property.

image

Expression binding allows us to generate value from parent scope properties and pass the result to directive, as below.

   1: <div sx-isolated-scope-demo 
   2:      the-person="person" 
   3:      the-foo="{{person.name}} is {{person.age}} years old working at {{person.company}}."></div>
Here is the result.
image

Sometimes we also need to invoke functions defined in the parent scope. In this case we can use "&" to tell the directive which function it will use. In the code below I added another directive scope options named "hi" which pointing to the attribute named "the-say-hi".

   1: app.directive('sxIsolatedScopeDemo', function () {
   2:   return {
   3:     scope: {
   4:       me: '=thePerson',
   5:       foo: '@theFoo',
   6:       hi: '&theSayHi'
   7:     },
   8:     templateUrl: '/tpl.html'
   9:   };
  10: });

In the page I bind it with a function in the scope named "sayHi".

   1: <div sx-isolated-scope-demo 
   2:      the-person="person" 
   3:      the-foo="{{person.name}} is {{person.age}} years old working at {{person.company}}."
   4:      the-say-hi="sayHi(exp)"></div>

And below is the function defined in parent scope, which just popup a browser alert window.

   1: app.controller('HomeCtrl', function($scope) {
   2:   $scope.person = {
   3:     name: 'Shaun',
   4:     age: 36,   
   5:     company: 'IGT'
   6:   };
   7:   $scope.parentFoo = 'bar';
   8:   $scope.sayHi = function (exp) {
   9:     alert('Hi. ' + exp);
  10:   };
  11: });

Finally in my directive template I added a button to invoke the function defined in the isolated scope, then it will find the binding function defined in parent scope. Note that how I pass parameters into the function.

   1: <button type="button" class="btn btn-default" ng-click="hi({exp: foo})">Say Hi</button>

Below is the result.

ezgif.com-optimize (3)

 

Summary

Directive is very powerful in Angular.js. It's very important to understand isolated scope when creating a reusable directive. In this post I described the different between shared scope and isolated scope. I also demonstrated how to pass data from parent scope into directive isolated scope through "=", "@" and "&", and the different between them.

 

Hope this helps,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.


When I'm working in my project, as well as development "sx.wizard" toolkit, I need to deal with asynchronous operation. For example, in "sx.wizard", template for each steps are loaded in parallel asynchronously. And the "$wizard" service will start to render UI once all templates are loaded successfully. In this case we need to use $q.

 

$q was explained in Angular.js document as follow.

A service that helps you run functions asynchronously, and use their return values (or exceptions) when they are done processing.

And it's inspired by Kris Kowal's Q, which is widely used in Node.js. It's also following ES6 promises.

To me, when you have a function which the result will be returned in sometime in the future, and you need to continue your procedure based on the return value, then you need to consider using $q. Let's take a very simple example.

 

Handle Asynchronous Operation

Assuming I have an angular.js application which need to display "Hello Shaun!", and "Shaun" should be appeared 2 seconds later, the code could be as below.

   1: <!DOCTYPE html>
   2: <html>
   3:   <head>
   4:     <script data-require="angular.js@1.4.0-rc.1" data-semver="1.4.0-rc.1" src="https://code.angularjs.org/1.4.0-rc.1/angular.js"></script>
   1:  
   2:   </head>
   3:  
   4:   <body ng-app="Demo" ng-controller="HomeCtrl">
   5:     <h1>Hello {{name}}!</h1>
   6:  
   7:       <script>
   8:         (function () {
   9:             var app = angular.module('Demo', []);
  10:  
  11:             app.controller('HomeCtrl', function ($scope, $timeout) {
  12:                 $timeout(function () {
  13:                     $scope.name = 'Shaun';
  14:                 }, 2000);
  15:             }); 
  16:         }());
  17:       
</script>
   5:  
   6:   </body>
   7: </html>

Here is the result.

01

If we want to continue our procedure we can simply add codes inside $timeout function as below.

   1: app.controller('HomeCtrl', function ($scope, $timeout) {
   2:     $timeout(function () {
   3:         $scope.name = 'Shaun';
   4:         alert('Continue working...');
   5:     }, 2000);
   6: }); 

In this case it seems no need to introduce $q. But let's assume the name will be retrieved from another service or component. Normally it might be retrieved some external web service through $http but in order to simplify the example let create service and let it return name value in 2 seconds.

   1: app.factory('nameService', function ($timeout) {
   2:     return {
   3:         get: function () {
   4:             $timeout(function () {
   5:                 return 'Shaun';
   6:             }, 2000);
   7:         }
   8:     };
   9: });

In controller we use this service.

   1: app.controller('HomeCtrl', function ($scope, nameService) {
   2:     $scope.name = nameService.get();
   3: }); 

Looks great. But when we launch our website we will find the name will never be shown in the page, even though in the service we returned in 2 seconds. This is because "nameService.get" function returned "undefined" after it processed $timeout. 2 seconds later we returned name value but our controller has no chance to get it.

 

image

In this case we can use $q to notify controller the name was retrieved please update scope.

   1: app.factory('nameService', function ($q, $timeout) {
   2:     return {
   3:         get: function () {
   4:             return $q(function (resolve, reject) {
   5:                 $timeout(function () {
   6:                     resolve('Shaun');
   7:                 }, 2000);
   8:             });
   9:         }
  10:     };
  11: });

We use "$q" as a constructor, which takes a function with two parameters: resolve and reject, both of them are functions. When we got the value, we can invoke "resolve" with the value as the parameter. When there's an exception, we can invoke "reject" to tell the caller it failed. In the code above I invoked "resolve" after 2 seconds with "Shaun", the name value I got.

Back to the controller, after invoked "nameService.get" we got a promise created by $q. When the result was ready it will invoke a function defined through its "then" method as following.

   1: app.controller('HomeCtrl', function ($scope, nameService) {
   2:     var getPromise = nameService.get();
   3:     
   4:     getPromise.then(function (name) {
   5:         $scope.name = name;
   6:     });
   7: }); 

 

Invocation Chaining

We can return a new promise inside "then" function from an existing promise. This makes us very easy to manage multiple asynchronous functions especially when some are depends on others. Database operation is a good example. Normally we need to open the database, then it's opened we will query records, and then we got the records we will filter them, prepare data and insert or update, then we close the connection. Below is "database" service I created, note that each function returns a promise.

   1: app.factory('database', function ($q, $timeout) {
   2:     return {
   3:         open: function (db) {
   4:             return $q(function (resolve, reject) {
   5:                 $timeout(function () {
   6:                     resolve(db + ' is connected.');
   7:                 }, 2000);
   8:             });
   9:         },
  10:         query: function (db, query) {
  11:             return $q(function (resolve, reject) {
  12:                 $timeout(function () {
  13:                     resolve(db + ' [' + query + '] processed successuflly.');
  14:                 }, 2000);
  15:             });  
  16:         },
  17:         execute: function (db, command) {
  18:             return $q(function (resolve, reject) {
  19:                 $timeout(function () {
  20:                     resolve(db + ' [' + command + '] invoked successuflly.');
  21:                 }, 2000);
  22:             });  
  23:         },
  24:         close: function (db) {
  25:             return $q(function (resolve, reject) {
  26:                 $timeout(function () {
  27:                     resolve(db + ' is disconnected.');
  28:                 }, 2000);
  29:             });  
  30:         }
  31:     };
  32: });

In controller I invoked "database.open" method and handle and append a log entry when connected. Note that in this function I invoked "database.query", which return another promise.

   1: app.controller('HomeCtrl', function ($scope, database) {
   2:     $scope.logs = [];
   3:     var db = 'shaun-db';
   4:     
   5:     database.open(db)
   6:             .then(function (result) {
   7:                 $scope.logs.push(result);
   8:                 return database.query(db, 'SELECT * FROM [Products]');
   9:             });
  10: }); 

Now I can invoke "then" right after it, which called "invocation chaining", to perform my next operation.

   1: app.controller('HomeCtrl', function ($scope, database) {
   2:     $scope.logs = [];
   3:     var db = 'shaun-db';
   4:     
   5:     database.open(db)
   6:             .then(function (result) {
   7:                 $scope.logs.push(result);
   8:                 return database.query(db, 'SELECT * FROM [Products]');
   9:             })
  10:             .then(function (result) {
  11:                 $scope.logs.push(result);
  12:                 return database.execute(db, 'INSERT INTO [Products] VALUE (5, \'Worktile\')');
  13:             })
  14:             .then(function (result) {
  15:                 $scope.logs.push(result);
  16:                 return database.close(db);
  17:             })
  18:             .then(function (result) {
  19:                 $scope.logs.push(result);
  20:             });
  21: }); 

Now let's run it you will see the operations are invoked one by one in series.

02

 

$q also supports execute asynchronous functions in parallel. In the example above we invoked one command when database was connected. It's a common case we can run some query in parallel, and when all of them are finished we will run some commands, then close the connection.

Below I tweaked "database" service a little bit in its "execute" method, allow user specify the timeout period.

   1: execute: function (db, command, timeout) {
   2:     return $q(function (resolve, reject) {
   3:         $timeout(function () {
   4:             resolve(db + ' [' + command + '] invoked successuflly.');
   5:         }, timeout); 
   6:     });  
   7: },

In controller when the database was connected and first query finished, I invoked 5 commands through "execute" method, which will be processed in parallel, and pushed the promises into an array, Then I was using "$q.all" method. This method accepts an array of promises and will invoke its "then" when all of them are resolved.

   1: .then(function (result) {
   2:     $scope.logs.push(result);
   3:     var commandPromises = [];
   4:     commandPromises.push(database.execute(db, 'CMD 1', 1000));
   5:     commandPromises.push(database.execute(db, 'CMD 2', 500));
   6:     commandPromises.push(database.execute(db, 'CMD 3', 200));
   7:     commandPromises.push(database.execute(db, 'CMD 4', 200));
   8:     commandPromises.push(database.execute(db, 'CMD 5', 3000));
   9:     return $q.all(commandPromises);
  10: })

After that I executed two commands in parallel in the same way, and close the database connection when all of them are finished.

   1: .then(function (result) {
   2:     $scope.logs.push(result);
   3:     var commandPromises = [];
   4:     commandPromises.push(database.execute(db, 'CMD 6', 200));
   5:     commandPromises.push(database.execute(db, 'CMD 7', 300));
   6:     return $q.all(commandPromises);
   7: })

When we run our application you will see the procedure was waiting for all parallel commands finished then process the next one.

03

 

Examples in Shaun's Angular Toolkits

Now let's take a look on how I used $q in the code of my angular toolkits. In "sx.wizard" user can specify step template URL or template inline code. When "template" property was specified I will use it directly, when "templateUrl" was specified I will load the content through $http, which is an asynchronous operation. Hence I have a function responsible for creating template promise.

   1: var _getTemplatePromise = function(step) {
   2:     if (step.template) {
   3:         step.template = '' +
   4:             '<div class="sx-wizard-step" sx-wizard-step-id="' + step.id + '">' +
   5:             step.template +
   6:             '</div>';
   7:         return $q.when(step);
   8:     } else {
   9:         return $http.get(step.templateUrl).then(function(response) {
  10:             step.template = '' +
  11:                 '<div class="sx-wizard-step" sx-wizard-step-id="' + step.id + '">' +
  12:                 response.data +
  13:                 '</div>';
  14:             return step;
  15:         });
  16:     }
  17: };

In the code above I checked if "template" property was specified. If so I will wrap some HTML code and return a new promise through "$q.when". "$q.when" can be used when we are not pretty sure if an object is a normal object or a promise. It create a new promise wrapping this kind of object so that we can dealing it in $q. If the value passed in "$q.when" is a normal object it will be resolved immediately.

If "templateUrl" was specified I invoked "$http.get" to retrieve template content. Since "$http.get" returns a promise I use its "then" method to assign the content to step's template property.

Finally I returned the promise, no matter created by "$q.when" or "$http.get", into a local array.

   1: var _stepTemplatePromises = [];
   2:  
   3: ... ...
   4:  
   5: wizard.addStep = function(step) {
   6:     ... ...
   7:     _stepTemplatePromises.push(_getTemplatePromise(step));
   8:     return wizard;
   9: };

Then when the wizard is going to be shown, it will wait until all template are loaded successfully through "$q.all", then start to prepare scope and render UI.

   1: wizard.open = function(data, success, cancel) {
   2:     ... ...
   3:     $q.all(_stepTemplatePromises).then(function() {
   4:         var instance = $modal.open({
   5:             ... ...
   6:         });
   7:         instance.result.then(function(data) {
   8:             return success(data);
   9:         }, function() {
  10:             return cancel();
  11:         });
  12:     });
  13:     return wizard;
  14: };

 

Another example in "sx.tabs", which I'm going to introduce in the near future. User can specify tab content through inline template or URL. So I have the similar function for tab content loading.

   1: var _setTemplatePromise = function (tab) {
   2:     if (tab.template) {
   3:         tab.$templatePromise = $q.when(tab);
   4:     }
   5:     else {
   6:         tab.$templatePromise = $http.get(tab.templateUrl, {cache: $templateCache}).then(function (response) {
   7:             tab.template = response.data;
   8:             return tab;
   9:         });
  10:     }
  11: };

Then a tab was clicked, I checked the template promise status, when it's resolved I will start execute the tab's controller, compile and render.

   1: scope.switchTab = function (options, callback) {
   2:     ... ...
   3:     if (id && (!scope.activeTab || !scope.activeTab.id || scope.activeTab.id !== id)) {
   4:         var tab = scope.$tabs[id];
   5:         if (tab) {
   6:             $q.when(tab.$templatePromise).then(function () { 
   7:                 ... ...
   8:             });
   9:         }
  10:         else {
  11:             return callback(false);
  12:         }
  13:     }
  14:     else {
  15:         return callback(false);
  16:     }
  17: };

 

Summary

Asynchronous function is very common in JavaScript. There are many libraries handles this scenario. Angular.js support it by using "$q". Comparing with others pattern "$q" is lightweight and simple, with enough functionalities. And all asynchronous components in Angular.js are built based on "$q" such as "$http", "$timeout", etc.

When your operation is asynchronous, and you need to invoke some following operations after it finished, you should think about "$q" firstly.

 

Hope this helps,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.


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.

 

Features & Demo

Below is the image I recorded. You can check the live demo here.

sx-wizard

I call this directive "sx.wizard" and it supports:

1. Open wizard in a bootstrap modal window.

2. Configure window size, wizard title, etc..

3. Add steps from JavaScript (your angular code not only in controller, but anywhere you like).

4. Specify wizard behaviors in each steps, such as input validation, configure which step will be navigated to when clicked "Next", add more buttons in the bottom, show "Finish" button or not, etc..

 

Install

To use "sx.wizard" you need the following pre-requirements:

1. Bootstrap: You need full package of bootstrap, includes CSS and JavaScript.

2. jQuery: In theory this is not necessary since Angular will inject jQLite if no jQuery installed. But as I've never tested it without jQuery I recommended to install.

3. Angular.js: Just make sure it's Angular 1.x. Angular 2.x is NOT supported right now (and in future I think).

4. UI-Bootstrap: I'm using Angular UI Bootstrap to show a modal window that contains the wizard.

5. Font Awesome: (Optional) I'm using font awesome to display icons for step entering and leaving. But this is not mandatory. The icon will not be shown if you skip this requirement.

 

After that just download and include template and source code of "sx.wizard".

1. Template: https://github.com/shaunxu/angular-toolkits/blob/master/wizard/sx-wizard-tpls.js

2. Source: https://github.com/shaunxu/angular-toolkits/blob/master/wizard/sx-wizard.js

 

Add dependencies in your angular root module.

   1: window.app = window.angular.module('Demo', [
   2:     'ui.bootstrap', 
   3:     'sx.wizard'
   4: ]);

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".

4. backdrop: Bootstrap modal options. Default is 'static' which means user cannot dismiss the wizard until clicked "Cancel" button.

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: <div class="panel panel-info">
   9:   <div class="panel-heading">
  10:     Data
  11:   </div>
  12:   <div class="panel-body">
  13:     <pre>{{$context.data | json}}</pre> 
  14:   </div>
  15: </div>

Now I can launch the wizard.

image

 

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.2: forward: Indicates whether it's navigated forward (from step defined previously). It only depended on the sequence of step definition.

  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.

   1: var wizard = $wizard
   2:     .config({
   3:         ... ...
   4:     })
   5:     .addStep({
   6:         ... ...
   7:     })
   8:     .addStep({
   9:         id: 'step-2-update-data',
  10:         title: 'Update data, entering and validating',
  11:         templateUrl: 'steps/step-02-update-data.html',
  12:         controller: function($scope, $timeout) {
  13:             $scope.$context.behaviour.entering = function(options, callback) {
  14:                 $scope.basicInfoForm.$setPristine();
  15:                 if (options.entered) {
  16:                     return callback();
  17:                 } else {
  18:                     $timeout(function() {
  19:                         $scope.genders = ['Male', 'Female'];
  20:                         $scope.countries = ['China', 'US', 'UK'];
  21:                         return callback();
  22:                     }, 2000);
  23:                 }
  24:             };
  25:         }
  26:     });

The template would be like this.

   1: <p>
   2:   You can update data (add, remove or change properties) in each steps.
   3: </p>
   4: <p>
   5:   It support defines logic for data loading when step entered, as well as validation when leaving.
   6:   Both are in standard JavaScript callback-style.
   7: </p>
   8:  
   9: <form novalidate name="basicInfoForm">
  10:   <div class="form-group">
  11:     <label for="username">Username</label>
  12:     <input type="text" class="form-control" id="username" name="username" ng-model="$context.data.username" required>
  13:     <p class="help-block">Update exising property.</p>
  14:     <p ng-show="basicInfoForm.$submitted" class="text-danger">
  15:       <span ng-show="basicInfoForm.username.$error.required">This field is required.</span>
  16:     </p>
  17:   </div>
  18:   <div class="form-group">
  19:     <label for="email">Email</label>
  20:     <input type="email" class="form-control" id="email" name="email" ng-model="$context.data.email" required email>
  21:     <p class="help-block">Update exising property.</p>
  22:     <p ng-show="basicInfoForm.$submitted" class="text-danger">
  23:       <span ng-show="basicInfoForm.email.$error.required">This field is required.</span>
  24:       <span ng-show="basicInfoForm.email.$error.email">This field must be in email format.</span>
  25:     </p>
  26:   </div>
  27:   <hr>
  28:   <div class="form-group"> 
  29:     <label for="firstName">First name</label>
  30:     <input type="text" class="form-control" id="firstName" name="firstName" ng-model="$context.data.name.firstName">
  31:     <p class="help-block">Specify new property.</p>
  32:   </div>
  33:   <div class="form-group">
  34:     <label for="lastName">Last name</label>
  35:     <input type="text" class="form-control" id="lastName" name="lastName" ng-model="$context.data.name.lastName" required>
  36:     <p class="help-block">Specify new property.</p>
  37:     <p ng-show="basicInfoForm.$submitted" class="text-danger">
  38:       <span ng-show="basicInfoForm.lastName.$error.required">This field is required.</span>
  39:     </p>
  40:   </div>
  41:   <hr>
  42:   <div class="form-group">
  43:     <label>Gender</label>
  44:     <select class="form-control" name="gender" ng-model="$context.data.gender" required>
  45:       <option ng-repeat="gender in genders" value="{{gender}}">{{gender}}</option>
  46:     </select>
  47:     <p class="help-block">Async load options when entered (use <code>$timeout</code> to simulation.</p>
  48:     <p ng-show="basicInfoForm.$submitted" class="text-danger">
  49:       <span ng-show="basicInfoForm.gender.$error.required">This field is required.</span>
  50:     </p>
  51:   </div>
  52:   <div class="form-group">
  53:     <label>Country</label>
  54:     <select class="form-control" name="country" ng-model="$context.data.contry" required>
  55:       <option ng-repeat="contry in countries" value="{{contry}}">{{contry}}</option>
  56:     </select>
  57:     <p class="help-block">Async load options when entered (use <code>$timeout</code> to simulation.</p>
  58:     <p ng-show="basicInfoForm.$submitted" class="text-danger">
  59:       <span ng-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.

image

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.

image

 

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.

1. options: Information related with this leaving operation.

  1.1 toStepId: Step id that is going to be navigated to.

  1.2 forward: Indicates whether it's navigated forward (from step defined previously). This might be useful when you don't want to perform validation when user click "Previous" button.

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.

   1: var wizard = $wizard
   2:     .config({
   3:         ... ...
   4:     })
   5:     .addStep({
   6:         ... ...
   7:     })
   8:     .addStep({
   9:         id: 'step-2-update-data',
  10:         title: 'Update data, entering and validating',
  11:         templateUrl: 'steps/step-02-update-data.html',
  12:         controller: function($scope, $timeout) {
  13:             $scope.$context.behaviour.entering = function(options, callback) {
  14:                 ... ...
  15:             };
  16:             $scope.$context.behavior.leaving = function(options, callback) {
  17:                 if (options.forward) {
  18:                     $timeout(function() {
  19:                         $scope.basicInfoForm.$setSubmitted();
  20:                         return callback($scope.basicInfoForm.$valid);
  21:                     }, 2000);
  22:                 } else {
  23:                     return callback(true);
  24:                 }
  25:             };
  26:         }
  27:     });

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

image

If the validation was failed you cannot leave this step.

image

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.

In order to demonstrate I added three steps in wizard.

   1: var wizard = $wizard
   2:     .config({
   3:         ... ...
   4:     })
   5:     .addStep({
   6:         ... ...
   7:     })
   8:     .addStep({
   9:         ... ...
  10:     }
  11:     .addStep({
  12:         id: 'step-3-config-next',
  13:         title: 'Configure the "Next" button behavior',
  14:         templateUrl: 'steps/step-03-config-next.html',
  15:         controller: function($scope) {
  16:             $scope.skipTemplateStep = false;
  17:  
  18:             $scope.$watch('skipTemplateStep', function(skipTemplateStep) {
  19:                 $scope.$context.navigation.nextStepId = skipTemplateStep ? 'step-6-summary' : null;
  20:             });
  21:         }
  22:     })
  23:     .addStep({
  24:         id: 'step-4-step-template',
  25:         title: 'Specify step layout with inline HTML',
  26:         template: '<p>Besides <code>templateUrl</code> you can specify step\'s layout by using inline HTML with <code>template</code>.</p>'
  27:     })
  28:     .addStep({
  29:         id: 'step-6-summary',
  30:         title: 'Summary',
  31:         templateUrl: 'steps/step-06-summary.html'
  32:     });

The layout of step 3 would be like this. I added two input fields in UI. The 'Next" and "Finish" button will be changed based on user's input.

When user checked the first box on, it will go to the last step "step-6-summary". Otherwise it will returned "null" which make "Next" button perform default action, go to next step.

   1: <div class="panel panel-default">
   2:     <div class="panel-body">
   3:         <p>You can specify which step will be navigated when user clicks "Next" button in your step's controller.</p>
   4:         <div class="checkbox">
   5:           <label>
   6:             <input type="checkbox" ng-model="skipTemplateStep"> Skip next step: Specify step layout in template.
   7:           </label>
   8:           <p class="help-block">Check this box on and press "Next" you will skip the next step.</p>
   9:           <p class="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: <div class="panel panel-default">
  15:     <div class="panel-body">
  16:         <p>You can specify whether the "Finish" button should be shown.</p>
  17:         <div class="checkbox">
  18:           <label>
  19:             <input type="checkbox" ng-model="$context.navigation.showFinish"> Show finish button in this step.
  20:           </label>
  21:           <p class="help-block">Check this box on you will see "Finish" button.</p>
  22:           <p class="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.

image

 

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".

"sx.wizard" supports adding navigation buttons in step's controller via "$scope.$context.navigation.buttons". It's an array contains button objects displayed in this step with the properties of each button object.

1. text: Text shown as the button caption.

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>
   9:  
  10: <div class="form-group">
  11:   <label>3rd buttons targeting step</label>
  12:   <select class="form-control" name="targetStepId" ng-model="targetStepId">
  13:     <option ng-repeat="step in steps" value="{{step.id}}">{{step.title}}</option>
  14:   </select>
  15:   <p class="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.

image

 

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".

image

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

image

 

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.

 

Hope this helps,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.