Update in December 2016: Added some information like downgrading an component with bindings.

ngUpgrade, which ships with Angular 2+ (referred to simply as Angular from here on), enables the creation of hybrid applications that combine AngularJS 1.x and Angular 2+ services and components. This facilitates a gradual migration of an existing AngularJS 1.x application to Angular 2+. The primary drawback is that such an application must load both Angular versions simultaneously.

Luckily, starting with Angular 2.2.0, released in mid-November 2016, an ngUpgrade implementation supports ahead-of-time (AOT) compilation. As a result, the Angular portion of the bundle can be reduced to only the framework features actually used, thanks to tree shaking.

This post demonstrates how to leverage this capability using an example I prepared for ngEurope. The sample consists of several components and services written in both AngularJS 1.x and Angular:

Demo Application

Although a hybrid application is always bootstrapped as an AngularJS 1.x application, it can incorporate building blocks from both frameworks. To use an AngularJS 1.x building block (component or service) within Angular, it must be upgraded, meaning it receives a wrapper that makes it appear as an Angular component or service. Conversely, to use an Angular building block inside AngularJS 1.x, it must be downgraded, which also generates a wrapper to make it look like an AngularJS 1.x counterpart. The diagram below illustrates this: arrows indicate the direction of up- or downgrading for each building block.

Up- and Downgrading Components and Services

The full sample used here can be found at github.

Downgrading an Angular Component to AngularJS 1.x

To downgrade an Angular component to AngularJS 1.x, the sample leverages the new downgradeComponent method available in the @angular/upgrade/static module. The returned value of this method can be registered as a directive on an AngularJS 1.x module:

import { downgradeComponent } from '@angular/upgrade/static';

var app = angular.module('flight-app', [...]);

[...]

app.directive('flightSearch', <any>downgradeComponent({ component: FlightSearchComponent }));

Following that, the AngularJS 1.x portion of the hybrid app is free to use this directive. For instance, the sample does so within a template for a route:

$stateProvider
    [...]
    .state('flightBooking.flightSearch', {
        url: '/flight',
        template: '<flight-search></flight-search>'
    });

Since a downgraded component originates from Angular, it must still be declared within an Angular module.

@NgModule({
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        UpgradeModule
    ],
    declarations: [
        FlightSearchComponent
    ],
    entryComponents: [
        FlightSearchComponent
    ]
})
export class AppModule {
    ngDoBootstrap() {}
}

This module must import the UpgradeModule. Because it provides Angular building blocks for an app bootstrapped with AngularJS 1.x, it does not contain any root components itself. However, it still requires bootstrapping. To enable Angular to bootstrap a module without a top-level component, the module class must implement an ngDoBootstrap method. Crucially, the FlightSearchComponent must be designated as an entry component so that the Angular compiler generates the necessary files.

Downgrading an Angular Component with Bindings to AngularJS 1.x

When downgrading an Angular component that has bindings, the application must explicitly provide the names of its inputs and outputs:

app.directive('passengerCard', <any>downgradeComponent({
                                        component: PassengerCardComponent,
                                        inputs: ['item', 'selectedItem'],
                                        outputs: ['selectedItemChange']
                                }));

Furthermore, the component must be registered with the Angular module:

@NgModule({
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        UpgradeModule
    ],
    declarations: [
        FlightSearchComponent,
        PassengerCardComponent
    ],
    entryComponents: [
        FlightSearchComponent,
        PassengerCardComponent
    ]
})
export class AppModule {
    ngDoBootstrap() {}
}

To use such a downgraded component from AngularJS 1.x, the template must mark attributes that are used for property and event bindings. Typically, this is done with brackets and parentheses, as is customary in Angular templates, but the attribute names must be in kebab-case, following AngularJS 1.x conventions:

<div ng-repeat="p in $ctrl.passenger" class="col-sm-4" style="padding:20px;">
    <passenger-card
            [item]="p"
            [selected-item]="$ctrl.selectedPassenger"
            (selected-item-change)="$ctrl.selectedPassenger = $event">
    </passenger-card>
</div>

Downgrading an Angular Service to AngularJS 1.x

The process for downgrading an Angular service to AngularJS 1.x mirrors that of a component: the @angular/upgrade/static module provides the <code>@angular/upgrade/static</code>` method for this purpose. The result can be registered as an AngularJS 1.x factory:

import { downgradeInjectable } from '@angular/upgrade/static';

var app = angular.module('flight-app', [...]);

[...]

app.factory('passengerService', downgradeInjectable(PassengerService));

Later, the service can be injected into an AngularJS 1.x building block:

class PassengerSearchController {

    constructor(private passengerService: PassengerService) {
    }

    [...]
}

It is essential to remember that AngularJS 1.x relies on names, not types, for dependency injection. As such, the controller in the sample must name its constructor argument passengerService. The declared type is not relevant for DI resolution.

Because PassengerService is an Angular service, it needs to be registered with an Angular module as well:

@NgModule({
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        UpgradeModule
    ],
    declarations: [
        FlightSearchComponent
    ],
    entryComponents: [
        FlightSearchComponent
    ],
    providers: [
        PassengerService
    ]
})
export class AppModule {
    ngDoBootstrap() {}
}

Upgrading an AngularJS 1.x Component to Angular

Upgrading an AngularJS 1.x component is more involved. Here, the application must provide a wrapper for the upgraded component by extending UpgradeComponent:

import {UpgradeComponent} from "@angular/upgrade/static";
[...]

@Directive({selector: 'flight-card'})
export class FlightCard extends UpgradeComponent implements OnInit, OnChanges {

    @Input() item: Flight;
    @Input() selectedItem: Flight;
    @Output() selectedItemChange: EventEmitter<any>;

    constructor(elementRef: ElementRef, injector: Injector) {
        super('flightCard', elementRef, injector);
    }

    ngOnInit() { return super.ngOnInit(); }
    ngOnChanges(c) { return super.ngOnChanges(c); }

}

Unfortunately, this cannot be extracted into a convenience function, as doing so would prevent the compiler from locating the required metadata. The wrapper must declare an input for every inbound property of the AngularJS 1.x component and an output for each event. ngUpgrade will bridge these to the corresponding elements in the AngularJS 1.x component. To signal which AngularJS 1.x component to wrap, the constructor must pass its canonical name to the base constructor using super. Additionally, super requires an ElementRef and an Injector, which can be obtained via dependency injection.

The wrapper also needs to implement lifecycle hooks relevant to the AngularJS 1.x component. At a minimum, it must implement ngOnInit, because ngUpgrade uses reflection to find this method and instantiate the wrapped component. Simply delegating this method to the base implementation suffices. To enable data binding, the wrapper should also include an ngOnChanges method for the same reason.

Afterward, this wrapper can be registered with an Angular 2 module.

import {UpgradeModule} from "@angular/upgrade/static";

@NgModule({
    imports: [
        [...],
        UpgradeModule
    ],
    [...],
    declarations: [
        FlightSearchComponent,
        FlightCard // <-- Upgraded Component
    ],
    [...]
})
export class AppModule {
    ngDoBootstrap() {}
}

Once registered, the wrapper can be used in the templates of other Angular components:

<flight-card
        [item]="f"
        [selectedItem]="selectedFlight"
        (selectedItemChange)="selectedFlight = $event"></flight-card>

Upgrading an AngularJS 1.x Service to Angular

To upgrade an AngularJS 1.x service, the application must supply a function that takes an AngularJS 1.x injector and returns the service in question:

export function createFlightService(injector) {
    return injector.get('flightService');
}

This again cannot be abstracted into a generic helper, as doing so would hinder the AOT compiler from discovering the necessary metadata. To employ this function within the Angular 2 side of the hybrid, it is registered as a factory via a provider:

@NgModule({
    [...]
    providers: [
        PassengerService,
        {
            provide: FlightService,
            useFactory: createFlightService,
            deps: ['$injector']
        },
        [...]
    ]
})
export class AppModule {
    ngDoBootstrap() {}
}

With that in place, Angular 2 can inject the service into dependent components or services:

@Component({ [...] })
export class FlightSearchComponent {

    constructor(
        private flightService: FlightService, [...]) {
    }

    [...]
}

Bootstrapping

To bootstrap a hybrid application, the demo uses a bootstrap function "borrowed" from ngUpgrade's unit tests. This function initializes both the AngularJS 1.x and Angular 2 parts of the app:

import {PlatformRef, NgModuleFactory} from "@angular/core";
import {UpgradeModule} from "@angular/upgrade/static";
import {platformBrowser} from "@angular/platform-browser";
import {AppModuleNgFactory} from "../aot/app/app2.module.ngfactory";

// bootstrap function "borrowed" from the angular test cases
export function bootstrap(
    platform: PlatformRef, Ng2Module: NgModuleFactory<{}>, element: Element, ng1ModuleName: string) {
    // We bootstrap the Angular 2 module first; then when it is ready (async)
    // We bootstrap the Angular 1 module on the bootstrap element
    return platform.bootstrapModuleFactory(Ng2Module).then(ref => {
        let upgrade = ref.injector.get(UpgradeModule) as UpgradeModule;
        upgrade.bootstrap(element, [ng1ModuleName]);
        return upgrade;
    });
}

bootstrap(
    platformBrowser(),
    AppModuleNgFactory,
    document.body,
    'flight-app')
    .catch(err => console.error(err));

Please note, that AppModuleNgFactory is generated by the AOT Compiler. This is described in the next sections. Before this file has been generated, you could use null as a placeholder to avoid compilation errors.

A key point here is that the AngularJS 1.x module is bootstrapped through the UpgradeModule's bootstrap method. This serves as a replacement for ng-app or angular.bootstrap. The NgModuleFactory passed to it is produced by the AOT compiler when compiling the Angular 2 module.

AOT Compilation

To take advantage of the AOT compiler, the application should supply an (additional) tsconfig.json. In this sample, it is named tsconfig.aot.json. It includes an angularCompilerOptions property, which directs the compiler on where to place generated files.

{
    "compilerOptions": {
        "target": "es5",
        "module": "es2015",
        "moduleResolution": "node",
        "sourceMap": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "removeComments": false,
        "noImplicitAny": false,
        "suppressImplicitAnyIndexErrors": true,
        "typeRoots": [
            "typings/globals/"
        ]
    },

    "files": [
        "app/app2.module.ts"
    ],

    "angularCompilerOptions": {
        "genDir": "aot",
        "skipMetadataEmit" : true
    }
}

To permit tree shaking, which optimizes the Angular 2 bundle size, the module format es2015 is selected. For compilation and launching the sample, the package.json defines several scripts. The most crucial is ngc, which invokes the AOT compiler with the tsconfig.aot.json file:

[...]
"scripts": {
"webpack": "webpack",
"server": "live-server",
"start": "npm run server",
"ngc": "ngc -p tsconfig.aot.json",
"build": "npm run ngc && npm run webpack"
},
[...]

The sample also employs webpack for bundling, executed after the AOT compiler via the build script. To facilitate this, one must install the @angular/compiler-cli package. The start script uses live-server instead of webpack-dev-server, since the latter had incomplete support for AOT recompilation at the time of writing.

Build and Starting the Application

To build and run the demonstrated application, the following commands are used:

npm run build
npm start