AngularJS to Angular5 — Upgrading a large application

There are two likely cases for an application to be under AngularJS in 2018. Either the application is rarely used and will be abandoned one day in which case no migration is needed. Or, the application is wide, with a major impact on the business and undergoes a lot of interventions on a daily basis and upgrading to another framework is obviously not the priority.

The latter is what we had at Contentsquare.

We found out many good reasons to migrate on Angular besides the simple great pleasure to experiment, learn and create with a tool of his time: security, keeping dependencies up to date, HR (for finding Angular 5 developers with 5+ years of experience) and so on.

But how to migrate knowing that the product needs to evolve ? No question of making freeze code for 6 months to re-write everything, so we need a happy medium. The hybrid solution that consists of making the two frameworks live together the transition comes naturally.

I will present the approach we adopted at Contentsquare to make possible this upgrade between the two very different versions/frameworks. However, don’t take this as a strict guideline as your project might be different.

Going to a hybrid state : upgrading without forking

In this post, I will show you why we chose to upgrade incrementally by having a transitional hybrid app with both AngularJS and Angular 5. I will review a basic hybrid configuration.

Pros and cons of going hybrid

Here are the pros and cons that I identified:

Pros:

  • Official supported way — Thanks to ngUpgrade, it’s not too hacky
  • Promote a “learn and create” spirit — It will motivate your team to learn a complete new way of working
  • Incremental upgrade — the upgrade is incremental, it does not overload the roadmap
  • Many technical advantages and features to expect from Angular 5 — faster change detection, template precompilation and reuse, lower memory usage, observables and so on
  • No legacy code on purpose — Feature additions are no longer made with legacy code

Cons:

  • Change of environment — Much of the development environment and tool builds must be reviewed
  • Adaptation — Above all, the team must be willing to learn a new language, to change its habits. This point is important and the willingness to migrate must be general.
  • Heavier app — The code becomes necessarily heavier because 2 different frameworks are loaded.

Going hybrid

It is not easy to understand at first glance how the cohabitation between Angular 1 and Angular 5 occurs.

Let’s start by reassuring you, there is almost no line of code to change in Angular 1. Angular’s official documentation tells you that you should prepare and align AngularJS applications with Angular in order to be compatible (like choosing component over directive for instance). But this compatibility is only necessary if you plan to use Angular dependencies from AngularJS components or services.

Our goal is to minimize technical debt, so we will prefer to fully rewrite a component or a service even if we do the job twice. The real benefit of going hybrid is to be able to rewrite your code as you go, incrementally.

It will therefore be necessary to find the first component to re-write in your code. It must be sufficiently isolated and have his own route. It must also be fairly simple if you are novice with Angular.

Setup your environment

Our first aim is to have the possibility to execute AngularJS from Angular. This can be done starting today, no matter the structure of your AngularJS code. It requires to tied up together some toolings :

  • npmTo declare and install your dev tools and front-end dependencies.
  • TypeScriptAngular is written in TypeScript, this post explain why.
  • SystemJS (or Webpack) — Angular is split in many dependencies (Zone.js, Rxjs, etc). To load them, you will need a module loader and bundler.

npm: load your app dependencies

Upgrading to Angular is a good opportunity to use npm for your front-end dependencies.

If you are using bower, it’s time to let it go (as it is deprecated) and use npm instead. My best advice is to load directly these dependencies inside your node_modules folder in your dev environment.

If you do so, don’t place node_modules in a public folder but create a symlink instead with grunt or gulp (or any javascript task runner you may have) to avoid having this folder served in the production environment.

We will need to use SystemJS to “require” our front-end dependencies straight from your node_modules folder. But before, we need to configure typescript.

TypeScript: compile your app

TypeScript is straightforward to install (npm install typescript -g).

The architecture of a project is a main entry point that is a root.js file which imports all other files. You can have several entries but we won’t cover that specific case here.

Create a file named tsconfig.json and place it your main JS directory.

Here is our configuration :

Beware that some of those parameters are opinionated and should be adapted to your needs.

To transpile every files recursively in plain javascript :

$ tsc --project main_js_folder

You can add “-w” to this command while you are in development. Also, you can integrate this command in your tasks runner to take advantage of the incremental compilation.

SystemJS: build your app

SystemJS lets you use require() in the browser to resolve your dependencies. It needs to be a bit configured.

You can resolve your dependencies while you are refreshing a page in development mode.

In a production environment, dependencies are resolved with SystemJS Builder that will produce a bundle file that will contain all of them.

Those two behaviors use the same systemjs.config.js configuration that you can adapt for your case:

(function (global) {
 const pathPrefix = (typeof window !== 'undefined' ? '/' : './');
 System.config({
   baseURL: '/dist',
   defaultJSExtensions: true,
   paths: {
     // paths serve as alias
     'npm:*': pathPrefix + 'node_modules/*'
   },
   // map tells the System loader where to look for things
   map: {
     // our app is within the app folder
     app: 'app',
     // angular bundles
     '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
     '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
     '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
     '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
     '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
     '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
     '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
     '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
     '@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
     // other libraries
     rxjs: 'npm:rxjs',
     'zone.js': 'npm:zone.js/dist/zone.js',
     'reflect-metadata': 'npm:reflect-metadata/Reflect.js',
     'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
   },
   // packages tells the System loader how to load when no filename and/or no extension
   packages: {
     app: {
       defaultExtension: 'js',
       main: './main./js'
     },
     rxjs: {
       defaultExtension: 'js'
     }
   }
 });
})(this);

Now, you have everything you need to run Angular.

Run Angular then bootstrap AngularJS

What is making our current Angular 1 bootstrap’s is the ngApp directive in our body or div element of our main template. We can remove by replacing this :

<div ng-app="MyApp"></div>

by :

<div id="ng-app"></div>

But then, you get a blank screen.

We will bootstrap our AngularJs app manually in Angular 5. To do this, we will create a file named app.module.ts that will contain this code :

From the Angular 5 world, we trigger the AngularJS bootstrap.

Then we need an entry point to bootstrap the Angular 5 app, let’s call it main.ts :

The app should run again.

Elaborating an upgrade strategy with ngUpgrade

At this stage, we can think of a migration strategy to ensure that one day, you won’t have any line of code in AngularJS in your app. After all, this is our purpose. Having an hybrid app is not especially a cool thing but it allows us to manage a smooth transition.

What does ngUpgrade offer?

With ngUpgrade we can upgrade an AngularJS dependency or downgrade an Angular 5 dependency.

But tying down together components looks quite not convenient, even if this is done temporarily. This would result to writing legacy code in live as we know we want to migrate the very same code later. This is why we have decided to not upgrade nor downgrade any components, except some AngularJS services that we needed in Angular 5.

For most of our AngularJS components, it was more difficult to upgrade it than to rewrite it from scratch. We prefer to use this time to better learn and practice with Angular 5.

UI-router to the rescue

Ui-router can do switch logic between Angular 1 and 5 thanks to its module angular-hybrid.

Angular-hybrid enables UI-Router to route to both AngularJS components (and/or templates) and Angular components.

In AngularJS, you just have to add one module in your dependencies, nothing else.

Let’s say that we want to add a route to our application to instantiate an Angular 5 component, we would add the state from the Angular 5 code like this:

import { NgAboutComponentClass } from "./mycomp.ng.component";

.state({
 name: 'mynewroute',
 url: '/mynewroute',
 component: NgAboutComponentClass
});

Then, angular-hybrid will add a new route to our existing AngularJS routes.

Splitting concerns per routes seems the most logical approach to migrate progressively our app by decreasing the amout of lines written in AngularJS in our code while increasing the other part written in Angular 5.

The team of Ui-router has created a sample app here, this will give you a concrete example : https://github.com/ui-router/sample-app-angular-hybrid

Note that your AngularJS app don’t need to be written in typescript.

Conclusion

This approach is not displayed nor advised in the official Angular documentation and yet it is this one that seemed for us the most relevant rather than upgrading or downgrading components.

Today, the vast majority of our front codebase is still written in AngularJS. Our mid-term strategy is to allocate a part of our sprints to rewrite old components (between 20% and 30%). And we decided to limit as much as possible adding new features in AngularJS. It’s sometimes a challenge but for a good cause.

Another strategy would be to dedicate some team members on this rewriting. But the task is monotonous, we prefer to share it.

As the time of writing, we have switched to Webpack and have explicitly declared all dependencies in our AngularJs components, we don’t concatenate js files anymore. This has highlighted many bad dependencies, poorly isolated components and therefore offers us the opportunity to re-write properly the code in Angular.

We share this work equitably in the team and what motivates us the most is our common desire to finish this long line work.