This tutorial is out of date and no longer maintained.
Angular (2+) is here, and we’re all super excited about it. For some of us, though, we’re still maintaining large AngularJS (1.x) codebases at work. How do we start migrating our application to the new version of Angular – especially if we can’t afford to take six months away from a feature development for a complete rewrite?
That’s where the ngUpgrade library comes in. ngUpgrade is the official tool to allow you to migrate your application litle by little. It lets Angular run side-by-side along with your AngularJS code for as long as you need to slowly upgrade.
In this guide, you will install and set up ngUpgrade and Angular. Then, you’ll learn the basics of rewriting components.
(P.S. If the length of this guide freaks you out, don’t worry. I’ve built a step-by-step, super detailed video program called Upgrading AngularJS that covers all of this in detail.)
To complete this tutorial, you will need:
This tutorial was verified with Node v14.4.0, npm
v6.14.5, angular
v1.6.6, and @angular/common
v5.2.11.
To get started with ngUpgrade, your application needs to meet a few prerequisites:
For now though, take a minute to clone or fork the course sample project on GitHub:
- git clone https://github.com/upgradingangularjs/ordersystem-project.git
Navigate to the project directory:
- cd ordersystem-project
Checkout this commit to see our starting point:
- git checkout fdfcf0bc3b812fa01063fbe98e18f3c2f4bcc5b4
We’ve got an Order System project that we can use to work through ngUpgrade. Starting at this commit, our application meets all of the above criteria. We’re using component architecture, TypeScript, and Webpack (we’ve even got builds for both development and production).
Note: In many large AngularJS apps, you just can’t move everything into a brand new Git repository and wipe out years of history. You also might be using a different app structure than the CLI. If you can use the CLI for your upgrade, then feel free to do so. This guide, however, will teach you the manual setup here so that you can have complete control over your upgrade.
We’re ready to install Angular, ngUpgrade, and all of the peer dependencies. In the sample project, go ahead and update your package.json
dependencies array so it looks like this:
"dependencies": {
"@angular/common": "^5.2.5",
"@angular/compiler": "^5.2.5",
"@angular/core": "^5.2.5",
"@angular/forms": "^5.2.5",
"@angular/platform-browser": "^5.2.5",
"@angular/platform-browser-dynamic": "^5.2.5",
"@angular/router": "^5.2.5",
"@angular/upgrade": "^5.2.5",
"angular": "1.6.6",
"angular-route": "1.6.6",
"bootstrap": "3.3.7",
"core-js": "^2.5.3",
"jquery": "^2.2.4",
"lodash": "4.17.4",
"moment": "~2.17.1",
"reflect-metadata": "^0.1.12",
"rxjs": "^5.5.6",
"zone.js": "^0.8.20"
}
(We’re going to use Angular 5 in this series, even though the sample project uses version 4, but the steps are identical.)
We could put all of these packages in one long command in the terminal with the save flag, but we will take the time to explain what each of these packages are.
First are our libraries under the @angular
namespace:
@angular/common
: These are the commonly needed services, pipes, and directives for Angular. This package also contains the new HttpClient
as of version 4.3, so we no longer need @angular/http
.@angular/compiler
: This is Angular’s template compiler. It takes the templates and converts them into the code that makes your application run and render. You almost never need to interact with it.@angular/core
: These are the critical runtime parts of Angular needed by every application. This has things like the metadata decorators (e.g., Component
, Injectable
), all the dependency injection, and the component life-cycle hooks like OnInit
.@angular/forms
: This is everything we need with forms, whether template or reactive.@angular/platform-browser
: This is everything DOM and browser related, especially pieces that help render the DOM. This is the package that includes bootstrapStatic
, which is the method that we use for bootstrapping our applications for production builds.@angular/platform-browser-dynamic
: This package includes providers and another bootstrap method for applications that compile templates on the client. This is the package that we use for bootstrapping during development and we’ll cover switching between the two in another video.@angular/router
: As you might guess, this is just the router for Angular.@angular/upgrade
: This is the ngUpgrade library, which allows us to migrate our AngularJS application to Angular.After all of our Angular packages come our polyfill packages that are dependencies of Angular:
core-js
: Patches the global context or the window with certain features of ES6 or ES2015.reflect-metadata
: This is a polyfill library for the annotations that Angular uses in its classes.rxjs
: This is the library that includes all of the observables that we’ll use for handling our data.zone.js
: THis is a polyfill for the Zone specification, which is part of how Angular manages change detection.Sometimes, there are conflicts involving the version of TypeScript you’re using. This can be due to RxJS, the Angular compiler, or Webpack. If you start getting weird compilation errors, do some research to find out of any of those need a specific version range of TypeScript for the version you’re using.
Open your terminal, cd
into the public
folder of the project:
- cd public
And use npm
to install the dependencies (you’re welcome to install and use Yarn if you’d prefer):
- npm install
You will see that all of your packages were installed.
We’re now ready to make our application a hybrid application by dual-booting both AngularJS and Angular.
To set up ngUpgrade, we need to do a series of steps to allow AngularJS and Angular to run alongside of each other.
index.html
The first thing we need to do is remove our bootstrap directive from index.html
. This is how AngularJS normally gets started up at page load, but we’re going to bootstrap it through Angular using ngUpgrade. So, just open index.html
and remove that data-ng-app
tag. (If you’re using strict DI in your own app, you’ll remove ng-strict-di
as well in this step.)
Your index.html
file will look like this now:
<html>
<head>
<title>Amazing, Inc. Order System</title>
</head>
<body>
<navigation></navigation>
<div class="container" ng-view></div>
</body>
</html>
Now, we need to make some changes in AngularJS module. Open up app.ts
.
The first thing we need to do is rename app.ts
to app.module.ajs.ts
to reflect that it’s the module for AngularJS. It’s kind of a lengthy name, but in Angular we want to have our type in our file name. Here we’re using app.module
and then we’re adding that ajs
to specify that it’s for AngularJS instead of our root app.module
for Angular (which we’ll make in a second).
As the app is now, we’re just using AngularJS, so we have all of our import statements here and we’re registering everything on our Angular module. However, now what we’re going to do is export this module and import it into our new Angular module to get it up and running.
So, on Line 28 let’s create a string constant of our app name:
const MODULE_NAME = 'app';
Then we’ll replace our app string with module name in our angular.module
declaration:
angular.module(MODULE_NAME, ['ngRoute'])
// component and service registrations continue here
And finally, we need to export our constant:
export default MODULE_NAME;
At this point, changes made to app.module.ajs.ts
should resemble this diff.
Our AngularJS module is ready to go, so we’re now ready to make our Angular module. We’ll then import our AngularJS module so we can manually bootstrap it here. That’s what let’s the two frameworks run together, and enables ngUpgrade to bridge the gap between them.
The first thing we need to do is create a new file at the same level as our AngularJS module called app.module.ts
. Now for the first time, you’re about to see a pattern that’s going to become familiar to you throughout your upgrade: making and exporting a class, decorating it with an annotation, and importing all of the dependencies.
In our new app module, let’s create a class named AppModule
:
export class AppModule {
}
Now, let’s add our first annotation (also called a decorator). An annotation is just a bit of metadata that Angular uses when building our application. Above our new class, we’ll use the NgModule
annotation and pass in an options object:
@NgModule({})
export class AppModule {
}
If you’re following along in an editor like Visual Studio Code, you’ll see that TypeScript is mad at us because it doesn’t know what NgModule
is. This is because we need to import it from the Angular core library. Above our decorator, we can fix this with:
import { NgModule } from '@angular/core';
Now, in our options object for ngModule, we need to pass an array of imports. The imports array specifies other NgModules that this NgModule
will depend on. (These imports are different than the TypeScript imports at the top of our file.) Right now, we need the BrowserModule
and the UpgradeModule
:
import { NgModule } from '@angular/core';
@NgModule({
imports: [
BrowserModule,
UpgradeModule
]
})
export class AppModule {
}
Of course, we don’t have those imported either at the top of our file, so we need to do that, too. After our first import, we can add:
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
There’s an UpgradeModule
in both upgrade
and upgrade/static
. We want to use the static
one because it provides better error reporting and works with AOT (ahead-of-time) compiling.
We’ve got the basic scaffolding of our root module for Angular set up and we’re ready to do the bootstrapping itself.
To bootstrap our application, the first thing we need to do is inject UpgradeModule
using a constructor function:
constructor(private upgrade: UpgradeModule){
}
We don’t need to do anything in our constructor function. The next thing we’ll do is override the doBootstrap
function. After the constructor, type:
ngDoBootstrap(){
}
Next, we’ll use the UpgradeModule’s bootstrap function. It has the same signature as the Angular bootstrap function, but it does a couple extra things for us. First, it makes sure that Angular and AngularJS run in the correct zones, and then it sets up an extra module that allows AngularJS to be visible in Angular and Angular to be visible in AngularJS. Lastly, it adapts the testability APIs, so that Protractor will work with hybrid apps, which is super important.
Let’s add it:
ngDoBootstrap(){
this.upgrade.bootstrap(document.documentElement, [moduleName], {strictDi: true});
}
We’re first passing in our document
element and then our AngularJS module inside an array. Lastly, just so you can see an example of this, we’re adding a config object so we can switch on strict dependency injection.
You may be wondering where the moduleName
came from. We need to import it up with our other import statements:
import moduleName from './app.module.ajs';
Here’s what our completed app.module.ts
file looks like now:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
import moduleName from './app.module.ajs';
@NgModule({
imports: [
BrowserModule,
UpgradeModule
]
})
export class AppModule {
constructor(private upgrade: UpgradeModule) { }
ngDoBootstrap(){
this.upgrade.bootstrap(document.documentElement, [moduleName], {strictDi: true});
}
}
This is going to be a pattern that’s going to become familiar to you over time.
main.ts
Now that we’ve got our AngularJS module and our Angular module set up, we need an entry point that’s going to bring these two together and get our application running. Let’s create a new file under our src
folder called main.ts
.
In main.ts
, we need to import a few things, tell Angular which version of AngularJS to load, and then tell it to bootstrap our Angular module. First, we need to import two polyfill libraries and Angular’s platformBrowserDynamic
function:
import 'zone.js';
import 'reflect-metadata';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
Why platformBrowserDynamic
instead of just platformBrowser
? Angular has two ways to compile: a dynamic option and a static option. In the dynamic option (known as just-in-time, or JIT), the Angular compiler compiles the application in the browser and then launches the app. The static option (known as ahead-of-time, or AOT) produces a much smaller application that launches faster. This is because the Angular compiler runs ahead of time as part of the build process. We’re just going to be using the JIT method here along with the Webpack dev server.
Now we need to import both our Angular and AngularJS modules, as well as a method that tells Angular which version of AngularJS to use:
import { setAngularLib } from '@angular/upgrade/static';
import * as angular from 'angular';
import { AppModule } from './app.module';
Now to finish this off, we just need to call setAngularLib
and pass in our version of AngularJS, and we need to call platformBrowserDynamic
and tell it to bootstrap our app module.
The finished file looks like this:
import 'zone.js';
import 'reflect-metadata';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { setAngularLib } from '@angular/upgrade/static';
import * as angular from 'angular';
import { AppModule } from './app.module';
setAngularLib(angular);
platformBrowserDynamic().bootstrapModule(AppModule);
Now that we’ve got that set up, we just need to change our Webpack entry point in our config.
Hopefully, this process of bootstrapping a hybrid application is starting to make sense to you. We have a main.ts
file that’s our entry point, which sets up our AngularJS library and bootstraps our Angular module. Then, our Angular module bootstraps our AngularJS module. That’s what let’s both frameworks run alongside each other.
We’re now ready to change our Webpack config so that it’s starting with our main.ts
file and not one of our app module files. Open up webpack.common.js
(it’s under the webpack-configs
folder). Under module.exports
for entry
, we’ll change our app root to main.ts
:
entry: {
app: './src/main.ts',
}
Now, we’re ready to see our hybrid application in action. You can run the dev server by opening a terminal and running these commands:
- # ensure that you are in the `ordersystems-project` directory
- cd server
- npm install
- npm start
In a second terminal session, run these commands:
- # ensure that you are in the `ordersystems-project` directory
- cd public
- npm run dev
You will see that Webpack is loading and that our TypeScript is compiled successfully.
Let’s go check out the browser at localhost:9000
. You can see that our application still runs on our dev server.
You might see a couple of warnings in the console about core-js
depending on your version, but don’t worry about them, they won’t affect us. You can also open the network tab and see the vendor bundle and app bundle:
The vendor bundle is absolutely huge, and that’s because:
We’ll fix this downstream when we talk about AOT compiling, but we can navigate around here and see that all of our data is still loading.
We now have Angular and AngularJS running alongside of each other, which means we’ve successfully set up our hybrid application. That means we’re ready to start upgrading our application piece by piece.
We’ve got our application bootstrapped and running in hybrid mode, so we’re ready to get started with migrating each piece of our application. One common approach is to pick a route and then start from the bottom up to rewrite each piece, starting with whatever has the least dependencies. This allows us to iteratively upgrade our application so that every point along the way, we have something that’s deployable to production.
Let’s start with the home route because that’s an easy one with just the Home component. We’ll first rename our Home component from home.ts
to home.component.ts
.
Now, we need to rewrite our Home component as an Angular class. The first thing we need to do is import Component
from the Angular core library at the top of our file:
import { Component } from '@angular/core'
The next thing we’ll do is convert our function homeComponentController
to a class. We can also capitalize it and remove the controller
at the end of the name, so that it’s just called HomeComponent
. Lastly, let’s get rid of the parenthesis. It looks like this now:
class HomeComponent {
var vm = this;
vm.title = 'Awesome, Inc. Internal Ordering System';
}
Now, let’s clean up what’s inside the class. We no longer need the declaration of vm
since we’re using a class. We can also add a property of title
as a string, and move setting the title to a constructor function. Our class looks like this now:
class HomeComponent {
title: string;
constructor(){
title = 'Awesome, Inc. Internal Ordering System';
}
}
We also need to export
this class and then delete that export default
line.
Now, we need to apply the Component
metadata decorator that we imported to tell Angular that this is a component. We can replace the Home component object with the component decorator and an options object:
@Component({
}
The first option of our component decorator is the selector
. This is just the HTML tag that we’ll use to reference this component, which will just be ‘home’. Note that in Angular, the selector is a string literal. This is different than in AngularJS, where we would name the component in camel case, and then it would translate to an HTML tag with hyphens. Here, we’re going to put exactly the tag that we want to use. In this case, we’re just keeping it to ‘home’, so it doesn’t matter too much. After that, we’ll specify our template
, just like we did with AngularJS, so I’ll just say template: template
. And believe it or not, that’s all there is to it. Our finished component looks like this:
import { Component } from '@angular/core';
const template = require('./home.html');
@Component({
selector: 'home',
template: template
})
export class HomeComponent {
title: string;
constructor(){
this.title = 'Awesome, Inc. Internal Ordering System';
}
}
Note: If you’re working on an application that will use the AOT compiler, you’ll want to use templateUrl
instead of what we’re doing here and make some changes to Webpack. This is totally fine for JIT and the development server, though.
We now need to use the ngUpgrade library to “downgrade” this component. “Downgrading” means to make an Angular component or service available to AngularJS. “Upgrading,” on the other hand, means to make an AngularJS component or service available to Angular. We’ll cover that in another article. Luckily, downgrading is super easy.
First, we need to do two things at the top of our file along with our imports. We need to import the downgradeComponent
function from the Angular upgrade library declare a variable called angular
so we can register this component on our AngularJS module. This looks like this:
import { downgradeComponent } from '@angular/upgrade/static';
declare var angular: angular.IAngularStatic;
Downgrading the component is fairly straightforward. Down at the bottom of our component, we’ll register this component as a directive. We’ll pass in our directive name, which is just home
, the same as our selector in this case. Then after that, we’ll pass in the downgradeComponent
function from ngUpgrade. This function converts our Angular component into an AngularJS directive. Finally, we’ll cast this object as angular.IDirectiveFactory
. The finished registration looks like this:
app.module('app')
.directive('home', downgradeComponent({component: HomeComponent} as angular.IDirectiveFactory);
At this point, changes made to home.component.ts
should resemble this diff.
Now, we have a downgraded Angular component that’s available to our AngularJS application. You might be wondering why we registered that directive here at the bottom of this file instead of importing and registering it in our AngularJS module TypeScript file. The end goal is to get rid of that file altogether once all of our application is converted, so we want to gradually remove things from that file and then eventually delete it altogether when we uninstall AngularJS. This works great for sample applications or rapid migrations (more on that in a second).
Go ahead and open up app.module.ajs.ts
and remove the import of homeComponent
on Line 12 and the component registration on Line 37.
At this point, changes made to app.module.ajs.ts
should resemble this diff.
This method of downgrading – registering the downgraded component in the component file and removing it from the AngularJS module file – works perfectly well for development or if you plan on quickly rewriting your application before you deploy. However, the Angular AOT compiler for production won’t work with this method. Instead, it wants all of our downgraded registrations in the AngularJS module.
The downgrade is identical, but instead you’d:
downgradeComponent
in app.module.ajs.ts
(you’ve already got angular
in there so you don’t need to declare it).homeComponent
to import { HomeComponent } from './home/home.component';
since we switched to a named export.You can read more about setting up ngUpgrade for AOT in this article.
After a component is updated, we need to be sure to update its template so it complies with the new Angular syntax. In this case, there are only minimal changes you must make to homeComponent
. We just need to remove $ctrl
on Line 2. The template looks like this now:
<div class="row">
<h1>{{title}}</h1>
</div>
Now, we have a fully functional downgraded Home component in our hybrid application.
Let’s add our new Angular component to our Angular module. Open up app.module.ts
. First, we need to just import our HomeComponent
after all of our other imports:
import { HomeComponent } from './home/home.component';
Now, we need to add HomeComponent
to our Angular application. All Angular components must be added to a declarations
array of our NgModule. So, after Line 12 in our options object, we’ll add a new array called declarations and add our component:
declarations: [
HomeComponent
]
We also need to create an entryComponents
array and add our HomeComponent
to that. All downgraded components must be added to this entryComponents
array. We’ll add it after our declarations
:
entryComponents: [
HomeComponent
]
At this point, changes made to app.module.ts
should resemble this diff.
With that, we’re finished.
Let’s run those same commands as before and make sure our application is still working. Here are those commands again:
- # ensure that you are in the `ordersystems-project` directory
- cd server
- npm start
In a second terminal session, run these commands:
- # ensure that you are in the `ordersystems-project` directory
- cd public
- npm run dev
Head back over to localhost:9000
. You can see that our HomeComponent
is loading in the browser as a rewritten Angular component!
You can even go look at the Sources tab of Chrome DevTools just to be positive. Open up webpack://
, scroll down to ./src/home/home.component.ts
, and sure enough, there it is!
In this tutorial, you installed Angular and ngUpgrade, set up an Angular module, bootstrapped Angular and AngularJS, updated Webpack, and rewrote and downgraded your first component.
To learn more about Angular, check out the Angular topic page.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Near the top, you have an HTML error. Your link anchor to UpgradingAngularJS.com on the image has 2 backslashes. Remove them.