Skip to content

Commit

Permalink
feat(upgrade): support downgrading multiple modules
Browse files Browse the repository at this point in the history
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 angular#26062
  • Loading branch information
gkalpak committed Oct 11, 2018
1 parent c918c21 commit e799782
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 19 deletions.
18 changes: 12 additions & 6 deletions aio/content/guide/upgrade-performance.md
Expand Up @@ -281,22 +281,28 @@ The differences between `downgradeModule()` and `UpgradeModule` end here. The re
`upgrade/static` APIs and concepts work in the exact same way for both types of hybrid apps.
See [Upgrading from AngularJS](guide/upgrade) to learn about:

- [Using Angular Components from AngularJS Code](guide/upgrade#using-angular-components-from-angularjs-code).
- [Using Angular Components from AngularJS Code](guide/upgrade#using-angular-components-from-angularjs-code).<br />
_NOTE: If you are downgrading multiple modules, you need to specify the name of the downgraded
module each component belongs to, when calling `downgradeComponent()`._
- [Using AngularJS Component Directives from Angular Code](guide/upgrade#using-angularjs-component-directives-from-angular-code).
- [Projecting AngularJS Content into Angular Components](guide/upgrade#projecting-angularjs-content-into-angular-components).
- [Transcluding Angular Content into AngularJS Component Directives](guide/upgrade#transcluding-angular-content-into-angularjs-component-directives).
- [Making AngularJS Dependencies Injectable to Angular](guide/upgrade#making-angularjs-dependencies-injectable-to-angular).
- [Making Angular Dependencies Injectable to AngularJS](guide/upgrade#making-angular-dependencies-injectable-to-angularjs).
- [Making Angular Dependencies Injectable to AngularJS](guide/upgrade#making-angular-dependencies-injectable-to-angularjs).<br />
_NOTE: If you are downgrading multiple modules, you need to specify the name of the downgraded
module each injectable belongs to, when calling `downgradeInjectable()`._

<div class="alert is-important">

While it is possible to downgrade injectables, downgraded injectables will not be available until
the Angular module is instantiated. In order to be safe, you need to ensure that the downgraded
injectables are not used anywhere _outside_ the part of the app that is controlled by Angular.
the Angular module that provides them is instantiated. In order to be safe, you need to ensure
that the downgraded injectables are not used anywhere _outside_ the part of the app where it is
guaranteed that their module has been instantiated.

For example, it is _OK_ to use a downgraded service in an upgraded component that is only used
from Angular components, but it is _not OK_ to use it in an AngularJS component that may be used
independently of Angular.
from a downgraded Angular component provided by the same Angular module as the injectable, but it
is _not OK_ to use it in an AngularJS component that may be used independently of Angular or use
it in a downgraded Angular component from a different module.

</div>

Expand Down
@@ -0,0 +1,28 @@
/**
* @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
*/

import {browser, by, element} from 'protractor';

import {verifyNoBrowserErrors} from '../../../../../_common/e2e_util';


describe('upgrade/static (lite with multiple downgraded modules)', () => {
const navButtons = element.all(by.css('nav button'));
const mainContent = element(by.css('main'));

beforeEach(() => browser.get('/upgrade/static/ts/lite-multi/'));
afterEach(verifyNoBrowserErrors);

it('should correctly bootstrap multiple downgraded modules', () => {
navButtons.get(1).click();
expect(mainContent.getText()).toBe('Component B');

navButtons.get(0).click();
expect(mainContent.getText()).toBe('Component A | ng1(ng2)');
});
});
118 changes: 118 additions & 0 deletions packages/examples/upgrade/static/ts/lite-multi/module.ts
@@ -0,0 +1,118 @@
/**
* @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, Injectable, Injector, NgModule, StaticProvider, getPlatform} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeComponent, downgradeComponent, downgradeInjectable, downgradeModule} from '@angular/upgrade/static';


declare var angular: ng.IAngularStatic;

// An Angular module that declares an Angular service and a component,
// which in turn uses an upgraded AngularJS component.
@Component({
selector: 'ng2A',
template: 'Component A | <ng1A></ng1A>',
})
export class Ng2AComponent {
}

@Directive({
selector: 'ng1A',
})
export class Ng1AComponentFacade extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) { super('ng1A', elementRef, injector); }
}

@Injectable()
export class Ng2AService {
getValue() { return 'ng2'; }
}

@NgModule({
imports: [BrowserModule],
providers: [Ng2AService],
declarations: [Ng1AComponentFacade, Ng2AComponent],
entryComponents: [Ng2AComponent],
})
export class Ng2AModule {
ngDoBootstrap() {}
}


// Another Angular module that declares an Angular component.
@Component({
selector: 'ng2B',
template: 'Component B',
})
export class Ng2BComponent {
}

@NgModule({
imports: [BrowserModule],
declarations: [Ng2BComponent],
entryComponents: [Ng2BComponent],
})
export class Ng2BModule {
ngDoBootstrap() {}
}


// The downgraded Angular modules.
const downgradedNg2AModule = downgradeModule(
(extraProviders: StaticProvider[]) =>
(getPlatform() || platformBrowserDynamic(extraProviders)).bootstrapModule(Ng2AModule));

const downgradedNg2BModule = downgradeModule(
(extraProviders: StaticProvider[]) =>
(getPlatform() || platformBrowserDynamic(extraProviders)).bootstrapModule(Ng2BModule));


// The AngularJS app including downgraded modules, components and injectables.
const appModule =
angular.module('exampleAppModule', [downgradedNg2AModule, downgradedNg2BModule])
.component('exampleApp', {
template: `
<nav>
<button ng-click="$ctrl.page = page" ng-repeat="page in ['A', 'B']">
Page {{ page }}
</button>
</nav>
<hr />
<main ng-switch="$ctrl.page">
<ng2-a ng-switch-when="A"></ng2-a>
<ng2-b ng-switch-when="B"></ng2-b>
</main>
`,
controller: class ExampleAppController{page = 'A';},
})
.component('ng1A', {
template: 'ng1({{ $ctrl.value }})',
controller: [
'ng2AService', class Ng1AController{
value = this.ng2AService.getValue(); constructor(private ng2AService: Ng2AService) {}
}
],
})
.directive('ng2A', downgradeComponent({
component: Ng2AComponent,
downgradedModule: downgradedNg2AModule,
propagateDigest: false,
}))
.directive('ng2B', downgradeComponent({
component: Ng2BComponent,
downgradedModule: downgradedNg2BModule,
propagateDigest: false,
}))
.factory('ng2AService', downgradeInjectable(Ng2AService, downgradedNg2AModule));


// Bootstrap the AngularJS app.
angular.bootstrap(document.body, [appModule.name]);
1 change: 0 additions & 1 deletion packages/examples/upgrade/static/ts/lite/module.ts
Expand Up @@ -17,7 +17,6 @@ import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
/* tslint:disable: no-duplicate-imports */
import {UpgradeComponent} from '@angular/upgrade/static';
import {downgradeComponent} from '@angular/upgrade/static';
import {downgradeInjectable} from '@angular/upgrade/static';
// #docregion basic-how-to
import {downgradeModule} from '@angular/upgrade/static';
// #enddocregion
Expand Down
16 changes: 13 additions & 3 deletions packages/upgrade/src/common/downgrade_component.ts
Expand Up @@ -46,8 +46,14 @@ interface Thenable<T> {
*
* @param info contains information about the Component that is being downgraded:
*
* * `component: Type<any>`: The type of the Component that will be downgraded
* * `propagateDigest?: boolean`: Whether to perform {@link ChangeDetectorRef#detectChanges
* - `component: Type<any>`: The type of the Component that will be downgraded
* - `downgradedModule?: string`: The name of the downgraded module (if any) that the component
* "belongs to", as returned by a call to `downgradeModule()`. It is the module, whose
* corresponding Angular module will be bootstrapped, when the component needs to be instantiated.
* <br />
* (This option is only necessary when using `downgradeModule()` to downgrade more than one
* Angular module.)
* - `propagateDigest?: boolean`: Whether to perform {@link ChangeDetectorRef#detectChanges
* change detection} on the component on every
* [$digest](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest). If set to `false`,
* change detection will still be performed when any of the component's inputs changes.
Expand All @@ -61,6 +67,8 @@ interface Thenable<T> {
export function downgradeComponent(info: {
component: Type<any>;
/** @experimental */
downgradedModule?: string;
/** @experimental */
propagateDigest?: boolean;
/** @deprecated since v4. This parameter is no longer used */
inputs?: string[];
Expand Down Expand Up @@ -98,7 +106,9 @@ export function downgradeComponent(info: {
let ranAsync = false;

if (!parentInjector) {
const lazyModuleRef = $injector.get(LAZY_MODULE_REF) as LazyModuleRef;
const downgradedModule = info.downgradedModule || '';
const lazyModuleRefKey = `${LAZY_MODULE_REF}${downgradedModule}`;
const lazyModuleRef = $injector.get(lazyModuleRefKey) as LazyModuleRef;
needsNgZone = lazyModuleRef.needsNgZone;
parentInjector = lazyModuleRef.injector || lazyModuleRef.promise as Promise<Injector>;
}
Expand Down
23 changes: 21 additions & 2 deletions packages/upgrade/src/common/downgrade_injectable.ts
Expand Up @@ -43,16 +43,35 @@ import {INJECTOR_KEY} from './constants';
*
* {@example upgrade/static/ts/full/module.ts region="example-app"}
*
* <div class="alert is-important">
*
* When using `downgradeModule()`, downgraded injectables will not be available until the Angular
* module that provides them is instantiated. In order to be safe, you need to ensure that the
* downgraded injectables are not used anywhere _outside_ the part of the app where it is
* guaranteed that their module has been instantiated.
*
* For example, it is _OK_ to use a downgraded service in an upgraded component that is only used
* from a downgraded Angular component provided by the same Angular module as the injectable, but
* it is _not OK_ to use it in an AngularJS component that may be used independently of Angular or
* use it in a downgraded Angular component from a different module.
*
* </div>
*
* @param token an `InjectionToken` that identifies a service provided from Angular.
* @param downgradedModule the name of the downgraded module (if any) that the injectable
* "belongs to", as returned by a call to `downgradeModule()`. It is the module, whose injector will
* be used for instantiating the injectable.<br />
* (This option is only necessary when using `downgradeModule()` to downgrade more than one Angular
* module.)
*
* @returns a [factory function](https://docs.angularjs.org/guide/di) that can be
* used to register the service on an AngularJS module.
*
* @experimental
*/
export function downgradeInjectable(token: any): Function {
export function downgradeInjectable(token: any, downgradedModule: string = ''): Function {
const factory = function(i: Injector) { return i.get(token); };
(factory as any)['$inject'] = [INJECTOR_KEY];
(factory as any)['$inject'] = [`${INJECTOR_KEY}${downgradedModule}`];

return factory;
}
21 changes: 16 additions & 5 deletions packages/upgrade/src/static/downgrade_module.ts
Expand Up @@ -17,6 +17,8 @@ import {angular1Providers, setTempInjectorRef} from './angular1_providers';
import {NgAdapterInjector} from './util';


let moduleUid = 0;

/**
* @description
*
Expand Down Expand Up @@ -104,7 +106,10 @@ import {NgAdapterInjector} from './util';
export function downgradeModule<T>(
moduleFactoryOrBootstrapFn: NgModuleFactory<T>|
((extraProviders: StaticProvider[]) => Promise<NgModuleRef<T>>)): string {
const LAZY_MODULE_NAME = UPGRADE_MODULE_NAME + '.lazy';
const lazyModuleName = `${UPGRADE_MODULE_NAME}.lazy${++moduleUid}`;
const lazyModuleRefKey = `${LAZY_MODULE_REF}${lazyModuleName}`;
const lazyInjectorKey = `${INJECTOR_KEY}${lazyModuleName}`;

const bootstrapFn = isFunction(moduleFactoryOrBootstrapFn) ?
moduleFactoryOrBootstrapFn :
(extraProviders: StaticProvider[]) =>
Expand All @@ -113,17 +118,19 @@ export function downgradeModule<T>(
let injector: Injector;

// Create an ng1 module to bootstrap.
angular.module(LAZY_MODULE_NAME, [])
angular.module(lazyModuleName, [])
.factory(INJECTOR_KEY, [lazyInjectorKey, identity])
.factory(
INJECTOR_KEY,
lazyInjectorKey,
() => {
if (!injector) {
throw new Error(
'Trying to get the Angular injector before bootstrapping an Angular module.');
}
return injector;
})
.factory(LAZY_MODULE_REF, [
.factory(LAZY_MODULE_REF, [lazyModuleRefKey, identity])
.factory(lazyModuleRefKey, [
$INJECTOR,
($injector: angular.IInjectorService) => {
setTempInjectorRef($injector);
Expand All @@ -140,5 +147,9 @@ export function downgradeModule<T>(
}
]);

return LAZY_MODULE_NAME;
return lazyModuleName;
}

function identity<T = any>(x: T): T {
return x;
}
11 changes: 11 additions & 0 deletions packages/upgrade/test/common/downgrade_injectable_spec.ts
Expand Up @@ -21,5 +21,16 @@ import {downgradeInjectable} from '@angular/upgrade/src/common/downgrade_injecta
expect(injector.get).toHaveBeenCalledWith('someToken');
expect(value).toEqual('service value');
});

it('should inject the specified module\'s injector when specifying a module name', () => {
const factory = downgradeInjectable('someToken', 'someModule');
expect(factory).toEqual(jasmine.any(Function));
expect((factory as any).$inject).toEqual([`${INJECTOR_KEY}someModule`]);

const injector = {get: jasmine.createSpy('get').and.returnValue('service value')};
const value = factory(injector);
expect(injector.get).toHaveBeenCalledWith('someToken');
expect(value).toEqual('service value');
});
});
}

0 comments on commit e799782

Please sign in to comment.