Directly upgrading from AngularJS 1.x to Angular without preparing the existing Code Base

When upgrading from AngularJS 1.x to Angular (2/4/5 etc.), we usually prepare our AngularJS 1.x code base first:

This can involve leveraging new AngularJS 1.x techniques like components. Additionally, introducing TypeScript as well as module loaders like SystemJS or webpack are further tasks to prepare the existing code. The goal behind this is to draw near Angular in order to allow a better integration.

But in some situations preparing the existing code is too costly. For instance, think about situations where we just want to write new parts of the application with Angular without the need to modify much of the existing AngularJS 1.x code. When this holds true for your project, skipping the preparation phase could be a good idea:

This post shows step by step, how this approach can be accomplished. Like the official and well written upgrading tutorial which includes preparing the code base, it upgrades the popular AngularJS 1.x Phone Catalog Sample.

Even though this sample leverages AngularJS components introduced with AngularJS 1.5, everything shown here also works with more "traditional" AngularJS code using controllers and directives.

The whole sample can be found in my GitHub repository. In order to make following everything easier, I've also created one commit for each of the steps described here.

Step 1: Creating the new Angular Application

As a starting point this article assumes that we are scaffolding a new Angular application with the Angular CLI:

ng new migrated

To make the structure of this new solution clearer, create one folder for the existing AngularJS code and another one for the new Angular code within src. In the sample presented here, I've used the names ng1 and ng2:

After this, move every generated file except the shown tsconfig.app.json, tsconfig.spec.json, favicon.ico and index.html into the ng2 folder.

To inform the CLI's build task about this new structure, adopt the file .angular-cli.json. Using the assets section in this file, we can also tell the CLI to directly copy the ng1 folder to the output directory:

{
  "$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 copy the whole AngularJS 1.x application into the ng1 folder but omit it's index.html. To make the existing application work with the adopted folder structure, we have to update it. This involves updating every reference to a template as well as references to JSON files and images.

After this, we can merge the existing index.html with the new one in the folder 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>

Note that this merged index.html contains references to the CSS files and scripts the AngularJS 1.x application needs. It also bootstraps the AngularJS 1.x application using ng-app and provides its shell containing a div with the ng-view directive. This is where the router activates the configured templates.

In this file we can also find the root element for our Angular application. References to the generated Angular bundles are not necessary as they are generated by the build task.

When this application is started (ng serve), it loads both applications independently of each other into the browser. To see this, navigate to http://localhost:4200:

As those two applications are bootstrapped separately, they are not able to communicated with each other or to exchange services and components. To make this work, we have to bootstrap them together as a hybrid application. The next section show how to do this.

Step 2: Bootstrap an AngularJS+Angular Hybrid Application

To bootstrap one application with both, AngularJS 1.x and Angular, we can leverage ngUpgrade which is part of Angular:

npm install @angular/upgrade --save

As we don't want to bootstrap the Angular (2/4/5 etc.) application on its own, we remove it's root component within the index.html:

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

Now, we can bootstrap both applications together. For this, import the UpgradeModule into the Angular application's AppModule. Remove the AppComponent from bootstrap in order to bootstrap the hybrid application 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 });
  }
}

As you can see here, this example bootstraps the hybrid application within ngDoBootstrap using the injected UpgradeModule. To prevent bootstrapping the AngularJS 1.x part twice, we have to remove the ng-app directive within the file index.html.

When we start the application after this, we just see the AngularJS 1.x components:

Nevertheless, it's a hybrid application with both versions of Angular. To proof this, the next section shows how to use an Angular component within the shown AngularJS component.

Step 3: Downgrading an Angular Component

To show how to use an Angular Component within our hybrid application's AngularJS context, this tutorial uses a very simple 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 source for the shown image can be found in scaffolded AppComponent.

To use this component within an AngularJS template, we have to downgrade it. For this, ngUpgrade provides the function downgradeComponent:

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 you see in this sample, the downgraded component is registered as a directive within the AngularJS 1.x module. For this, we can leverage the global angular variable. In order to tell TypeScript about this preexisting variable, we have to use the declare keyword.

After this, we can call the Angular Component within 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>

As usual in AngularJS, we have to make use of kebab case within HTML files while we have to register the directive with its normalized name the JavaScript counterpart. The latter one is using camel case.

When we reload the application, it shows the AngularJS 1.x phone list with our Angular demo component:

Perhaps you are wondering how this new Anglar Component can use the existing application logic provided by AngularJS 1.x services. Read the next section to get an answer.

Step 4: Upgrade a service

In order to use an existing AngularJS 1.x service within a new Angular Component, we have to upgrade it. According to the official docs, we have to create an Angular service provider which uses a factory. This factory gets a reference to the AngularJS 1.x injector ($injector) and uses it to obtain the service in question:

// 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']
}

Normally, we could use the service's type as the dependency injection token defined by the provide property. But in this case we explicitly decided to not upgrade the existing AngularJS 1.x code to TypeScript and so we don't have any type for it. Because of this, this sample uses a constant based Token called PHONE_SERVICE. For such tokens Angular 4+ provides the type InjectionToken. In Angular 2 we would use OpaqueToken instead. The InjectionToken takes a type parameter which identifies the type of the service it is pointing to. As mentioned, we don't have a type for this service and so we are just going with any.

The discussed service provider has to be registered with our Angular module:

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

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

[...]

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

After this, we can inject the phoneService into our Ng2DemoComponent and use it to load 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();
    }

}

As our token is just a constant, this sample uses the Inject decorator which points to it. After loading the phones, it just displays their count.

After reloading the application we see can this:

Please note that we have an AngularJS 1.x component with an Angular Component that is displaying data from an AngularJS 1.x service.

Instead of nesting AngularJS 1.x and Angular stuff, we also need the possibility to activate routes from both versions. The next sections deal with this.

Step 5: Routing to Angular Components

Making the AngularJS 1.x Router to activate Angular components is quite easy. For this, we just have to configure a route with a template that points to the component in question:

$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 to use the Angular Component alongside AngularJS routes which can base upon traditional controllers as well as directives and components.

I want to strike out, that this very approach works with the popular UI-Router too.

While this solution is very easy, it also comes with a drawback: We cannot leverage the new Angular Router for the newly written components. To make this possible, we can implement Victor Savkin's Sibling Outlet approach, which uses both routers together. To foundation for this is his Upgrade Shell pattern. The next two sections describe how to introduce those ideas into the case study presented here.

Step 6: Using Victor Savkin's Upgrade Shell pattern

The upgrade shell pattern has been introduced by Victor Savkin who one of the masterminds behind Angular. He describes it in his blog as well in his eBook about ngUpgrade. It envisages an Angular component on top of an hybrid application. This is the upgrade shell that contains both, AngularJS building blocks (directives, components, controllers) as well as Angular Components.

To implement this pattern we can use the AppComponent the CLI generated when we started the endeavor described in this article:

// 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 -->

Note that this Angular components contains the ng-view for the AngularJS 1.x router.

To make this component the top level component of our application, we have to directly bootstrap it. For this, we have to put it into 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 });
  }
*/
}

Please note, that we also have to remove the code to bootstrap the application manually. This code is moved into the AppComponent and does it's job 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']);
    }
}

Also, make sure that the index.html is just referencing our upgrade shell:

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

Reload the application and see the upgrade shell which contains the AngularJS 1.x application.

When this works, we have provided the foundation for the goal of the next section: Using both, the AngularJS 1.x as well the Angular router, alongside.

Step 7: Using Victor Savkin's Sibling Outlet approach to use both Routers

[Victor's sibling outlet approach]((https://blog.nrwl.io/upgrading-angular-applications-managing-routers-and-url-ca5588290aaa) describes a way to use routers of both Angular versions alongside. To implement it, we need to load the Angular router:

npm install @angular/router --save

After this, extend the app.component.html. It gets one outlet for each router version. For the AngularJS 1.x router it's a div with the ng-view directive. For the Angular Router it's 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 activating an AngularJS 1. based route, the first one gets a template; when activating an Angular route the latter one is used.

Now, let's configure the Angular router:

// 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 {
}

As you can see here, the configuration for the Angular router just defines two routes in this example. In addition to this, is is using the hash strategy for the sake of uniformity between the two versions.

We also have to make sure that the Angular router is doing nothing when a AngularJS 1.x route is activated. For this, Victor proposes the use of 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 has to be registered, e. g. with the AppModule:

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

After this, we have to modify the AngularJS 1.x routing configuration a bit. First of all, we have to remove the configured hash prefix because this would irritate the Angular router. In addition to this, we have to add a default route using otherwise that loads an empty template into the version-1-outlet when the route in question is handled by the other 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 above, everything shown using the AngularJS 1.x router also works the the popular UI-Router.

After this, just add some kind of menu to the AppComponent that allows to switch between the AngularJS 1.x based as well as Angular based routes:

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

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

After reloading the application we should have the possibility to switch between our routes:

 

 
Sie wollen mehr zum Thema Directly upgrading from AngularJS 1.x to Angular without preparing the existing Code Base wissen? Hier können Sie eine Anfrage für eine unverbindliche Schulung ode Beratung bzw. einen Workshop erstellen.
 
Unverbindliche Anfrage
 
 

Schulung und Beratung

Angular

Datenbindung, Formulare, Validierung, Routing, HTTP, Komponenten, ...

Details

Angular: Advanced

Erweiterte Aspekte von Angular

Details

Reaktive Architekturen mit Angular und Redux

Dieses interaktive Seminar vermittelt, wie Sie reaktive Anwendungen mit Angular entwickeln können.

Details

Migration von AngularJS 1.x auf Angular (2+)

Bestehende Projekte auf Angular 2 migrieren, ngUpgrade, ...

Details

Angular Review

Feedback und klärung offener Fragen, weiterführende Themen

Details

Angular Workshop

Start ohne Umwege

Details

Weitere Schulungen ...