When moving an AngularJS 1.x codebase to Angular (2/4/5 etc.), the usual path involves first preparing the existing code:

Directly Upgrading From AngularJS 1.X — figure 1

That preparation typically includes adopting newer AngularJS 1.x features such as components, and introducing TypeScript along with a module loader like SystemJS or webpack. The intention is to get closer to Angular's conventions so that the eventual integration goes more smoothly.

However, there are scenarios where spending time on that preparatory work doesn't make sense. Consider cases where you only need to build new pieces of the app with Angular while leaving the existing AngularJS 1.x code largely untouched. In such situations, skipping the preparation phase can be a reasonable shortcut:

Directly Upgrading From AngularJS 1.X — figure 2

This article walks through the steps required to make that shortcut work in practice. Like the official upgrading guide, which does include the preparation stage, it uses the well-known AngularJS 1.x Phone Catalog Sample as the basis.

Despite the sample making use of AngularJS components introduced in AngularJS 1.5, everything covered here applies equally to older-style AngularJS code built on controllers and directives.

The complete sample is available in the accompanying GitHub repository. To simplify following along, each step described below corresponds to a dedicated commit.

Step 1: Setting Up the Fresh Angular App

The starting point for this walkthrough is a new Angular application generated with the Angular CLI:

ng new migrated

To keep the layout of the new solution clear, create two directories under src: one for the inherited AngularJS code and another for the new Angular code. In the example, these are named ng1 and ng2 respectively:

Directly Upgrading From AngularJS 1.X — figure 3

Next, relocate all generated files into the ng2 folder, with the exception of tsconfig.app.json, tsconfig.spec.json, favicon.ico and index.html.

To inform the CLI's build process about this new directory layout, adjust the .angular-cli.json file. The assets section there can also be used to make the CLI copy the ng1 directory straight into the output folder:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "project": {
    "name": "migrated"
  },
  "apps": [
    {
      "root": "src",
      "outDir": "dist",
      "assets": [
        "ng1",
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "ng2/main.ts",
      "polyfills": "ng2/polyfills.ts",
      "test": "ng2/test.ts",
      "tsconfig": "tsconfig.app.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "ng2/styles.css"
      ],
      "scripts": [],
      "environmentSource": "ng2/environments/environment.ts",
      "environments": {
        "dev": "ng2/environments/environment.ts",
        "prod": "ng2/environments/environment.prod.ts"
      }
    }
  ],
  "e2e": {
    "protractor": {
      "config": "./protractor.conf.js"
    }
  },
  "lint": [
    {
      "project": "tsconfig.app.json"
    },
    {
      "project": "tsconfig.spec.json"
    },
    {
      "project": "tsconfig.e2e.json"
    }
  ],
  "test": {
    "karma": {
      "config": "./karma.conf.js"
    }
  },
  "defaults": {
    "styleExt": "css",
    "component": {}
  }
}

Now place the entire AngularJS 1.x application into the ng1 folder, but leave out its index.html. To make the existing app work with the revised structure, you'll need to update it. This means fixing every reference to templates as well as links to JSON files and images.

After that, combine the original index.html with the one generated inside src:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Migrated</title>
  <base href="/">

  <!-- ng1 -->
  <link rel="stylesheet" href="ng1/bower_components/bootstrap/dist/css/bootstrap.css" />
  <link rel="stylesheet" href="ng1/app.css" />
  <link rel="stylesheet" href="ng1/app.animations.css" />

  <script src="ng1/bower_components/jquery/dist/jquery.js"></script>
  <script src="ng1/bower_components/angular/angular.js"></script>
  <script src="ng1/bower_components/angular-animate/angular-animate.js"></script>
  <script src="ng1/bower_components/angular-resource/angular-resource.js"></script>
  <script src="ng1/bower_components/angular-route/angular-route.js"></script>
  <script src="ng1/app.module.js"></script>
  <script src="ng1/app.config.js"></script>
  <script src="ng1/app.animations.js"></script>
  <script src="ng1/core/core.module.js"></script>
  <script src="ng1/core/checkmark/checkmark.filter.js"></script>
  <script src="ng1/core/phone/phone.module.js"></script>
  <script src="ng1/core/phone/phone.service.js"></script>
  <script src="ng1/phone-list/phone-list.module.js"></script>
  <script src="ng1/phone-list/phone-list.component.js"></script>
  <script src="ng1/phone-detail/phone-detail.module.js"></script>
  <script src="ng1/phone-detail/phone-detail.component.js"></script>
  <!-- /ng1 -->

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body ng-app="phonecatApp">

  <!-- ng1 -->
  <div class="view-container">
      <div ng-view class="view-frame"></div>
  </div>
  <!-- /ng1 -->

  <app-root></app-root>

</body>
</html>

This merged index.html references the CSS and script files needed by the AngularJS 1.x app. It also bootstraps that app using ng-app and supplies its shell, which includes a div with the ng-view directive. The router uses that element to activate the configured templates.

The same file also contains the root element for the Angular application. There is no need to reference the generated Angular bundles, since the build process produces them automatically.

When you run ng serve, both applications are loaded into the browser independently. Visiting http://localhost:4200 confirms this:

Directly Upgrading From AngularJS 1.X — figure 4

Because the two apps bootstrap separately, they cannot share services or components. To enable that, they need to be bootstrapped together as a hybrid application, which is the subject of the next section.

Step 2: Bootstrapping a Hybrid AngularJS+Angular App

To bootstrap a single application containing both AngularJS 1.x and Angular, you can rely on ngUpgrade, which ships with Angular:

npm install @angular/upgrade --save

Since the Angular app should no longer bootstrap on its own, remove its root component from the index.html:

<!-- remove root component -->
<!--
    <app-root></app-root>
-->

Now bring both applications together during bootstrap. Start by importing the UpgradeModule into the Angular module (AppModule). Also take the AppComponent out of the bootstrap array, because the hybrid app will be bootstrapped manually:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { UpgradeModule, downgradeComponent } from '@angular/upgrade/static';
import { AppComponent } from './app.component';
import { Ng2DemoComponent } from "ng2/app/ng2-demo.component";

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  providers: [],
  // bootstrap: [AppComponent] // No Bootstrap-Component
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['phonecatApp'], { strictDi: true });
  }
}

In this example, the hybrid application is bootstrapped inside ngDoBootstrap using the injected UpgradeModule. To avoid bootstrapping the AngularJS 1.x part twice, the ng-app directive must be removed from index.html.

After these changes, starting the application displays only the AngularJS 1.x components:

Directly Upgrading From AngularJS 1.X — figure 5

Still, this is now a hybrid application running both versions of Angular. To verify that, the next section demonstrates how to embed an Angular component inside the AngularJS component shown above.

Step 3: Downgrading an Angular Component

For demonstrating the use of an Angular component within the AngularJS context of the hybrid app, this tutorial introduces a minimal dummy component:

// src/app/ng2-demo.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'ng2-demo',
  template: `
    <h3>Angular 2 Demo Component</h3>
    <img width="150" src="..." />
  `
})
export class Ng2DemoComponent  {
}

The image source used here comes from the scaffolded AppComponent.

To make this component available inside an AngularJS template, it needs to be downgraded. ngUpgrade provides the downgradeComponent function for exactly this purpose:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { UpgradeModule, downgradeComponent } from '@angular/upgrade/static';
import { AppComponent } from './app.component';
import { Ng2DemoComponent } from "ng2/app/ng2-demo.component";

declare var angular: any;

angular.module('phonecatApp')
  .directive(
    'ng2Demo',
    downgradeComponent({component: Ng2DemoComponent})
  );

@NgModule({
  declarations: [
    AppComponent,
    Ng2DemoComponent
  ],
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  entryComponents: [
    Ng2DemoComponent // Don't forget this!!!
  ],
  providers: [],
  // bootstrap: [AppComponent] // No Bootstrap-Component
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['phonecatApp'], { strictDi: true });
  }
}

As shown, the downgraded component is registered as a directive on the AngularJS 1.x module. This is done via the global angular variable. For TypeScript to recognize that preexisting variable, the declare keyword is necessary.

Once registered, the Angular component can be used directly in an AngularJS 1.x template:

  <!-- src/ng1/phone-list/phone-list.template.html -->

  <div class="row">
    <div class="col-md-2">
      <!--Sidebar content-->

      <p>
        Search:
        <input ng-model="$ctrl.query" />
      </p>

      <p>
        Sort by:
        <select ng-model="$ctrl.orderProp">
          <option value="name">Alphabetical</option>
          <option value="age">Newest</option>
        </select>
      </p>

      <p>

        <!-- Angular 2 Component -->
        <ng2-demo></ng2-demo>

      </p>

    </div>

In line with AngularJS conventions, HTML uses kebab case, while the directive must be registered with its normalized name, which is camel case in JavaScript.

Reloading the application now shows the AngularJS 1.x phone list together with the Angular demo component:

Directly Upgrading From AngularJS 1.X — figure 6

You might now be asking how this Angular component can tap into the existing application logic provided by AngularJS 1.x services. The next section answers that question.

Step 4: Upgrading a Service

To make an existing AngularJS 1.x service available to a new Angular component, it must first be upgraded. Per the official documentation, this involves creating an Angular service provider whose factory receives the AngularJS 1.x injector ($injector) and uses it to retrieve the desired service:

// src/ng2/app/phone.service.ts

import { InjectionToken } from "@angular/core";

export const PHONE_SERVICE = new InjectionToken<any>('PHONE_SERVICE');

export function createPhoneService(i) {
  return i.get('Phone');
}

export const phoneServiceProvider = {
  provide: PHONE_SERVICE,
  useFactory: createPhoneService,
  deps: ['$injector']
}

Ordinarily, the service's type could serve as the dependency injection token via the provide property. But in this case, the AngularJS 1.x code was deliberately not converted to TypeScript, so no such type exists. Instead, the sample defines a constant-based token named PHONE_SERVICE. For tokens like this, Angular 4+ offers the InjectionToken type. In Angular 2, you would use OpaqueToken. The InjectionToken accepts a type parameter that identifies the type of the service it represents. Since there is no type for this service, any is used.

This service provider must then be registered with the Angular module:

// src/ng2/app/app.module.ts

[...]
import { phoneServiceProvider } from "ng2/app/phone.service";

[...]

@NgModule({
  [...],
  providers: [
    phoneServiceProvider
  ]
})
export class AppModule {
  [...]
}

With that in place, phoneService can be injected into Ng2DemoComponent and used to fetch all managed phones:

import { Component, OnInit, Inject } from '@angular/core';
import { PHONE_SERVICE } from "ng2/app/phone.service";

@Component({
  selector: 'ng2-demo',
  template: `
    <h3>Angular 2 Demo Component</h3>
    <img width="150" src="[...]" />
    <p>
      {{phones.length}} Phones found.
    </p>
  `
})
export class Ng2DemoComponent implements OnInit {

  phones: any[] = [];

  constructor(
    @Inject(PHONE_SERVICE) private phoneService: any) {
    }

    ngOnInit() {
      this.phones = this.phoneService.query();
    }

}

Because the token is a plain constant, the sample relies on the Inject decorator to point to it. After fetching the phones, the component simply displays their count.

After a reload, the following becomes visible:

Directly Upgrading From AngularJS 1.X — figure 7

Notice that we now have an AngularJS 1.x component containing an Angular component that is showing data obtained from an AngularJS 1.x service.

Besides nesting AngularJS 1.x and Angular elements, it's also necessary to support routing from both versions. The upcoming sections address that challenge.

Step 5: Routing to Angular Components

Getting the AngularJS 1.x Router to activate Angular components is straightforward. All that's needed is a route whose template points to the component:

$routeProvider.
  when('/phones', {
    template: '<phone-list></phone-list>'   // AngularJS 1.x template 
  }).
  when('/phones/:phoneId', {
    template: '<phone-detail></phone-detail>' // AngularJS 1.x template 
  }).
  when('/ng2-demo', {
    template: '<ng2-demo></ng2-demo>' // Angular component
  })

This allows Angular components to be used alongside AngularJS routes that may rely on traditional controllers, directives, or components.

It's worth noting that this same technique works with the widely used UI-Router as well.

While this approach is simple, it has a limitation: the newly written components cannot use the modern Angular Router. To overcome that, one can implement Victor Savkin's Sibling Outlet approach, which lets both routers cooperate. The foundation for this is his Upgrade Shell pattern. The next two sections describe how to introduce these ideas into the example at hand.

Step 6: Applying Victor Savkin's Upgrade Shell Pattern

The upgrade shell pattern was introduced by Victor Savkin, one of the key architects behind Angular. He explains it in his blog as well as in his eBook on ngUpgrade. The idea is to have an Angular component sit at the top of a hybrid application. That component serves as the shell containing both AngularJS building blocks (directives, components, controllers) and Angular components.

To realize this pattern, the AppComponent created by the CLI can be reused:

// src/ng2/app/app.component.html
<!--The whole content below can be removed with the new code.-->
<div style="text-align:center">
  <h1>
    Welcome to {{title}}!!
  </h1>

</div>

<!-- ng1 -->
<div class="view-container">
    <div ng-view class="view-frame"></div>
</div>
<!-- /ng1 -->

This Angular component includes the ng-view element used by the AngularJS 1.x router.

To make it the root component of the application, it needs to be bootstrapped directly. That means adding it to the bootstrap array of the AppModule:

// src/ng2/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, InjectionToken } from '@angular/core';
import { UpgradeModule, downgradeComponent } from '@angular/upgrade/static';
import { AppComponent } from './app.component';
import { Ng2DemoComponent } from "ng2/app/ng2-demo.component";
import { phoneServiceProvider } from "ng2/app/phone.service";

declare var angular: any;

angular.module('phonecatApp')
  .directive(
    'ng2Demo',
    downgradeComponent({component: Ng2DemoComponent})
  );

@NgModule({
  declarations: [
    AppComponent,
    Ng2DemoComponent
  ],
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  entryComponents: [
    Ng2DemoComponent // Don't forget this!!!
  ],
  providers: [
    phoneServiceProvider
  ],
  bootstrap: [AppComponent]
})

export class AppModule {
// Remove code for bootstrapping hybrid app manually !!!
/*
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['phonecatApp'], { strictDi: true });
  }
*/
}

Also, the manual bootstrap code must be removed from the module. That logic is now moved into the AppComponent, where it runs after the upgrade shell has been bootstrapped:

// src/ng2/app/app.component.ts

import { Component, Inject } from '@angular/core';
import { PHONE_SERVICE } from "ng2/app/phone.service";
import { UpgradeModule } from "@angular/upgrade/static";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';

   phones: any[] = [];

   constructor(private upgrade: UpgradeModule) { }

    ngOnInit() {
      this.upgrade.bootstrap(document.body, ['phonecatApp']);
    }
}

In addition, ensure the index.html only references the upgrade shell:

<!-- src/index.html -->
<body>
  <app-root></app-root>
</body>

Reload the app and you should see the upgrade shell wrapping the AngularJS 1.x application.

Once that works, the groundwork is laid for the goal of the next section: running both the AngularJS 1.x router and the Angular router side by side.

Step 7: Applying Victor Savkin's Sibling Outlet technique to run both routers simultaneously

Victor Savkin's sibling outlet approach (https://blog.nrwl.io/upgrading-angular-applications-managing-routers-and-url-ca5588290aaa) offers a method for running the routers from both Angular versions concurrently. To get started, the Angular router must be imported:

npm install @angular/router --save

Next, update app.component.html to include separate outlets for each router version. For the AngularJS 1.x router, use a div with the ng-view directive; for the Angular Router, add a router-outlet element:

<!-- src/ng2/app/app.component.html -->
<div class="view-container">
    <div ng-view class="view-frame"></div>
    <router-outlet></router-outlet>
</div>

When an AngularJS 1-based route is triggered, the first outlet receives the template. Conversely, when an Angular route is activated, the second outlet is populated.

Now set up the Angular router configuration:

// src/ng2/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, InjectionToken } from '@angular/core';
import { RouterModule} from '@angular/router';

import { UpgradeModule, downgradeComponent } from '@angular/upgrade/static';
import { AppComponent } from './app.component';
import { Ng2DemoComponent } from "ng2/app/ng2-demo.component";
import { phoneServiceProvider } from "ng2/app/phone.service";

declare var angular: any;

angular.module('phonecatApp')
  .directive(
    'ng2Demo',
    downgradeComponent({component: Ng2DemoComponent})
  );

@NgModule({
  declarations: [
    AppComponent,
    Ng2DemoComponent
  ],
  imports: [
    BrowserModule,
    UpgradeModule,
    RouterModule.forRoot([
      {
        path: '',
        pathMatch: 'full',
        redirectTo: 'ng2-route'

      },
      {
        path: 'ng2-route',
        component: Ng2DemoComponent
      }
    ],
    {
      useHash: true
    }
    )
  ],
  entryComponents: [
    Ng2DemoComponent
  ],
  providers: [
    phoneServiceProvider
  ],
  bootstrap: [AppComponent]
})

export class AppModule {
}

The example configuration above defines only two routes for the Angular router. It also employs the hash strategy to keep URL handling consistent between the two router versions.

To prevent the Angular router from interfering when an AngularJS 1.x route is active, Victor recommends a custom UrlHandlingStrategy:

// src/ng2/app/app.module.ts

import { RouterModule, UrlHandlingStrategy } from '@angular/router';

[...]

export class CustomHandlingStrategy implements UrlHandlingStrategy {
  shouldProcessUrl(url) {
    return url.toString().startsWith("/ng2-route") || url.toString() === "/";
  }
  extract(url) { return url; }
  merge(url, whole) { return url; }
}

This strategy needs to be registered within the AppModule:

// src/ng2/app/app.module.ts
@NgModule({
  [...]
  providers: [
    phoneServiceProvider,
    { provide: UrlHandlingStrategy, useClass: CustomHandlingStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

Following that, adjust the AngularJS 1.x routing setup slightly. First, remove the configured hash prefix, as it would confuse the Angular router. Then, add a default route via otherwise that loads an empty template into the version-1 outlet whenever the current route is managed by the Angular router:

// src/app1/app.config.js

// No Prefix for the sake of uniformity
// $locationProvider.hashPrefix('!');

$routeProvider.
  when('/phones', {
    template: '<phone-list></phone-list>'
  }).
  when('/phones/:phoneId', {
    template: '<phone-detail></phone-detail>'
  }).
  when('/ng2-demo', {
    template: '<ng2-demo></ng2-demo>'
  })
  .otherwise({template : ''});

As mentioned earlier, everything demonstrated with the AngularJS 1.x router works equally well with UI-Router.

Finally, add a navigation menu to the AppComponent to toggle between routes handled by either router:

<!-- src/app2/app.component.html -->

<a routerLink="ng2-route">ng2-route</a> |
<a href="#/phones">Phones</a>

Once the application is reloaded, you should be able to navigate freely between the routes from both systems:

Directly Upgrading From AngularJS 1.X — figure 8