Hybrid Angular 1 and 2 apps with "ngUpgrade"

The Angular core team provides a module called "ngUpgrade" which gives us the power to create a hybrid application - one in which we use both Angular 1 and Angular 2 together.

This is a very natural migration-step for large Angular 1 apps, because it allows us to mix and match Components and Providers from the two frameworks.

ng-metadata both supports @angular/upgrade and @angular/upgrade/static and enhances it with some additional methods designed to help us take advantage of other ng-metadata features.

There are 2 ways how to proceed with hybrid/upgrade process:

AOT upgrade/downgrade helpers

before reading this, we highly recommend to read angular 2 cookbook so you now what is done under the hood

Creating the Angular 2 Root module

we need to Create an app.module.ng2.ts file and add the following NgModule class:

// app.module.ng2.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ]
})
export class AppModule {
  ngDoBootstrap() {}
}

next we need to export traditional Angular1Module instead of ngMetadata @NgModule from our root app.module.ts

import { 
  NgModule,
+ bundle
} from 'ng-metadata/core';

import { AppComponent } from './app.component';
import { FooComponent } from './components/foo.component';
import { HeroesService } from './heroes/heroes.service';

@NgModule( {
  declarations: [
    AppComponent,
    FooComponent,
  ],
  providers: [
    HeroesService,
  ]
} )
- export class AppModule {
- }
+ class AppModule {
+ }

+ export const Ng1AppModule = bundle(AppModule);

FINAL CODE

// app.module
import { 
  NgModule,
  bundle
} from 'ng-metadata/core';

import { AppComponent } from './app.component';
import { FooComponent } from './components/foo.component';
import { HeroesService } from './heroes/heroes.service';

@NgModule( {
  declarations: [
    AppComponent,
    FooComponent,
  ],
  providers: [
    HeroesService,
  ]
} )
class AppModule {
}

export const Ng1AppModule = bundle(AppModule);

Bootstrapping our hybrid app

Now we bootstrap AppModule from app.module.ng2.ts using platformBrowserDynamic's bootstrapModule method. Then we use dependency injection to get a hold of the UpgradeModule instance in AppModule, and use it to bootstrap our Angular 1 app. The upgrade.bootstrap method takes the exact same arguments as angular.bootstrap

We will no longer use ng-metadata to bootstrap our app:

// main.ts
- import { platformBrowserDynamic } from 'ng-metadata/platform-browser-dynamic';
+ import { UpgradeModule } from '@angular/upgrade/static/';
+ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
- import { AppModule } from './app.module'; // ng-metadata NgModule
+ import { Ng1AppModule } from './app.module'; // ng-metadata NgModule
+ import { AppModule } from './app/app.module.ng2';

- platformBrowserDynamic().bootstrapModule(AppModule);
+ platformBrowserDynamic().bootstrapModule(AppModule)
+  .then(platformRef => {
+    const upgrade = platformRef.injector.get(UpgradeModule) as UpgradeModule;
+
+    upgrade.bootstrap(document.body, [Ng1AppModule.name], {strictDi: true});
+ });

FINAL CODE

// main.ts
import { UpgradeModule } from '@angular/upgrade/static/';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { Ng1AppModule } from './app.module'; // ng-metadata NgModule
import { AppModule } from './app/app.module.ng2';

platformBrowserDynamic().bootstrapModule(AppModule)
 .then(platformRef => {
   const upgrade = platformRef.injector.get(UpgradeModule) as UpgradeModule;

   upgrade.bootstrap(document.body, [Ng1AppModule.name], {strictDi: true});
});

now we can proceed with upgrade/downgrade our components/services

Upgrading an Angular 1 Component and downgrading it back to ng1

ng-metadata is a project designed to help us write our Angular 1 Components just like Angular 2 Components, so "upgrading" them with the upgradeAdapter as an interim migration step doesn't really make sense.

In a hybrid Angular 1 and 2 app, it is actually really easy for us to just change a couple of things about our ng-metadata Component to make it a fully-fledged Angular 2 Component and then downgrade it for use in our hybrid app.

Here is an example of an ng-metadata Angular 1 Component which just renders its input:

// ./components/foo.component
import { Component, Input } from 'ng-metadata/core';

@Component({
  selector: 'my-foo',
  template: '<h1>Foo! {{ $ctrl.myInput }}</h1>',
})
export class FooComponent {
  @Input() myInput: string;
}

To update this Component to a fully fledged Angular 2 Component, all we need to do is change the import path of our decorators and make sure our template syntax is correct.

In this case, the template only needs to have the $ctrl reference removed - in Angular 2 the myInput property is available directly.

Upgraded Angular 1 Component:

- import { Component, Input } from 'ng-metadata/core';
+ import { Component, Input } from '@angular/core';

@Component({
  selector: 'foo',
- template: '<h1>Foo! {{ $ctrl.myInput }}</h1>',  
+ template: '<h1>Foo! {{ myInput }}</h1>',
})
export class FooComponent {
  @Input() myInput: string;
}

Now we need to downgrade the Component using one of the two functions outlined in the next section. We also need to register it within Angular 2 and ngMetadata @NgModule.

Registering to NgModule

we need to register upgraded Component to Angular 2

// app.module.ng2.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
+ import { FooComponent } from './components/foo.component';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
+  declarations: [
+    FooComponent
+  ]
})
export class AppModule {
  ngDoBootstrap() {}
}

and downgrade it back to Angular 1 and register it within ng-metadata @NgModule

// app.module
import { 
  NgModule,
  bundle
} from 'ng-metadata/core';
+ import { provideNg2Component } from 'ng-metadata/upgrade';
+ import { downgradeComponent } from '@angular/upgrade';

import { AppComponent } from './app.component';
import { FooComponent } from './components/foo.component';
import { HeroesService } from './heroes/heroes.service';

@NgModule( {
  declarations: [
    AppComponent,
-    FooComponent,    
+    provideNg2Component({component:FooComponent,downgradeFn:downgradeComponent}),
  ],
  providers: [
    HeroesService,
  ]
} )
class AppModule {
}

export const Ng1AppModule = bundle(AppModule);

Whole downgrade process and registration is handled by provideNg2Component ng-metadata function ( yes also @Inputs and @Outputs)

That's it! DONE!

We also provide helpers, if you are not using ng-metadata @NgModule for registration to angular 1 module

Please see API docs for further docs

Upgrading an Angular 1 Service and downgrading it back to ng1

We have 2 options here:

  • upgrade existing service to Angular 2 via Angular 2 @NgModule.providers and register it via ng-metadata upgradeInjectable helper
  • upgrade the service to Angular 2 physically ( by changing import paths ) and downgrading it back to Angular 1 ng-metadata NgModule via provideNg2Injectable

This is our initial ng-metadata Angular 1 service:

// ./heroes/heroes.service
import { Injectable } from 'ng-metadata/core'
import { Hero } from './hero';

@Injectable()
export class HeroesService {
  get() {
    return [
      new Hero(1, 'Windstorm'),
      new Hero(2, 'Spiderman'),
    ];
  }
}

upgradeInjectable method

let's upgrade it to Angular 2

// app.module.ng2.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
import { FooComponent } from './components/foo.component';
+ import { HeroesService } from '/heroes/heroes.service';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  declarations: [
    FooComponent
  ],
+  providers: [
+    provideNg1Injectable(HeroesService),
+  ]
})
export class AppModule {
  ngDoBootstrap() {}
}

done! we can now inject our Angular 1 service within Angular 2 entities, for example here is a ng2 component:

import { Component, Inject } from '@angular/core';
import { HeroesService } from '/heroes/heroes.service';

@Component({
 selector: 'my-hero',
 template: `<h1>My Hero</h1>`,
})
class HeroComponent {
 constructor(
   @Inject('$routeParams') private $routeParams: any, // by name using @Inject
   private heroesSvc: HeroesService // by type using ngMetadata @Injectable service class
 ) {}
}

provideNg2Injectable method

We can migrate it directly to Angular 2 (if it doesn't has any Angular 1 injections) by changing path imports just like we did with Component

Here is the result:

// ./heroes/heroes.service
- import { Injectable } from 'ng-metadata/core'
+ import { Injectable } from '@angular/core'
import { Hero } from './hero';

@Injectable()
export class HeroesService {
  get() {
    return [
      new Hero(1, 'Windstorm'),
      new Hero(2, 'Spiderman'),
    ];
  }
}

now we need to register it to Angular 2 @NgModule

// app.module.ng2.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
import { FooComponent } from './components/foo.component';
+ import { HeroesService } from '/heroes/heroes.service';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  declarations: [
    FooComponent
  ],
+  providers: [
+    HeroesService,
+  ]
})
export class AppModule {
  ngDoBootstrap() {}
}

and downgrade it to Angular 1 ng-metadata @NgModule.providers

// app.module
import { 
  NgModule,
  bundle
} from 'ng-metadata/core';
+ import { provideNg2Component } from 'ng-metadata/upgrade';
+ import { downgradeComponent } from '@angular/upgrade/static';

import { AppComponent } from './app.component';
import { FooComponent } from './components/foo.component';
- import { HeroesService } from './heroes/heroes.service';
+ import { HeroesService, HeroesServiceToken } from './heroes/heroes.service';

@NgModule( {
  declarations: [
    AppComponent,    
    provideNg2Component({component:FooComponent,downgradeFn:downgradeComponent}),
  ],
  providers: [
-    HeroesService,
+    provideNg2Injectable({token:HeroesServiceToken, injectable:HeroesService, downgradeFn: downgradeInjectable}),
  ]
} )
class AppModule {
}

export const Ng1AppModule = bundle(AppModule);

Note that we had to create ng-metadata OpaqueToken instance within heroes.service.ts so we have a Injectable reference within our Angular 1 app. With this we need to change injection type in all Angular 1 ng-metadata entities, because ng1 cannot inject by Class type

So we need to make changes like this:

- constructor(private heroesSvc: HeroesService){}
+ constructor(@Inject(HeroesServiceToken) private heroesSvc: HeroesService){}

I don't know about you, but for me this is not very productive because we need to do even more refactoring.

DON'T YOU WORRY THERE IS BETTER A WAY ;)

First we need to annotate our upgraded ng2 service with ng-metadata @Injectable:

// ./heroes/heroes.service
- import { Injectable } from 'ng-metadata/core'
+ import { Injectable as NgMetadataInjectable } from 'ng-metadata/core'
+ import { Injectable } from '@angular/core'
import { Hero } from './hero';

+ @NgMetadataInjectable()
@Injectable()
export class HeroesService {
  get() {
    return [
      new Hero(1, 'Windstorm'),
      new Hero(2, 'Spiderman'),
    ];
  }
}

then register it to Angular 2 @NgModule as previously

// app.module.ng2.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
import { FooComponent } from './components/foo.component';
+ import { HeroesService } from '/heroes/heroes.service';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  declarations: [
    FooComponent
  ],
+  providers: [
+    HeroesService,
+  ]
})
export class AppModule {
  ngDoBootstrap() {}
}

and downgrade it to Angular 1 ng-metadata @NgModule without token property !

JUST LIKE THIS:

// app.module
import { 
 NgModule,
 bundle
} from 'ng-metadata/core';
+ import { provideNg2Component } from 'ng-metadata/upgrade';
+ import { downgradeComponent } from '@angular/upgrade/static';

import { AppComponent } from './app.component';
import { FooComponent } from './components/foo.component';
import { HeroesService } from './heroes/heroes.service';

@NgModule( {
 declarations: [
   AppComponent,    
   provideNg2Component({component:FooComponent,downgradeFn:downgradeComponent}),
 ],
 providers: [
-    HeroesService,
+    provideNg2Injectable({injectable:HeroesService, downgradeFn: downgradeInjectable}),
 ]
} )
class AppModule {
}

export const Ng1AppModule = bundle(AppModule);

With this you don't need to change DI Injection within your app. I call this a WIN WIN !

We also provide helpers, if you are not using ng-metadata @NgModule for registration to angular 1 module Please see API docs for further docs


Singleton upgradeAdapter ( Deprecated )

Creating the upgradeAdapter singleton

Just as outlined in the Angular 2 docs, we want to create a single instance of the UpgradeAdapter class and use that everywhere in our application.

In the root of our project we create upgrade-adapter.ts, which will export the singleton and be referenced later.

We first instantiate the @angular/upgrade UpdateAdapter using an Angular 2 NgModule (NOTE: not an ng-metadata NgModule).

Then, in order to create our "supercharged" upgradeAdapter singleton, we pass the instantiated @angular/upgrade UpdateAdapter to the NgMetadataUpgradeAdapter constructor.

Our file should now look like this:

upgrade-adapter.ts:

import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { UpgradeAdapter } from '@angular/upgrade'
import { NgMetadataUpgradeAdapter } from 'ng-metadata/upgrade'

// Angular 2 NgModule (not ng-metadata NgModule)
@NgModule({
    imports: [BrowserModule], // required Angular 2 BrowserModule
})
class UpgradeModule {}

const instantiatedAdapter = new UpgradeAdapter(UpgradeModule)

// Export the "supercharged" ng-metadata upgradeAdapter singleton
export const upgradeAdapter = new NgMetadataUpgradeAdapter(instantiatedAdapter)

Bootstrapping our hybrid app

In order for the dependency injection and change detection systems of both frameworks to work harmoniously together, we need to update the bootstrap process of our app to use the upgradeAdapter singleton.

This is simply a case of importing it and using its bootstrap method instead of the bootstrap function created via platformBrowserDynamic() from ng-metadata/platform-browser-dynamic.

BEFORE: main.ts

import { platformBrowserDynamic } from 'ng-metadata/platform-browser-dynamic';
import { AppModule } from './app.module'; // ng-metadata NgModule
// ...other imports
// ...etc

platformBrowserDynamic().bootstrapModule(AppModule);

AFTER: main.ts

import { upgradeAdapter } from './upgrade-adapter';
import { AppModule } from './app.module.ts'; // ng-metadata NgModule
// ...other imports
// ...etc

upgradeAdapter.bootstrap( AppModule );

Upgrading an Angular 1 Component

ng-metadata is a project designed to help us write our Angular 1 Components just like Angular 2 Components, so "upgrading" them with the upgradeAdapter as an interim migration step doesn't really make sense.

In a hybrid Angular 1 and 2 app, it is actually really easy for us to just change a couple of things about our ng-metadata Component to make it a fully-fledged Angular 2 Component and then downgrade it for use in our hybrid app.

Here is an example of an ng-metadata Angular 1 Component which just renders its input:

import { Component, Input } from 'ng-metadata/core';

@Component({
  selector: 'foo',
  template: '<h1>Foo! {{ $ctrl.myInput }}</h1>',
})
export class FooComponent {

  @Input() myInput: string;

}

To update this Component to a fully fledged Angular 2 Component, all we need to do is change the import path of our decorators and make sure our template syntax is correct.

In this case, the template only needs to have the $ctrl reference removed - in Angular 2 the myInput property is available directly.

Angular 2 version of the Component above:

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

@Component({
  selector: 'foo',
  template: '<h1>Foo! {{ myInput }}</h1>',
})
export class FooComponent {

  @Input() myInput: string;

}

We can now downgrade the Component using one of the two methods outlined in the next section.

Downgrading an Angular 2 Component

When we start creating "native" Angular 2 Components in our hybrid application, we will need to downgrade them before we can register them as directives.

As you can see in this example Component, there is nothing but pure Angular 2 here:

ng2.component.ts

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

@Component({
  selector: 'angular-two',
  template: `<h1>Hello, ng2!</h1>`,
})
export class Ng2Component {

  constructor() {
    console.log( 'Woop!' );
  }

}

ng-metadata offers two ways for us to downgrade our Ng2Component for use in our hybrid app...

1) upgradeAdapter.downgradeNg2Component()

If we want to manually register the downgraded Component as a directive on an Angular 1 module, we can do that in a very similar way to how we have traditionally used ng-metadata's provide() function for Angular 1 Components.

As with provide(), ng-metadata will infer the name of the compiled directive from the Component selector:

import * as angular from 'angular';

// Our upgradeAdapter singleton
import { upgradeAdapter } from '../upgrade-adapter';

// Our example Angular 2 Component
import { Ng2Component } from './ng2.component.ts';

// Our old Angular 1 Module
export const FooModule = angular.module( 'foo', [] )

  // The classic `provide()` helper from ng-metadata would look
  // like this for an Angular 1 Component.
  // .directive( ...provide( SomeNg1Component ) )
  //
  // Just like `provide()`, ng-metadata's `downgradeNg2Component()`
  // helper removes the need for us to manually set a string for
  // the name of the compiled Angular 1 directive.
  //
  // In this example the directive name will be set to "angularTwo"
  // (from the Component selector "angular-two" above).
  .directive( ...upgradeAdapter.downgradeNg2Component( Ng2Component ) );

2) upgradeAdapter.provideNg2Component()

In order to make our ng-metadata apps match as closely to Angular 2 apps as is reasonable, we want to avoid dealing with Angular 1 modules directly in our code.

As of ng-metadata 3.0, we can let ng-metadata deal with creating the Angular 1 modules behind the scenes, and register our directives and providers directly onto parent NgModules.

For this reason, ng-metadata's upgradeAdapter also offers a helper function for registering a downgraded Angular 2 Component directly on an ng-metadata NgModule's declarations array called provideNg2Component().

import { NgModule } from 'ng-metadata/core';

// Our upgradeAdapter singleton
import { upgradeAdapter } from '../upgrade-adapter';

// Our example Angular 2 Component
import { Ng2Component } from './ng2.component.ts';

@NgModule({
  declarations: [
    // Ng1Component,
    // SomeOtherNg1Component,
    // ...etc...
    upgradeAdapter.provideNg2Component( Ng2Component ),
  ],
})
export class SomeParentModule {}

Upgrading an Angular 1 Provider

If we need to use an Angular 1 Provider within Angular 2 Components and Providers during our migration phase, we can use upgradeAdapter.upgradeNg1Provider().

When using the upgraded Provider for dependency injection, either the name string can be used with @Inject, or a given token can be injected by type.

Here is an example of each variant in action:

// Somewhere in your app, the providers are upgraded,
// either as a string or as a given token
// E.g.

class $state {}

upgradeAdapter.upgradeNg1Provider('$state', { asToken: $state })
upgradeAdapter.upgradeNg1Provider('$rootScope')

ng2.component.ts

import { Component, Inject } from '@angular/core';
import { $state } from '../some-file.ts'

@Component({
 selector: 'ng2',
 template: `<h1>Ng2</h1>`,
})
class Ng2Component {

 constructor(
   @Inject('$rootScope') private $rootScope: any, // by name using @Inject
   private $state: $state // by type using the user defined token
 ) {}

}

Downgrading an Angular 2 Provider

Naturally, we might also want to inject Angular 2 Providers into the Angular 1 Components in our hybrid app.

Just like with downgrading Angular 2 Components (as described above), ng-metadata offers two ways for us to downgrade our Angular 2 Providers and register their compiled factory functions...

1) upgradeAdapter.downgradeNg2Provider()

The downgradeNg2Provider() helper function works in a similar way to the downgradeNg2Component() function. In the case of Providers, however, there is no metadata to use to infer the name from, so we need to provide a string or an OpaqueToken for this purpose.

We can see an example of each method here:

import * as angular from 'angular';

// An Angular 2 Provider
import { Ng2Service } from './ng2.service.ts';

const otherServiceToken = new OpaqueToken( 'otherService' )

// Our old Angular 1 Module
export const FooModule = angular.module( 'foo', [] )

  // Using a string for the name
  .factory( ...upgradeAdapter.downgradeNg2Provider( 'ng2Service', { useClass: Ng2Service }) )

  // Using an OpaqueToken for the name
  .factory( ...upgradeAdapter.downgradeNg2Provider( otherServiceToken, { useClass: Ng2Service }) )

2) upgradeAdapter.provideNg2Provider()

The alternative to directly interacting with an Angular 1 module (not recommended) for the purposes of registering Provider, is to make use of the providers array on an NgModule.

Just like with downgrading and registering Angular 2 Components, ng-metadata offers us a helper for this called provideNg2Provider().

It takes the same arguments as downgradeNg2Provider(), as we can see in the example below:

import { Component } from 'ng-metadata/core';

// Our upgradeAdapter singleton
import { upgradeAdapter } from '../upgrade-adapter';

// An Angular 2 Provider
import { Ng2Service } from './ng2.service.ts';

const otherServiceToken = new OpaqueToken( 'otherService' );

@Component({
  selector: 'foo',
  template: '<h1>Foo!</h1>',
  providers: [

    // Using a string for the name
    upgradeAdapter.provideNg2Provider( 'ng2Service', { useClass: Ng2Service } ),

    // Using an OpaqueToken for the name
    upgradeAdapter.provideNg2Provider( otherServiceToken, { useClass: Ng2Service } ),

  ],
})
export class FooComponent {

  constructor() {
    console.log( 'No more angular.module!' );
  }

}

NOTE: Using downgraded Angular 2 Providers in other Angular 2 Components/Providers

If we want to also use our downgraded Angular 2 Providers in other Angular 2 Providers or Components, we need to additionally add it as a Provider to the Angular 2 NgModule that we pass into the @angular/upgrade UpgradeAdapter in upgrade-adapter.ts.

results matching ""

    No results matching ""