Time to upgrade your AngularJS application

This blog post is part of my talk at 1st Angular Athens meetup entitled From Gulp to Angular CLI and beyond. It describes the migration of an AngularJS application to Angular using an approach that is focused on tooling and development workflow. It addresses mainly projects that use Gulp as their development and build tool and want to replace it with Angular CLI.

This article is not trying to explain the upgrade process in detail but rather how to approach the problem of migration so that the process can be straight forward for you and your team. It gives you hints on how to move to the new Angular and simultaneously have your application running and deployed for the duration of the upgrade.

You can find the slides of the presentation here and the complete source code in this GitHub repository. The code is organized in branches, each one representing a step in the migration process, so that you can follow along. The master branch is the starting point and the final branch contains the code after completion of the upgrade.

The story

One of the main products at Plexscape, where I am currently working on, is a licensing web application with a code base of ~ 3000 LOC that is built with AngularJS. At the time of my talk, the application was using the latest version of the framework, 1.6.10 . After Angular released version 2.0, we decided that it was time to upgrade our application.

We started experimenting with the upgrade guide of the official Angular documentation which was using SystemJS. The process was not straight forward and demanded a lot of coding from our side to set it up (not enough boilerplate). At the same time, Angular CLI had started releasing its first beta versions. We decided to postpone the upgrade process and wait for the first release of the Angular CLI, since our purpose was to move closely to the Angular ecosystem.

The steps that we followed to upgrade our application are described below in the context of a demo AngularJS application, for the sake of simplicity.

The application

Angular Heroes

The application is called Angular Heroes and consumes the Marvel API to provide basic search functionality over the Marvel characters database. It is built with AngularJS 1.6.9 and AngularJS Material 1.1.7.

It uses Gulp for building, minification and bundling purposes. All 3rd party libraries are installed via Bower. The structure of the project is greatly inspired by the following repositories:

Lite-server is used as the preferred Web server during development.

Project structure

The root folder of the project consists of various configuration files for testing, serving and building the application as well as the src folder that contains all application assets (JS, CSS, images).

Contents of src folder

The src folder is further subdivided in two subfolders: one named app that contains the Javascript files separated into modules and another called content for CSS files and images.

Step 1: Move into Angular

Angular CLI

Our first goal is to change the toolchain early in the process and move closer to the Angular ecosystem by integrating Angular CLI. We need to first install it using npm. Open a terminal and execute the following to install it globally:

$ npm install -g @angular/cli@1.7.4

After it has been installed, create a new blank application using the CLI:

$ ng new my-app

We can bring our code into the new Angular environment in two ways: copy our files into the app directory of the newly created project or copy all files from the newly created project into our current project. Either way is valid.

We can then remove lite-server configuration and most of gulp tasks since we will be using Angular CLI for serving, building and bundling purposes, from now on.

We are not going to remove templateCache task yet because we still need a way to process templates from the AngularJS code during the upgrade. Please note that we may have duplicates because the template of an Angular component will be included in the final bundle from the CLI and in the template cache.

Any static assets such as images, are moved to the assets folder of the new project and global CSS styles are copied over to the styles.css file.

Application and 3rd party library asset paths are removed from the index.html file and are defined in the appropriate sections of .angular-cli.json file. Finally, we set the main property of the Angular CLI configuration file to app.js to define the starting point of our application.

Angular CLI configuration excerpt

Step 2: Move into Typescript

Typescript

In this step we are going to replace Javascript with Typescript, a process that will help us to spot bugs early during the development.

The easiest way towards Typescript integration is to rename all Javascript files (*.js) to Typescript (*.ts). That is the perfect time also to install types for AngularJS and 3rd party libraries and start using ES6 statements in our code such as let , const and arrow functions. We need to set typeRoots property in compilerOptions of tsconfig.app.json file so that the Typescript compiler can recognise the installed types.

"typeRoots": [

"../node_modules/@types"

]

You may experience errors from tslint after using types. You could try to correct these errors before continuing if you wish. Please note that if you don’t, the project will still build succesfully.

At this point, we can make appropriate changes in AngularJS code to benefit from the types installation.

Convert all AngularJS components and services to ES6 classes and export them.

If you are using the vm notation to declare an AngularJS controller, you could replace it with $ctrl which is a built-in feature of AngularJS 1.5.0 and later.

Declare the variable angular wherever you use it to keep the tslint compiler happy.

declare var angular: angular.IAngularStatic;

Add types to services and components.

A good practise is to add types everywhere even when you do not have one. You can declare it as any and this will act as reminder to fill in later the missing types (Just search for the any keyword 😃).

Add implements keyword to components or services that implement certain interface methods such as $onInit .

ComicList component implements $onChanges method

We can now remove bower and install package dependencies from npm . We will then be able to load libraries and application sources using RequireJS from app.ts file.

app.ts file excerpt

Remember to remove paths from scripts section of Angular CLI configuration file when you import them using RequireJS.

Step 3: Bootstrap as a hybrid application

AngularJS/Angular hybrid mode

Our goal in this step is to have the two frameworks (AngularJS and Angular) running together in hybrid mode. We will then be able to remove AngularJS gradually without disrupting the functionality of the application.

In order to bootstrap our application as a hybrid one, we first need to install the @angular/upgrade npm package. After the package has been installed, we create the main Angular application module AppModule and import UpgradeModule .

Import UpgradeModule

We remove the ng-app directive from index.html and implement the ngDoBootstrap method.

Implement ngDoBootstrap

Finally, we create the main.ts file which will be the entry point of the application and change the relevant main property in .angular-cli.json file. Please note that we use setAngularJSGlobal method because AngularJS is lazy loaded using RequireJS (it is not available on the window object).

Main entry point file

Step 4: Configuration and error handling

In this step we will use environment files for storing application settings and we will create a custom ErrorHandler for handling global errors that are not caught explicitly.

Environment files can be found in the environments folder of the project root and are created automatically from the Angular CLI, you can find more information here. In each of these files, we define the url and the developer key of the Marvel API as well as other configuration settings of the application such as the title and the current version.

environment.ts

We need to implement the handleError method of the ErrorHandler interface to create a custom error handler. This method will be responsible to catch the error and send it to the logger service for further processing.

app-error-handler.ts

The logger service, which is in AngularJS context, needs to be upgraded so that it can be used in the custom error handler. We could upgrade it straight away but then it would not be accessible from AngularJS components that already use it. For this purpose we will create an upgraded version of the logger service in a separate file.

It is a good practise to create such a file that will contain an upgraded version for each AngularJS service that you need to use in Angular.

ajs-upgraded-providers.ts

Note the use of Injector to get the upgraded logger service. This is needed because the ErrorHandler class is instantiated before bootstraping the AngularJS part and therefore the AngularJS $injector has not been created yet.

Step 5: Upgrade services

This step describes the process of upgrading services of the core module, such as the logger service that we described earlier, and 3rd party libraries such as the angular-resource that is used for HTTP data access.

Recall from the previous step that we maintain two versions of the logger service for each framework. We will now keep only the Angular one and import it into the respective CoreModule .

core.module.ts

We install @angular/http which is the new HTTP client for Angular. We import the HttpClientModule into the main application module and all services that use the HttpClient are converted to Angular and downgraded because there are still AngularJS components that use them.

characters.service.ts

If you are not upgrading all your data access services at once and you are using HTTP interceptors, you can keep both of them during the upgrade process.

Finally, we create Angular modules for each feature module in the same file with the AngularJS module definition, except of widgets and layout which we are going to upgrade in the next step.

characters.module.ts

Step 6: Upgrade components

This step describes the process of upgrading components of the application and 3rd party libraries such as AngularJS Material.

We install @angular/material npm package and create a separate module AppMaterialModule that imports and exports Angular Material components.

This is a good practise since feature modules that want to use Angular Material components, will need to import only this module.

We upgrade the widgets module and rename the respective folder to shared in order to conform with the Angular official style guide. We can also start upgrading AngularJS components of feature modules.

I generally prefer to upgrade children components first from the bottom up in order to eliminate dependencies as much as I can early in the process.

We refactor layout module to a single component, the AppComponent , since there is not so much code involved to keep it as a separate module.

If your layout components are large enough, you can move them to the core module since they will only be used once at the application startup.

We are currently using ui-router npm package to handle routing in the application. We would not like to bother upgrading it yet since it is a lot of work. So, we create an upgraded version of the $state service in the ajs-upgraded-providers.ts file in order to use it in the Angular context and create a custom Angular directive to mimic the behavior of the ui-sref directive.

You could just set the href property in the HTML element directly instead of creating the custom directive but this approach is more testable.

Step 7: Upgrade router

The upgrade of the routing mechanism is a complicated process. The ideal scenario is to have the ui-router (or whatever library you use for routing) and the Angular router running together during the process.

I suggest to leave routing upgrade at the end when all components and services have been upgraded

There are articles on the web and guides on the documentation of routing libraries that describe how to achieve this, so I will not go into so much detail. Mainly, it depends on how large the codebase is and whether you decide to keep your application deployed during the upgrade process or not. In this scenario, since the codebase is small we upgraded it all at once.

We install the @angular/router npm package and refactor all components to use the routerLink directive instead of the custom one that we created in the previous step. Finally, we upgrade the AppComponent and bootstrap it in Angular.

Goodbye AngularJS

Goodbye and thank you AngularJS

We are finally ready to remove AngularJS completely. It has served us good until now but it is time to move on with the shiny new Angular.

We can now remove the following from our project safely:

Gulp

ajs-upgraded-providers.ts file

file UpgradeModule

Implementation of ngDoBootstrap

Furthermore, there is no need of downgrading components/services and we do not need to import AngularJS globally in the main.ts file.

That’s it! We are done! Our application is now running in pure Angular.

I hope this article will be useful for you and your team as it helped me. What is your prefered method to upgrade in Angular? Let me know in the comments below!