Migrating from AngularJS to Angular using Angular Elements
Hi, my name is Aditya Baddur and I joined Fyle in June 2021 as a senior front-end developer. My job was to migrate the company's codebase from AngularJS to Angular.
Migrating from AngularJS to Angular can be a daunting task, as the two frameworks are quite different. However, with careful planning and a step-by-step approach, it is possible to migrate your AngularJS application to Angular successfully.
In this post, I will share my experiences and lessons learned from migrating the chrome extension by rewriting everything from the scratch and migrating one of the sub-apps of our web app from AngularJS v1.8.2 to Angular v14 using Angular Elements while releasing new features in parallel.
How did the migration journey begin?
Chrome Extension Migration
When I first joined Fyle, I was tasked with migrating the company's chrome extension from AngularJS to Angular. Since the extension only had four pages, I initially believed that it would be best to rewrite the entire app from scratch. However, this approach ended up taking a few months to complete, with 70% of the time spent on development and 30% spent on testing and fixing bugs. It was then that I realized that rewriting from scratch was not a viable option for migrating our web app, which consisted of 6 sub-apps.
Web app migration
I explored two potential migration approaches: ngUpgrade and Angular elements. After conducting some experimentation, I found that Angular Elements were the best fit for our needs because ngUpgrade
requires both AngularJS and Angular to run at the same time. This will have a performance issue because both AngularJS and Angular run the change detection cycle.
Angular elements allow us to use Angular components as custom elements in an AngularJS application. This allowed us to gradually migrate individual components from AngularJS to Angular without having to rewrite the entire app from scratch. It also allowed us to continue releasing new features during the migration process, rather than having to halt development for an extended period of time.
Before I began the migration, I finalized some key decisions. I chose
PrimeNG v14 as our UI component library
TailwindCSS v3 as our CSS framework
Selected AngularJS equivalent libraries in Angular
I decided to create an NX workspace in the existing repository because it allowed me to easily manage the migration of the various sub-apps of our web app from AngularJS to Angular. Creating an NX workspace also allowed me to take advantage of the latest features and functionality available in Angular, which made the migration process smoother and more efficient.
This is what our repository folder structure looked like before introducing NX.
angularjs_app1
components
controllers
services
angularjs_app2
components
controllers
services
angularjs_app3
components
controllers
services
package.json
package-lock.json
gulpfile.js
This is what our repository folder structure looked like after introducing NX.
app-v2
apps
angular_app1
src
app
assets
package.json
package-lock.json
nx.json
angularjs_app1
components
controllers
services
angularjs_app2
components
controllers
services
angularjs_app3
components
controllers
services
package.json
package-lock.json
gulpfile.js
Before creating the NX workspace, I had to upgrade the existing repository's Gulp from version 3 to version 4 and node from version 10 to version 16 due to compatibility issues. Additionally, I added support for ES6 to the repository. Upgrading the gulp and node versions and adding support for ES6 were important steps in preparing the repository for the migration to Angular. I will delve further into the details in a future blog post.
I further divided the migration tasks into smaller sub-tasks.
Services Migration → There were 19 services in the AngularJS app.
Components Migration → There were 25 components in total out of which 11 components were child components and the remaining 14 components were full-page components.
The following link leads to a GitHub repository containing a proof-of-concept (POC) demonstration of using Angular Elements https://github.com/rvab/poc-angular-elements
Services Migration
Initially, the plan was to migrate all the AngularJS components to Angular Elements in the first iteration and all the AngularJS services to Angular in the second iteration. However, after conducting a proof of concept, I realized that injecting AngularJS services inside Angular Elements did not work properly as the change detection was not working. This was because the API call was made outside of the Angular zone, which caused problems with updating the user interface (UI).
If there were any UI changes that needed to be made after the API call, such as displaying an error message on a failed login attempt, these changes would not be reflected in the UI because the API call was being made within the AngularJS zone rather than the Angular zone. Therefore, I decided to migrate all the AngularJS services to Angular in the first iteration to keep a copy of the services in both AngularJS and Angular.
Components Migration
Before you start the migration process from AngularJS to Angular, it is important to list down all the components you will be working with. This will help you to plan your migration and ensure that it is as smooth and efficient as possible.
One approach is to divide your AngularJS components into two categories: child/reusable components and full-page components.
Child/reusable components are smaller, self-contained pieces of functionality that can be used multiple times within an application.
Full-page components, on the other hand, are larger components that are typically used to represent an entire page or section of an application.
In the first iteration, I migrated all the child/reusable components to Angular Elements. This allowed me to gradually introduce Angular into the AngularJs application and push new Angular components to production without disrupting the user experience.
In the second iteration, I focused on migrating full-page components to Angular. This required more work and coordination, but it ultimately allowed me to migrate the entire AngularJS application to Angular.
Child component migration
I started with a simple header
component from the AngularJS app and converted it to an Angular element using createCustomElement the function provided by @angular/elements.
The below code is the header component in the AngularJS app.
ajs-header.component.html
<header class="new-header" layout="row" layout-align="center center">
<div>
<a ng-href="#">
<img src="./assets/images/icons/fyle_logo_new.png" aria-label="Fyle Logo" alt="Fyle" class="brand-logo">
</a>
</div>
</header>
ajs-header.component.js
;(function () {
'use strict';
angular
.module('Components')
.component('ajsHeader', header());
function header() {
var component = {
templateUrl: '/ajs-header/ajs-header.template.html',
controller: HeaderCtrl,
controllerAs: 'vm'
};
return component;
}
/* @ngInject */
function HeaderCtrl () {
var vm = this;
vm.$onInit = function () {
};
}
})();
ajs-login.component.html
<div layout="column" class="container-auth-new">
<div layout="column" layout-align="center center" flex="noshrink">
<ajs-header></ajs-header>
</div>
</div>
The below code is the header component in the Angular app.
header.component.html
<header class="tw-flex tw-justify-center tw-items-center tw-mb-40-px tw-w-full">
<a href="#">
<img src="./assets/images/icons/fyle-logo-new.png" aria-label="Fyle Logo" alt="Fyle" class="brand-logo tw-h-30-px">
</a>
</header>
header.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
app.module.ts
import { createCustomElement } from '@angular/elements';
export class AppModule implements DoBootstrap {
constructor(private injector: Injector) {}
private createCustomElement(component: Type<any>, selectorName: string) {
const customElement = createCustomElement(component, {
injector: this.injector,
});
customElements.define(selectorName, customElement);
}
ngDoBootstrap() {
this.createCustomElement(HeaderComponent, 'app-header-ce');
}
}
How to embed angular elements in the AngularJS app?
To embed the Angular elements in the AngularJS app, you need to import the generated Angular bundled files in the AngularJS app's index.html file. This can be done by adding the following code to the index.html file.
<html>
<head>
<title>Angular Element POC</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="./ng.min.css">
</head>
<body ng-app="app">
<script src="<https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/1.0.30/angular-ui-router.min.js>" integrity="sha512-HdDqpFK+5KwK5gZTuViiNt6aw/dBc3d0pUArax73z0fYN8UXiSozGNTo3MFx4pwbBPldf5gaMUq/EqposBQyWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div ui-view class="container"></div>
<script src="./ng.min.js"></script>
<script src="./module.js"></script>
<script src="./services/users.js"></script>
</body>
</html>
To generate the ng.min.js and ng.min.css files, you can write a custom JS script to merge all the individual JS and CSS files into a single file. The gzipped size of these files was ~325KB and ~24KB respectively. This can be done using the fs
module in Node.js, as shown in the following example:
function concatBuildFiles(distPath, writePath) {
const jsFiles = fs.readdirSync(distPath).filter(file => file.endsWith('.js'));
const cssFiles = fs.readdirSync(distPath).filter(file => file.endsWith('.css'));
let jsFileData = '';
let cssFileData = '';
for (const file of jsFiles) {
jsFileData += fs.readFileSync(`${distPath}/${file}`);
}
for (const file of cssFiles) {
cssFileData += fs.readFileSync(`${distPath}/${file}`);
}
fs.writeFileSync(`${writePath}/ng.min.js`, jsFileData);
fs.writeFileSync(`${writePath}/ng.min.css`, cssFileData);
}
folder structure
public
angularJSapp
index.html
ng.min.js
ng.min.css
Full-page component migration
How to access AngularJS services such as resolvers, ui-router, window, etc in Angular Elements?
When migrating from AngularJS to Angular using Angular Elements, I encountered a few AngularJS components that were dependent on services such as AngularJS resolvers, $state
, $stateParams
, and $window
. I will explain how to use these services in Angular elements with an example.
Passing resolvers as input to Angular element from the AngularJS component
login.component.ts
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
})
export class LoginComponent implements OnInit {
@Input() username: string
constructor() {}
ngOnInit() {}
}
ajs-login.component.html
<app-login-ce class="app-login-ce">
</app-login-ce>
ajs-login.controller.js
const appLoginCe = $document[0].getElementsByClassName('app-login-ce')[0];
appLoginCe.username = 'John Doe';
If you have an Angular element that depends on input variables in the
ngOnInit
method, the above code may not work as expected. This is because Angular elements are created asynchronously and thengOnInit
method may be called before the input variables are set. In order to access the input variables inside thengOnInit
method, you can do the following:
login.component.ts
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
})
export class LoginComponent implements OnInit {
@Input() username: string
firstName: string
constructor() {}
ngOnInit() {
this.firstName = this.username.split(' ');
}
}
ajs-login.component.html
<div class="app-login-ce">
</div>
ajs-login.controller.js
const appLoginCeParent = $document[0].getElementsByClassName('app-login-ce')[0];
const appLoginCe = $document[0].createElement('app-login-ce');
appLoginCe.username = 'John Doe';
appLoginCeParent.appendChild(appLoginCe);
Using
$state
,$stateParams
, and$window
.To take the user to a different AngularJS page from the Angular element login page, you can use the AngularJS $state service. In the Angular element component, I was able to access
$state
by injecting it into the constructor like this: The below code explains what the code looks like in the AngularJS app.
ajs-login.controller.js
;(function () {
'use strict';
angular
.module('Controllers')
.controller('LoginCtrl', LoginCtrl);
function LoginCtrl($state, $stateParams, usernameResolver) {
const vm = this;
vm.onLoginSuccess = () => {
$state.go('ajs-dashboard-page', { userid: 1234 });
}
init = () => {
vm.firstName = usernameResolver.split(' ');
// accessing email from the queryparams.
vm.email = $stateParams.email;
}
init();
}
})();
The below code explains what the code looks like in the Angular app.
app.module.ts
function getAngularJSService(angularJSServiceName: string) {
const $injector = (window as any).angular?.element(document.body).injector();
return $injector.has(angularJSServiceName) ? $injector.get(angularJSServiceName) : null;
}
@NgModule({
providers: [
{
provide: '$window',
useFactory: () => getAngularJSService('$window')
},
{
provide: '$state',
useFactory: () => getAngularJSService('$state')
},
{
provide: '$stateParams',
useFactory: () => getAngularJSService('$stateParams')
}
]
})
login.component.ts
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
})
export class LoginComponent implements OnInit {
@Input() usernameResolver: string
firstName: string
email: string
constructor(
@Inject('$state') private $state: any,
@Inject('$stateParams') private $stateParams: any
) {}
ngOnInit() {
this.firstName = this.usernameResolver.split(' ');
// accessing email from the queryparams.
this.email = this.$stateParams.email;
}
onLoginSuccess() {
this.$state.go('ajs-dashboard-page', { userid: 1234 });
}
}
Through this process, I was able to migrate our web app from AngularJS to Angular using Angular Elements. This allowed us to migrate the app incrementally, without having to rewrite the entire codebase from scratch.
Clean up
Once the whole app was migrated, remove all the references of the AngularJS app that were used in the Angular component.
$state.go()
→router.navigate()
$stateParams
→activatedRoute.snapshot.queryParams
$window
→window
.For example, the previous Angular login component will look like the following when replaced with AngularJS references.
login.component.ts
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
})
export class LoginComponent implements OnInit {
usernameResolver: string
firstName: string
email: string
constructor(
private router: Router,
private activatedRoute: ActivatedRoute
) {}
ngOnInit() {
this.usernameResolver = this.activatedRoute.snapshot.data
this.firstName = this.usernameResolver.split(' ');
this.email = this.activatedRoute.snapshot.queryParams.email;
}
onLoginSuccess() {
this.router.navigate(['dashboard-page'], { queryParams: { userid: 1234 }});
}
}
Conclusion
I wasn’t sure when I started the process of converting my AngularJS application to Angular using Angular Elements as the migration method. This was because I was unsure of the potential issues I might encounter, how to resolve them, and what to do if I encountered any roadblocks. There were also few resources available on using Angular elements for this type of migration.
During the migration process, I encountered two major issues. The first was that injecting AngularJS services into Angular components did not trigger UI changes within the components. The second issue was that accessing input variables within the ngOnInit
method did not work as expected, as Angular elements are created asynchronously and the ngOnInit
method is called before the input variables were set. I had to resort to a different approach to solve these issues.
In conclusion, the process of migrating our AngularJS app to Angular using Angular Elements was successful. By leveraging Angular Elements, I was able to reuse our existing AngularJS code and integrate it into our new Angular app, saving time and resources. The use of Angular Elements also allowed us to take advantage of the latest features and capabilities of Angular, resulting in a more modern and efficient application. Along with it, I was able to upgrade the existing repository's Gulp from version 3 to version 4 and node from version 10 to version 16, as well as add support for ES6, in order to ensure compatibility with the tools and technologies that we would be using in our Angular project.
Overall, the migration to Angular using Angular Elements was a smooth and successful process. I was able to migrate the whole app along with releasing new features parallel.
I have created a demonstration of the migration process, including each step, in repository commits that you can clone and experiment with. Feel free to explore the repository and try out the migration process for yourself. You can ping me on LinkedIn in case you’ve any questions.