The upgrading of an Angularjs SPA into an hybrid one (Angularjs + Angular.io) is composed by one step that is hardest than the others and this step is the refactoring of js files in typescript modules.
What we’ve seen in the previous post is prepare the Angularjs files and remove incompatibilities like $scope and implicit annotations, in this step we transform these file into Typescript modules, fully compliant to Angular.io; after that, we’ll be able to inject for example a service to a new Angular component.
For that, we need to do these steps:
- Installing and configuring Typescript
- Upgrade modules, controllers, factories and directives to Typescript Modules
Configuring Typescript
The first step is installing Typescript into the project, npm install typescript –save-dev.
Once installed, we create the tsconfig.json with these parameters:
{ "compilerOptions": { "downlevelIteration": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "importHelpers": true, "module": "esnext", "moduleResolution": "node", "target": "es2015", "outDir": "transpiled/app", "declaration": false, "sourceMap": true, "skipLibCheck": true, "lib": [ "es6", "dom" ] } }
These parameters are about the adoption of the functionalities of ES6 and webpack as module bundler.
The values define how the dependencies chain of the modules will be resolve (moduleResolution: node means that it’ll be used the Node.js resolution algorithm) and the format of the generated js outputs (module: esnext, webpack compliant).
In this task we can install another npm package, @types/angular, that contains the definitions of typescript types for Angulajs; this will be useful in the next step.
Finally we can change the file extension of our Angulajs SPA (modules, controllers…) in ts, the compiler will generate same files with js extension in the outDir folder.
Typescript Modules
As said, this is the most challenging step, where we’ll spend the large part of time.
Speaking about the components that we have seen in the previous post, we could have a service like this one:
(function (window, angular) { 'use-strict'; angular.module('blogsModule') .factory('blogsService', function ($http, odataGenericResource) { return new odataGenericResource('odata', 'Blogs', 'Id'); }) })
The new version will be something like this:
import { OdataGenericResource } from '../shared/odataResourcesModule'; export class BlogsService extends OdataGenericResource { static $inject = ['$http']; constructor($http: ng.IHttpService) { super($http, 'Blogs'); } }
With Typescript and ES6 we can use the import statement to import dependencies like classes; in this example my service extends a base class.
The other detail is the explicit annotation that is applied with the $inject.
More interesting the refactoring of a controller:
import { StateService } from '@uirouter/core'; import { IResource } from '../shared/OdataResource'; import { BlogsService } from './blogsService'; export class BlogsController { public Blogs: Array<IResource>; static $inject = ['$state', 'blogsService', '$http']; constructor(private $state: StateService, private blogsService: BlogsService, private $http: ng.IHttpService) { var vm = this; this.blogsService.list().then(function (result) { vm.Blogs = result.data["value"]; }); } new() { this.$state.go("home.blog", { id: null }); } detail(id) { this.$state.go("home.blog", { id: id }); } }
We can see that in the same way we have import statements for the dependencies and the $inject used for explicit annotations.
The last one is the module:
import * as angular from 'angular'; import { BlogsService } from './blogsService'; import { BlogsController } from './blogsController'; import BlogController from './blogController'; export const blogsModule = angular.module('blogsModule', ['ui.router']) .config(['$stateProvider', function ($stateProvider) { $stateProvider .state('home.blogs', { url: '/blogs', templateUrl: 'app/main/blogs/blogs.html', controller: 'blogsCtrl' }) .state('home.blog', { url: '/blog/:id', templateUrl: 'app/main/blogs/blog.html', controller: 'blogCtrl' }); }]) .factory('blogsService', BlogsService) .controller('blogsCtrl', BlogsController) .controller('blogCtrl', BlogController);
Here we have the imports of factories and controllers, the ui router states declarations and the registrations of the factories and controllers.
As we have seen, we have defined the classes of controllers and services but we didn’t registered them to the module, so we do it now.
The last one is the app module:
import * as angular from 'angular'; import { mainModule } from './main/mainModule'; import { blogsModule } from './main/blogs/blogsModule'; import { postsModule } from './main/posts/postsModule'; import { uiModule } from './main/shared/uiModule'; export const angularjsAppModule = angular.module('angularjsApp', ['ui.router', mainModule.name, blogsModule.name, postsModule.name, uiModule.name]) .config(['$urlRouterProvider', '$locationProvider', function ($urlRouterProvider, $locationProvider) { $urlRouterProvider.otherwise('/home/blogs'); $locationProvider.hashPrefix(''); }]);
The new version is not very different from the original one, excluding the imports of the modules.
At the end, we have converted our application to Typescript modules and we are ready to the last two steps of the activity.
Summary
Implementing Typescript modules means that all our code is organized in classes and modules, the main class have to be exported and the dependencies imported in order to be used; webpack will take care about the process of resolving the dependencies.
Differently from the part 1, this activity of converting an Angularjs app into an Hybrid one leave the application into an inconsistent state.
If at the end of the first part we could push all the changes in production without regressions, now we can’t because without a module bundler our application will not works.
Don’t worry, the last two steps are faster that this one and consists to install Angular.io and webpack.
You can find the source code of this post here.