Steps for upgrading AngularJS to hybrid Angular 9
Tags: angular, webdev, javascript
Published: 2020-06-18 08:02:15

Update:  What happened a year after, [read this](https://dev.to/psamim/micro-frontends-after-one-year-with-single-spa-1eoo)

AngularJS end of life is near. Its LTS version will end on June 2021. So many projects using AngularJS have started considering the options. One option is to migrate to ReactJS, but some choose to ``upgrade and continue using Angular.

Recently I had to upgrade a 5-year-old codebase from AngularJS 1.4 to Angular 9. It took some effort as I had forgotten the tools which were popular in the olden times! The project used Gulp, Sass, and server-side Jade (Pug). 

These are the steps to upgrade.

### Install Angular
The first step was to install Angular CLI and bootstrap a new project right next to the old AngularJS app files inside the same repository.

### Remove Gulp
Gulp was used to compile Sass files and concatenate JS files. Angular 9 uses Webpack to do this and it is configured by default. I removed gulp and its related files.

Sass files had a single entry. I added the entry file to the Angular's `src/styles.scss` so that all existing styles are compiled now.

```scss
@import "app/main";
...
```

JS concatenation is not needed anymore because AngularJS is bootstrapped inside Angular and all dependent files are linked and bundled using the `import` syntax. I will discuss how to bootstrap AngularJS inside Angular in the next step.

If any files outside Angular needs concatenation or Sass compilation, Angular's Webpack can be customized and extended using [custom Webpack configurations](https://github.com/just-jeb/angular-builders/tree/master/packages/custom-webpack).

### Bootstrap AngularJS
Following [documentations here](https://angular.io/guide/upgrade#how-ngupgrade-works) I used `ngUpgrade` to bootstrap the AngularJS module. 

In the `app.module.ts` file:
```ts
import { UpgradeModule } from "@angular/upgrade/static";
import { angularJsModule } from "../ajs/app";

...

export class AppModule {
  constructor(private upgrade: UpgradeModule) {}

  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, [angularJsModule.name], {
      strictDi: true
    });
  }
}
```

You can see that I have imported `angularJsModule`. For it to work I had to refactor the AngularJS app a little bit. I created a new file to define the AngularJS module as `angularJsModule` and export it to use it in the code above for bootstrapping. Also in the process of refactoring, I decided to put all AngularJS code into a separate directory called `ajs` inside `src`.

```ts
import * as angular from "angular";
import ngStorage from "ngstorage";
import ngSanitize from "angular-sanitize";
import ngMaterial from "angular-material";

export const angularJsModule = angular
  .module("MyApp", [
    ngStorage.name,
    ngSanitize,
    ngMaterial,
  ])
  .service("toaster", ["$mdToast", ToasterService])
 ...
```

It is better to define all services or directives you have in this file. If you have other JS files where you have defined services or directives you have to import them too so that they end up in the final bundle.

```ts
import "./components/date-tools.svc.js";
import "./components/toaster.svc.ts";
import "./payment/currency-tools.svc.js";
...
```

It is better to import all needed dependencies in the same file where they are going to be used and avoid globally scoped variables like `moment` or `lodash`. If you have correctly imported dependencies they all will be in the final bundle. But sometimes refactoring the code and adding `import lodash from 'lodash'` to many files takes time so I added them to global scope temporarily to mitigate the issue for now.

```ts
import * as lodash from "lodash";
import * as moment from "moment";

(window as any).moment = moment;
(window as any)._ = lodash;
```

There were remaining scripts that did not work with the above method and which I needed to have in the global scope or need something like HTML script tag so I added them to `angular.json` file's `scripts`.

```json
...
      "architect": {
        "build": {
         ...
            "scripts": [
              "node_modules/dropzone/dist/min/dropzone.min.js",
              "node_modules/d3/d3.min.js"
            ]
          },
...
```

Previously the project did not use npm to manage the dependencies. It used gulp to concatenate and bundle JS files and they were not pulled from `node_modules`. The library files were just copied into the project. In the process of removing gulp I also removed all library files from the project and let Webpack and npm manage those.

At this point, the app should have worked.

But I had one specific problem here. Many templates were compiled on the server-side by Jade (Pug) template engine using `express`'s view engine feature. The main problem was the `index.html` file. Angular and in particular Webpack needs to create `index.html` to inject scripts and styles. I had to manually edit and convert this file from Jade to pure client-side HTML. Some variables were being injected from the server-side using Jade templates. I had to find other ways to access those variables. I ended up using Handlebars template engine instead of Jade because Handlebars is valid HTML and can be given to Angular. So I created `index.html` having Handlebar variables. Then Angular injects scripts and styles into it on build time and this file is finally served by `express` injecting server-side variables through Handlebars template variables. Ideally, I would like to not use Handlebars and passing variables this way seems not clean to me.

Here I configured Angular to use my Handlebars template file:

```json
...
      "architect": {
        "build": {
         ...
            "options": {
               "index": "client/main/src/ajs/index.handlebars",
            }
          },
...
```

### Migrate Routes
The old AngularJS app used `ui-router` for routing. The good news was that `ui-router` can share routes between AgnularJS and Angular using [angular-hybrid](https://github.com/ui-router/angular-hybrid).

In the `app.module.ts`:
```ts
import { UIRouterUpgradeModule } from "@uirouter/angular-hybrid";
import { routerStates } from "./router-states";

...

@NgModule({
  imports: [
    UIRouterUpgradeModule.forRoot(routerStates),
  ],
 ...
})
export class AppModule {}
```

And in the `router-states.ts` file one route is using AngularJS and another route is using Angular:
```ts
import { VisaComponent } from "./trip-details/visa/visa.component";

...

export const routerStates = {
  states: [
    {
      name: "documents",
      url: "/documents",
      templateUrl: "/views/documents",
      controller: "DocumentsController",
      redirectTo: "documents.manage"
    },
    {
      name: "trip-details.visa",
      url: "/visa",
      component: VisaComponent,
    }
...
```

### Change Deployment Scripts
Finally, I had to change deployment scripts used by npm and CI/CD pipelines to use `ng build` command.


### Development Server
There was still one specific problem to this project setup. In this project `express` server was set up in the same repository for both serving the front-end (and its server-side Jade templates) and also serving the back-end API endpoints. Angular has its own server used in development mode with hot-reload and specific configurations. For development, I wanted to have both the old `express` server (for APIs and legacy Jade templates) and the new Angular development server running at the same time.

So I used [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware). `express` can serve the APIs as before and also proxy requests to Angular development server running on port 4200 only in development mode.

in `express-app.ts`:
```ts
import { createProxyMiddleware } from "http-proxy-middleware";

...

  if (process.env.NODE_ENV === "development") {
    app.use(
      ["/client", "/sockjs-node", "/assets"],
      createProxyMiddleware({
        target: "http://localhost:4200",
        ws: true,
        pathRewrite: {
          "^/assets": "/client/assets"
        }
      })
    );
  } else {
    app.use("/client", express.static(path.join(__dirname, "..", "public")));
    app.use(
      "/assets",
      express.static(path.join(__dirname, "..", "public", "assets"))
    );
  }

```

The ideal setup would be to separate concerns and put the back-end code into its own separate repository but that is a separate issue.

So, in the end, the development setup is something like this:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jpdi4fofp3mabdblzy97.png)


In the production setup, there is no Angular dev server.


### Sharing Services and Components
To use a service or component from Angular in AngularJS we need to downgrade those services and components. In the same file that I defined the `angularJsModule`:
```ts
import {
  downgradeComponent,
  downgradeInjectable
} from "@angular/upgrade/static";

...

  .module("GlobalWorldApp", [
   ...
   ])
  .directive(
    "nextPaymentPlans",
    downgradeComponent({ component: PlansComponent })
  )
  .factory("nextPayment", downgradeInjectable(PaymentService) as any);
```
So `nextPayment` is accessible as an AngularJS service and `<next-payment-plans>` can be used in AngularJS templates. So now we can start moving components and services from AngularJS to Angular gradually.

To access the AngularJS's `rootScope` from Angular we can inject it as a service, but better to avoid it if possible.
```ts
import { IRootScopeService } from "angular";
...
  constructor(
    @Inject("$rootScope") private _rootScope: IRootScopeService) {}
...
```
Good to mention that to properly use the `rootScope` we need to `watch` it.

### Linter
Setting up the linter is a separate issue and process from upgrading AngularJS. But I just want to mention here that I decided to remove `tslint` and replace it with `eslint`. Mainly because `tslint` [is deprecated](https://github.com/palantir/tslint/issues/4534) and also because of the smoother integration of `eslint` with `prettier` and other tools. I also love and use `prettier` everywhere I can.

### Upgrade
After all these steps we can just start upgrading the app! I mean now we can let AngularJS and Angular components co-exist. So we can rewrite and move components from AngularJS to Angular gradually. The full upgrade takes months or even may never come. I think we will develop new parts of the app in Angular and only rewrite the AngularJS parts which are buggy or need refactoring.

I hope this helps others and future me trying to upgrade AngularJS apps.