Steps for upgrading AngularJS to hybrid Angular 9
Samim Pezeshki
Posted on June 18, 2020
Update: What happened a year after, read this
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.
Bootstrap AngularJS
Following documentations here 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.
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. 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:
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);
nextPayment
Sois accessible as an AngularJS service and
` 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 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.
Posted on June 18, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 31, 2024