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.

Post Categories

.NET

Last week after watched videos of ngConf 2015, I decided to have a try on Angular 2.0. And I think the best start point would be its new router. The application I'm going to change is "My DocumentDB", a web-based Microsoft Azure DocumentDB management tool I created last year. Although Google said it would be very simple and smoothly to migrate from Angular 1.x to this new router and then easy to go forward to Angular 2.0, I found a lot of problem if I'm using Angular-UI-Router.

In this post I would like to introduce what I did, what I suffered and the resolutions. But please keep in mind that Angular New Router is still in development phase. The code and API is being changed very often. So it's recommended by Google and the community, NOT to be used in any production application.

Upgrade to Angular.js 1.3.15 or Higher

I didn't find any documentation on the wiki of the new router (and the wiki is outdate right now I think) about the dependencies. But in fact we need to upgrade Angular.js reference to at least 1.3.15, since the new router needs template module that's not available in pervious version of Angular.

Download & Install

There are several ways we can get the code of the new router. The wiki told us to download through NPM. But I recommended just download directly from its GitHub repository. The source code is under "dist" folder and we need to download "router.es5.js" for Angular 1.x application.

Add to Dependency

As I mentioned my application was using Angular-UI-Router. In order to use Angular New Router I need to switch the dependency. The module name is 'ngNewRouter'.

1:var app = angular.module('MyDocDB', [

2:'ngNewRouter',

3:'ui.bootstrap'

4: ]);

Define the Routes

New router uses a factory named "$router" for routes definition, URL generation and the page navigation. It likes previous router and Angular UI Router very much. The only difference is "$router" is a factory instead of a provider. This means we cannot define our routes in Angular.js "configuration" routine, but in a controller or it's "run" routine.

Below is the routes definition for the new router in my application. I put the code in "app.run" function.

1: app.run(['$router', function ($router) {

2: $router.config([

3: {

4: path: '/dashboard',

5: component: 'dashboard'

6: },

7: {

8: path: '/console',

9: component: 'console'

10: },

11: {

12: path: '/credits',

13: component: 'credits'

14: },

15: {

16: path: '/database',

17: component: 'database'

18: },

19: {

20: path: '/collections',

21: component: 'collection'

22: },

23: {

24: path: '/documents',

25: component: 'document'

26: },

27: {

28: path: '/',

29: redirectTo: '/dashboard'

30: }

31: ]);

32: }]);

Re-Organize Source Files to Support Component

Angular new router introduces a new concept called "component". A component is a view, plus a controller, and an optional router. This sound like "state" in UI Router. (In UI Router, a route contains a state, an option with URL and views.) This encourages us to put everything related with a feature in a dedicate folder. For example let's say I have a contact feature, then I need a folder named "contact" that contains all stuff related with contact, such as "contact.js" for controllers, "contact.html" for view, and some other files such as "contact-import-dialog.js", "contact-import-dialog.html" and "contact-avatar.js" for directives, factories and filters etc. If we did like this, it would not be a massive effort in this step.

But if I organized my application by Angular categories, this would be painful. For example, I have a folder named "views" contains "contact.html", "account.html" and "order.html" while a folder named "controllers contains all Angular controllers, "services" folder contains all factories and services.

And even worse, there's a project named "angular-seed" by the Angular team, demonstrated the best practice an application skeleton for a typical Angular.js web app, which was organized in this way. Although the current version of angular-seed was organized by features to fulfill the component concept in Angular 2. But if your application was referred by this project before last year, you might run into the problem that I have, as below.

So what I need is to re-organize them in the proper way. And as you can see, file name for my default views are all "index.html". This will make angular new router cannot find them when a route is active. So I also need to change the file name from "index.html" to the related component name.

You may notice that I didn't put my controllers into the components folder. Well I personally suggest you move them as well to make the folder structure clean. But this is actually optional. Angular new router utilizes "controller as" syntax to find the controller name from the component name. The default syntax is, convert the first char of the component name to upper case, then append "Controller". For example, if I have a component named "database", the controller name should be "DatabaseController".

The good news is, this can be configured. There's a provider named "$componentLoaderProvider". We can define how Angular find the controller by the component name. In my case all my controllers a named with "Ctrl" suffixed. So this is the code for my application.

"$componentLoaderProvider.componentToTemplate" can be used to configure how new router find view template path by component name. The default logic is go to "/components" folder, find the folder with the component name, then find file in dash mode. For example, the default view path of "database" component would be "/components/database/database.html".

Change $scope in Controllers

In Angular 1.x we use "$scope" to communicate between view and controller. But in angular new router, we cannot do this. Angular new router use the component name to bind data with the controller. This means if we have a database controller and want to show database name in view, the code below would not work.

1:// Controller

2: app.controller('DatabaseController', function ($scope) {

3: $scope.name = 'db1';

4: });

5:

6:// View

7: <p>{{name}}</p>

Instead we need to change the code as below.

1:// Controller

2: app.controller('DatabaseController', function () {

3:this.name = 'db1';

4: });

5:

6:// View

7: <p>{{database.name}}</p>

I don't know whether this change is good or not, but in fact it introduces a lot more effort in migration. And more confusing, "$scope" would turned to be available if the controller was not activated by new router. For example, if I specified a controller by "ng-controller" in HTML, "$scope" would be available.

In order to minimize migration effort what I did is to have a local variant named "$scope" and assign "this" to it in each controllers. Then we don't need to change any further codes in JavaScript, just need to change view content.

Change View and Link Directives

Angular new router uses "ng-viewport" directive for view definition. It uses "ng-link" to generate link for a component. This would be very easy it migrate.

In the official wiki it said you need to use "router-view-port" and "router-link". But this should be obsoleted. The latest documents in Github have been changed based on the latest commits, says using "ng-viewport" and "ng-link".

Below are some code changes related with them in my application.

Define & Pass Parameters in Route

Sometimes we need to pass parameters though route. For example when user tried to view the details of a contact, we might want to pass the contact ID to the route through URL, then the details controller can retrieve ID and fire $http request to the backend service. This can be done easily in Angular UI Router, but not that easy in new router at this stage, especially if we want to pass parameters through query string with some specially chars for URL. Let's have a look to my application.

In "My DocumentDB" when user click into a collection or a document, I need to pass some parameters, such as the collection ID, link, document ID and link. It can be done in Angular UI Router in the way below.

1: $stateProvider.state('document', {

2: url: '/documents/?did&dl&cid&cl',

3: templateUrl: '/views/document/index.html',

4: controller: 'DocumentIndexCtrl'

5: });

Then I can generate the actual URL by using "ui-sref" directive. The URL will be generated accordingly with necessary URL encoding.

Unfortunately angular new router doesn't support passing parameter through query string at this stage. After dig into its Github repository I found an issue that someone already raised. timkindberg said since angular new router utilizes route-recognizer which is definitely solid in its support for query parameters, it should be OK to use query string. Then SamVerschueren posted a solution and a commit which angular new router must be changed to support it.

Since this commit was not included in "router.es5.js" in the latest version (0.5.1), we need to change it manually.

Now when we need to pass parameters in query string we can use "ng-link" as below. And we can retrieve them through "$location.search()".

This can solve most of the cases, but not mine. In "My DocumentDB" I need to pass database, collection and document link through query string and the link contains some chars which need to be URL encoded. This is being taken cared by UI Router. But when I migrated to angular new router I have to do this by myself.

And when I retrieved values from query string I also need decode them as well.

Two Other Issues

Basically after applied steps listed above, we should be able to make our application run under the new angular router. But when I migrated I also find some other issues. It may or may not impact your application but I think I'd better point them out.

The first one is "Controller Executed Twice". I have a plunker to demonstrate and already sent an issue to angular team. After launch my plunker and navigate between three routes, you will find the controller was executed twice indicated in browser console. Angular team had assign this issue fix into the next version (0.5.2).

Second, related with the first. When I passed parameters though query string, since the controller was executed twice, some parameters was missed in its first execution. This caused my application crashed due to mandatory information missed. So what I did is to perform some additional validation.

6:// it's only because angular new router missed some query strings in its first execution

7:if ($scope.col.collectionLink) {

8: ... ...

9: }

10: };

11:

12: ... ...

13: });

Summary

In this post I introduced my experience of migrating my application from Angular UI Router to Angular New Router. I listed all steps, as well as issues and resolution I found. But there are some features I didn't cover such as URL generation, multiple views, controller lazy load etc.

Since the angular new router is still in development phase, there are many problems and effort to migrate. And based on the experience I have to say it's very hard and time consuming to migrate an Angular application from 1.x to new router, then might cost more to move to 2.0. Hence I strongly suggest NOT trying to do this for your Angular application until it become more stable. And I also have to say, this might be a big problem when user wants to use Angular especially when 2.0 is going to be out. Google should be thinking more carefully about the backward compatibility and migration effort.