Upgrade from AngularJS to Angular 2
Requirements
- Node.js installed on your local development machine
- IDE: I prefer WebStorm from JetBrains, but it is not a must
- The current Angular 2 version: 2.4.4
- Angular-cli version: 1.0.0-beta.26
- Download our helper module Ng1UpgradeModule from here:
About this tutorial
After we decided to upgrade our app from AngularJS to Angular 2 the whole process was not easy. There are tons of tutorials but most of them become obsolete in a short time. The code snippets I found were not working. But finally we could migrate our application to Angular 2 successfully.
The aim of this tutorial is to simplify the migration process for developers who want to upgrade their AngularJS applications to Angular 2 as well.
About our hybrid mobile app
We’ve developed in the last 2 years a large and complex hybrid mobile app in the medical area based on frameworks such as AngularJS, Cordova and some other frameworks. Our company wants to stay up to date and always tries to use the latest modern frameworks, so we decided to make an upgrade to Angular 2.
Why you should upgrade?
AngularJS is dead and reached its end of life which means there will be no more releases and support in the future. Angular 2 comes with a lot of cool features. I summarised the most important and relevant features on the following list:
- Speed & Performance (supposedly 100 x faster than AngularJS)
- Supports code generation with angular-cli, web workers and virtual DOM
- UI framework Angular Material 2 (Material Design components built on top of Angular 2)
- Mobile optimized (hybrid apps should run faster and smoother)
- Develop object oriented and type safe using the TypeScript language, which introduces the following improvements: Class-based Object Oriented Programming, Static Typing, Generics, Lambdas etc...
- Supports TDD (Test Driven Development) and BDD (Behaviour Driven Development)
- Frequency of public updates, fixes and releases: Angular 2 is updated at least once in a month. And the next major releases of Angular 4, 5 or 6 are not far far away. So you should do an upgrade as soon as possible.
- Lazy loading feature, what is that?
Let’s assume we have two pages in our application, “home” and “admin”. Some people might never reach the admin page, so it makes sense to only serve the load of the admin page to people that actually need it or that have access to it. When you don’t have access to the admin page, this part of the application will never be downloaded to the client. This is where we will use lazy loading feature
Angular 2 is built by the same people who built AngularJS. They are not gonna repeat their mistakes again. At least I hope so :)
Decide for an upgrade strategy
I think there are 2 strategies which can lead you to a successful upgrade.
The first one is the “big bang” strategy. You will upgrade all parts of your application to Angular 2 and once you’re finished you can publish your app with a major release. This approach can take a lot of time without being able free to release your application. I recommend to do the whole upgrade definitely in a separate branch.
The second one is the “baby steps” strategy. I recommend this strategy especially for large and complex applications like our one. You’re gonna migrate your AngularJS application step by step. The idea is that you can commit your changes after each step without breaking the build.
In this tutorial we’re gonna use the baby steps strategy.
Preparation: AngularJS 1.5 or higher
First we need to upgrade our application to AngularJS 1.5 or higher. This is a must criteria before we can upgrade to Angular 2. The reason is there are a lot of concepts in AngularJS 1.5 or higher such as Components which are needed for the migration.
Note that there are some info about the 1.5 release, take a look on the following link. I recommend to upgrade first to 1.5.10 when everything is fine then update to the latest AngularJS version 1.6.1
Preparation: Layer application by feature/component
We still see applications using a project structure where all directives go into app/directives, services go into app/services, controllers into app/controllers and so on and so forth. While this totally works in smaller applications, it doesn’t really take us far when it comes to upgrading. We might want to upgrade component by component. Having an application layered by type rather than by feature/component, makes it harder to extract parts of the code base to upgrade it. So I changed the application layer to the following structure:
Upgrade baby step 1: Install and run Angular-CLI
I tried to bootstrap the application with a lot of different JavaScript loaders such as SystemJS or Webpack but the best and easiest way was using the angular-cli. For more information visit the official angular-cli page:
- First install globally the angular-cli using the following command:
npm install -g angular-cli
2. Go to the project root folder, run the following command and follow shown instructions.
ng init
Upgrade baby step 2: Use .service() instead of .factory()
Replace all your AngularJS factories with services. Factories are not supported in Angular 2.
Upgrade baby step 3: bootstrap the AngularJS application in an Angular 2 environment
Modify your generated angular-cli.json
- Include all your AngularJS codes in the scripts[] section
- Include all your AngularJS css files in the styles[] section
- Add all icons and images into the assets folder and define them in the assets[] section
Import and include our “Ng1UpgradeModule”.
I implemented an upgrade module to simplify the whole migration process and AngularJS dependencies, which we need during our upgrade. This module can save you a lot of time.
Modify app.module.ts
Modify your generated app.module.ts file and add the necessary imports, providers and declarations as shown below. Do not forget to export the UpgradeAdapter as a constant. We will use the UpgradeAdapter to bootstrap our Angular 2 application.
/**
* src/app/app.module.ts
*/
import { NgModule, forwardRef, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { DatePipe } from '@angular/common';
import { UrlHandlingStrategy } from '@angular/router';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
import { UpgradeAdapter, UpgradeAdapterRef } from '@angular/upgrade';
import { AppCoreModule } from "./core/app-core.module";
import { AppComponent } from "./app.component";
import { APP_ROUTING } from "./app.routing";
import { Ng1UpgradeModule } from "./shared/upgrade/ng1-upgrade-shared.module";
import { Ng1Ng2UrlHandlingStrategy } from "./shared/upgrade/ng1-ng2-url-handling-strategy";
export const UPGRADE_ADAPTER = new UpgradeAdapter(forwardRef(() => AppModule));
/**
* Application base root module
*/
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
Ng1UpgradeModule.forRoot(),
AppCoreModule,
APP_ROUTING,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
exports: [],
providers: [
DatePipe,
{
provide: UrlHandlingStrategy,
useClass: Ng1Ng2UrlHandlingStrategy
}
]
})
export class AppModule {
ngDoBootstrap() {}
}
Modify your AngularJS index.html
It is now time to remove the ng-app tag from your AngularJS index.html file which is not necessary for bootstrapping an Angular 2 app. We will point to our AngularJS module name in the static main.ts file.
Modify main.ts
Now we can let Angular 2 to bootstrap our AngularJS application in its environment. Do not forget to replace the placeholder ‘YOUR_NG1_APP_MODULE_NAME’ with your AngularJS ng-app=”myAngularJSApp” name (i.e. document.documentElement, [‘myAngularJSApp’]), which you removed recently from your index.html file above. Once the application is bootstrapped and ready we need to set a reference to the “upgradeAdapterRef” to access to the AngularJS dependencies such as the $injector or $rootScope. To be able to access to the upgradeAdapterRef we will store it in a global variable.
/**
* src/main.ts
*/
import './polyfills.ts';
import { enableProdMode, Inject } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { environment } from './environments/environment';
import { UpgradeModule } from '@angular/upgrade/static';
import { AppModule, UPGRADE_ADAPTER } from './app/app.module';
import { Ng1Ng2UpgradeSharedService } from "./app/shared/upgrade/ng1-upgrade-shared.service";
if (environment.production) {
enableProdMode();
}
// Bootstrap the application
platformBrowserDynamic().bootstrapModule(AppModule).then(platformRef => {
const upgradeSharedService = platformRef.injector.get(Ng1Ng2UpgradeSharedService);
// Bootstrap the application
UPGRADE_ADAPTER.bootstrap(document.documentElement, ['YOUR_NG1_APP_MODULE_NAME']).ready((upgradeAdapterRef) => {
upgradeSharedService.setUpgradeAdapterRef(upgradeAdapterRef);
});
});
Modify app.component.ts and app.component.html files
We keep our app.component.ts file simple for the time being.
/**
* src/app/app.component.ts
*/
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less']
})
export class AppComponent {
title = 'ng2 app works!';
}
Add your AngularJS index.html content to the root component src/app/app.component.html template. Actually from now on you don’t need this AngularJS index.html file anymore.
<!-- Angular 2: Routed views go here -->
<router-outlet></router-outlet>
<!-- Angular 1: index.html content goes here -->
<div class="app">
<!-- App Body -->
<div id="app-body" class="app-body">
<div class="app-content" ng-class="direction">
<div class="modules-side-bar-container" ng-controller="NavBarController" >
<div ng-include="'assets/ui/html/shared/modules/modules-sidebar.html'"></div>
</div>
<ng-view></ng-view>
</div>
</div>
<div ui-yield-to="modals"></div>
Angular 2 index.html file
Don’t be confused and do not mix the index.html file from the AngularJS application and from Angular 2 together. The Angular 2 index.html file is located in src/index.html. Please note to use the selector “<app-root>” that we defined in the app.component.ts file above.
<!doctype html>
<html>
<head>
<base href="/">
<meta charset="utf-8">
<title>Your Application Title</title>
</head>
<body>
<app-root>ng2 loading ...</app-root>
</body><!-- Angular 1 dependencies can also be defined here -->
<script src="assets/i18n/angular-locale_en-us.js"></script></html>
Upgrade baby step 4: AngularJS and Angular 2 routings
Create a new file src/app/app.routing.ts
Create a new file “app.routing.ts” in your app root folder. All your Angular 2 routes can be defined in this file. Please note that the “WalkthroughModule” is loaded as a lazy path and each module has its own routings as well. For more information take a look at the official Angular 2 Routing & Navigation documentation page:
/**
* src/app/app.routing.ts
*/
import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppComponent } from "./app.component";
const APP_ROUTES:Routes = [
{path: 'walkthrough', loadChildren: './walkthrough/walkthrough.module#WalkthroughModule'}
];
export const APP_ROUTING:ModuleWithProviders = RouterModule.forRoot(APP_ROUTES, {useHash: false, enableTracing: false, initialNavigation: true});
Lazy loaded module: walkthrough-routing.module.ts
Please note that the route “walkthrough” is already defined in the src/app/app.routing.ts file as a path: ‘walkthrough’, so you don’t have to define it again in the walkthrough-routing.module.ts file.
/**
* src/app/walkthrough/walkthrough-routing.module.ts
*/
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { WalkthroughComponent } from "./walkthrough.component";
const routes:Routes = [
{path: '', component: WalkthroughComponent, children: []}
];
/**
* Walkthrough routings
*/
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
})
export class WalkthroughRoutingModule {
}
How to distinguish AngularJS routing and Angular 2 routing?
We already implemented an AngularJS and Angular 2 URL handling strategy, take a look at ng1-ng2-url-handling-strategy.ts file. It tells the Angular 2 router if the current URL visited by the user should be processed. When it returns true, the angular 2 router will execute the regular navigation. Otherwise the url will be executed by AngularJS assumed it is defined in the AngularJS router.
i.e. in the code below the urls “/”, “/auth” and “/walkthrough” will be executed by Angular 2 router because we already migrated those parts of the application to components, so they must be managed by the Angular 2 router.
/**
* src/app/shared/upgrade/ng1-ng2-url-handling-strategy.ts
*/
import { UrlHandlingStrategy } from '@angular/router';
export class Ng1Ng2UrlHandlingStrategy implements UrlHandlingStrategy {
shouldProcessUrl(url) {
var urlStr:string = url.toString();
return urlStr == ("/") || urlStr == ("/auth") || urlStr == ("/login") || urlStr == ("/walkthrough");
}
extract(url) {
return url;
}
merge(url, whole) {
return url;
}
}
Verification: go through the following checklist before bootstrapping our partly migrated Angular 2 application
Please check that you copied all files, went through all steps and imported all necessary modules etc… All questions below must be answered with yes to be able to bootstrap the application.
- Preparation phase 1: Is your application upgraded to the AngularJS version 1.5 or higher?
- Preparation phase 2: Did you change your AngularJS application structure layer to feature/component based layer?
- Upgrade baby step 1: Did you install angular-cli and execute “ng init” command within your project folder?
- Upgrade baby step 2: Did you replace all AngularJS factories with services?
- Upgrade baby step 3: Did you include all your AngularJS codes in the scripts[], all your css files in the styles[] and all your icons and images in the assets[] section of your generated angular-cli.json file.
- Upgrade baby step 3: Did you import our Ng1UpgradeModule in your src/app/shared folder?
- Upgrade baby step 3: Did you modify your root component module src/app/app.module.ts as shown above in the corresponding section?
- Upgrade baby step 3: Did you remove the ng-app tag from your index.html file, and define the AngularJS module name in the static main.ts file?
- Upgrade baby step 3: Did you modify the static src/main.ts file as shown above in the corresponding section and defined in src/index.html the root component selector <app-root></app-root> tag?
- Upgrade baby step 3: Did you modify your root component src/app/app.component.ts file as shown above in the corresponding section?
- Upgrade baby step 3: Did you add the AngularJS index.html content to the src/app/app.component.html file and defined there <router-outlet></router-outlet> tag? Otherwise you will not be able to see the routed pages by Angular 2.
- Upgrade baby step 4: Did you define a routing file in src/app/app.routing.ts and modify its content as shown above in the corresponding section?
Please note that every part of your code should be green and you should fix all red marked errors.
Let’s run the application
Finally it’s now time to start our partly migrated Angular 2 application. Please run the following command in the shell to build the application.
ng build
To start the application in an http server please run the following command:
ng serve
Open your browser, enter the following url and hit enter.
http://localhost:4200
When you see the “ng2 loading …….” text in your browser permanently it means the application could not be bootstrapped properly. Open the developer console of your browser and try to fix the displayed issues under the console tab.
Upgrade baby step 5: Migrate your first controller to a component
To simplify the migration from a controller to a component choose a simple controller that doesn’t include a lot of client logic. I started with a simple part of our application, the walkthrough section, where the user can see and slide some images that represents some information about our app.
I created the following walkthrough.component.ts file. Based on this i’m gonna explain you the differences between an AngularJS controller and an Angular 2 component.
- All public variables in the component below are equivalent to the $scope variables
- All private variables to the controller variables (i.e. var name = “test”)
- All private functions to the controller functions (function foo(){})
- All public functions to the $scope functions ($scope.foo = function() {})
/**
* src/app/walkthrough/walkthrough.component.ts
*/
import { Component, Inject } from '@angular/core';
import { Router } from '@angular/router';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'walkthrough',
templateUrl: './walkthrough.component.html',
styleUrls: ['./walkthrough.component.less']
})
export class WalkthroughComponent { // is equivalent to $scope.walkthroughs
public walkthroughs:Array<IWalkthrough>; // is equivalent to var name;
private name:string;
constructor(@Inject('$translate') private $translate:any,
private router:Router, sanitizer:DomSanitizer) { // Data initialisation goes here to the constructor
this.walkthroughs = [];
this.initTranslations(); }
/**
* private functions are equivalent to the controller functions
*/
private initTranslations() {
// i.e. function initTranslations() {}}
/**
* public functions can be equivalent to the $scope functions
*/
callAPublicFunction() {
// This public function can be called from the // walkthrough.component.html file
}
}
Dependency injection is managed by Angular 2 automatically. i.e. when you import the Router from ‘@angular/router’ and declare a variable like private router:Router in a constructor as shown above then the Angular 2 router will be injected and a private variable “router” will be initialized automatically.
Because our application is not full migrated and we still have AngularJS dependencies i.e. a translation service like $translate (angular-module: pascalprecht.translate) that we need in our Angular 2 application, we need to provide this service to the Angular 2 environment then it can easily be injected as “@Inject(‘$translate’)”.
Don’t forget to import the Ng1UpgradeModule in your walkthrough.module.ts file. (imports: [Ng1UpgradeModule])
/**
* src/app/walkthrough/walkthrough.module.ts
*/
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WalkthroughRoutingModule } from "./walkthrough-routing.module";
import { WalkthroughComponent } from "./walkthrough.component";
import { Ng1UpgradeModule } from "../shared/upgrade/ng1-upgrade-shared.module";
/**
* Walkthrough module
*/
@NgModule({
imports: [
Ng1UpgradeModule,
WalkthroughRoutingModule
],
declarations: [WalkthroughComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [],
exports: []
})
export class WalkthroughModule {
}
Upgrade and provide AngularJS dependencies for Angular 2
To provide more than $translate service or other dependencies of your AngularJS application please take a look at our Ng1UpgradeModule in the ng1-upgrade-shared.module.ts and ng1-upgrade-shared.providers.ts files.
/**
* ng1-upgrade-shared.providers.ts
*/
import {Ng1Ng2UpgradeSharedService} from "../upgrade/ng1-upgrade-shared.service";
export function upgradeTranslateFactory(upgradeAdapterRef:Ng1Ng2UpgradeSharedService) {
return upgradeAdapterRef.$injector.get("$translate");
}
You need to declare AngularJS dependencies as exported functions as shown above and add them to the providers[] section as shown below, that’s it. Your AngularJS dependency can be now injected in your Angular 2 application as “@Inject(“$translate”)”
/**
* src/app/shared/ng1-upgrade-shared.module.ts
*/
import { NgModule } from '@angular/core';
import { Ng1Ng2UpgradeSharedService } from "./ng1-upgrade-shared.service";
import { TranslatePipe } from "./ng1-translate-shared.pipe";
import * as upgradeFactories from "./ng1-upgrade-shared.providers";
/**
* Ng1 Ng2 upgrade module
* Registers providers and AngularJS services, which can be injected in Angular 2 services or components.
*/
@NgModule({
declarations: [TranslatePipe],
exports: [TranslatePipe]
})
export class Ng1UpgradeModule {
static forRoot() {
return {
ngModule: Ng1UpgradeModule,
providers: [Ng1Ng2UpgradeSharedService,
{
provide: '$translate',
useFactory: upgradeFactories.upgradeTranslateFactory,
deps: [Ng1Ng2UpgradeSharedService]
}
]
}
}
}
AngularJS filters
We still have a little problem, that the translate pipe is unknown by Angular 2 in our walkthrough.component.html template.
There is unfortunately no way to upgrade an AngularJS filter, we need to rewrite them i.e. we used the translate filter in our html templates like {{‘WALKTHROUGH_START_APP’ | translate}} to translate a translation key.
So I implemented an Angular 2 translation pipe, declared and exported it in the ng1-upgrade-shared.module.ts file as shown above. From now on the translation pipe will be recognized by Angular 2 in the templates.
/**
* src/app/shared/ng1-translate-shared.pipe.ts
*/
import { Pipe, PipeTransform, Inject } from '@angular/core';
/**
* Translates AngularJS i18n translations.
*/
@Pipe({name: 'translate'})
export class TranslatePipe implements PipeTransform {
constructor(@Inject('$translate') private $translate:any) {}
transform(translationId:string) {
return this.$translate.instant(translationId);
}
}
You can repeat the 5. baby step to upgrade other parts of your AngularJS application.
Summary
I hope this tutorial could help you to migrate your AngularJS application to Angular 2 in a simple way. Did you know that Angular 4 is almost here? You might be asking, didn’t we just migrate to Angular 2? Don’t worry, upgrading to Angular 4 is not gonna be particularly painful.
If you liked my article, don’t forget to clap, share or comment it. It’s important for me to reach more people. Please check my latest article: