George Kalpakas 93837e9545 feat(upgrade): support downgrading multiple modules (#26217)
Currently, calling `downgradeModule()` more than once is not supported.
If one wants to downgrade multiple Angular modules, they can create a
"super-module" that imports all the rest and downgrade that.

This commit adds support for downgrading multiple Angular modules. If
multiple modules are downgraded, then one must explicitly specify the
downgraded module that each downgraded component or injectable belongs
to, when calling `downgradeComponent()` and `downgradeInjectable()`
respectively.

No modification is needed (i.e. there is no need to specify a module for
downgraded components and injectables), if an app is not using
`downgradeModule()` or if there is only one downgraded Angular module.

Fixes #26062

PR Close #26217
2018-11-05 16:33:55 -08:00

220 lines
7.7 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// #docplaster
import {Component, Directive, ElementRef, EventEmitter, Inject, Injectable, Injector, Input, NgModule, Output, StaticProvider} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
// #docregion basic-how-to
// Alternatively, we could import and use an `NgModuleFactory` instead:
// import {MyLazyAngularModuleNgFactory} from './my-lazy-angular-module.ngfactory';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
// #enddocregion
/* tslint:disable: no-duplicate-imports */
import {UpgradeComponent} from '@angular/upgrade/static';
import {downgradeComponent} from '@angular/upgrade/static';
// #docregion basic-how-to
import {downgradeModule} from '@angular/upgrade/static';
// #enddocregion
/* tslint:enable: no-duplicate-imports */
declare var angular: ng.IAngularStatic;
interface Hero {
name: string;
description: string;
}
// This Angular service will use an "upgraded" AngularJS service.
@Injectable()
class HeroesService {
heroes: Hero[] = [
{name: 'superman', description: 'The man of steel'},
{name: 'wonder woman', description: 'Princess of the Amazons'},
{name: 'thor', description: 'The hammer-wielding god'}
];
constructor(@Inject('titleCase') titleCase: (v: string) => string) {
// Change all the hero names to title case, using the "upgraded" AngularJS service.
this.heroes.forEach((hero: Hero) => hero.name = titleCase(hero.name));
}
addHero() {
const newHero: Hero = {name: 'Kamala Khan', description: 'Epic shape-shifting healer'};
this.heroes = this.heroes.concat([newHero]);
return newHero;
}
removeHero(hero: Hero) { this.heroes = this.heroes.filter((item: Hero) => item !== hero); }
}
// This Angular component will be "downgraded" to be used in AngularJS.
@Component({
selector: 'ng2-heroes',
// This template uses the "upgraded" `ng1-hero` component
// (Note that because its element is compiled by Angular we must use camelCased attribute names.)
template: `
<div class="ng2-heroes">
<header><ng-content selector="h1"></ng-content></header>
<ng-content selector=".extra"></ng-content>
<div *ngFor="let hero of this.heroesService.heroes">
<ng1-hero [hero]="hero" (onRemove)="onRemoveHero(hero)">
<strong>Super Hero</strong>
</ng1-hero>
</div>
<button (click)="onAddHero()">Add Hero</button>
</div>
`,
})
class Ng2HeroesComponent {
@Output() private addHero = new EventEmitter<Hero>();
@Output() private removeHero = new EventEmitter<Hero>();
constructor(
@Inject('$rootScope') private $rootScope: ng.IRootScopeService,
public heroesService: HeroesService) {}
onAddHero() {
const newHero = this.heroesService.addHero();
this.addHero.emit(newHero);
// When a new instance of an "upgraded" component - such as `ng1Hero` - is created, we want to
// run a `$digest` to initialize its bindings. Here, the component will be created by `ngFor`
// asynchronously, thus we have to schedule the `$digest` to also happen asynchronously.
this.$rootScope.$applyAsync();
}
onRemoveHero(hero: Hero) {
this.heroesService.removeHero(hero);
this.removeHero.emit(hero);
}
}
// This Angular directive will act as an interface to the "upgraded" AngularJS component.
@Directive({selector: 'ng1-hero'})
class Ng1HeroComponentWrapper extends UpgradeComponent {
// The names of the input and output properties here must match the names of the
// `<` and `&` bindings in the AngularJS component that is being wrapped.
@Input() hero !: Hero;
@Output() onRemove !: EventEmitter<void>;
constructor(elementRef: ElementRef, injector: Injector) {
// We must pass the name of the directive as used by AngularJS to the super.
super('ng1Hero', elementRef, injector);
}
}
// This Angular module represents the Angular pieces of the application.
@NgModule({
imports: [BrowserModule],
declarations: [Ng2HeroesComponent, Ng1HeroComponentWrapper],
providers: [
HeroesService,
// Register an Angular provider whose value is the "upgraded" AngularJS service.
{provide: 'titleCase', useFactory: (i: any) => i.get('titleCase'), deps: ['$injector']}
],
// All components that are to be "downgraded" must be declared as `entryComponents`.
entryComponents: [Ng2HeroesComponent]
// Note that there are no `bootstrap` components, since the "downgraded" component
// will be instantiated by ngUpgrade.
})
class MyLazyAngularModule {
// Empty placeholder method to prevent the `Compiler` from complaining.
ngDoBootstrap() {}
}
// #docregion basic-how-to
// The function that will bootstrap the Angular module (when/if necessary).
// (This would be omitted if we provided an `NgModuleFactory` directly.)
const ng2BootstrapFn = (extraProviders: StaticProvider[]) =>
platformBrowserDynamic(extraProviders).bootstrapModule(MyLazyAngularModule);
// #enddocregion
// (We are using the dynamic browser platform, as this example has not been compiled AoT.)
// #docregion basic-how-to
// This AngularJS module represents the AngularJS pieces of the application.
const myMainAngularJsModule = angular.module('myMainAngularJsModule', [
// We declare a dependency on the "downgraded" Angular module.
downgradeModule(ng2BootstrapFn)
// or
// downgradeModule(MyLazyAngularModuleFactory)
]);
// #enddocregion
// This AngularJS component will be "upgraded" to be used in Angular.
myMainAngularJsModule.component('ng1Hero', {
bindings: {hero: '<', onRemove: '&'},
transclude: true,
template: `
<div class="ng1-hero">
<div class="title" ng-transclude></div>
<h2>{{ $ctrl.hero.name }}</h2>
<p>{{ $ctrl.hero.description }}</p>
<button ng-click="$ctrl.onRemove()">Remove</button>
</div>
`
});
// This AngularJS service will be "upgraded" to be used in Angular.
myMainAngularJsModule.factory(
'titleCase', () => (value: string) => value.replace(/(^|\s)[a-z]/g, m => m.toUpperCase()));
// This directive will act as the interface to the "downgraded" Angular component.
myMainAngularJsModule.directive(
'ng2Heroes', downgradeComponent({
component: Ng2HeroesComponent,
// Optionally, disable `$digest` propagation to avoid unnecessary change detection.
// (Change detection is still run when the inputs of a "downgraded" component change.)
propagateDigest: false
}));
// This is our top level application component.
myMainAngularJsModule.component('exampleApp', {
// This template makes use of the "downgraded" `ng2-heroes` component,
// but loads it lazily only when/if the user clicks the button.
// (Note that because its element is compiled by AngularJS,
// we must use kebab-case attributes for inputs and outputs.)
template: `
<link rel="stylesheet" href="./styles.css">
<button ng-click="$ctrl.toggleHeroes()">{{ $ctrl.toggleBtnText() }}</button>
<ng2-heroes
ng-if="$ctrl.showHeroes"
(add-hero)="$ctrl.setStatusMessage('Added hero ' + $event.name)"
(remove-hero)="$ctrl.setStatusMessage('Removed hero ' + $event.name)">
<h1>Heroes</h1>
<p class="extra">Status: {{ $ctrl.statusMessage }}</p>
</ng2-heroes>
`,
controller: function() {
this.showHeroes = false;
this.statusMessage = 'Ready';
this.setStatusMessage = (msg: string) => this.statusMessage = msg;
this.toggleHeroes = () => this.showHeroes = !this.showHeroes;
this.toggleBtnText = () => `${this.showHeroes ? 'Hide' : 'Show'} heroes`;
}
});
// We bootstrap the Angular module as we would do in a normal Angular app.
angular.bootstrap(document.body, [myMainAngularJsModule.name]);