Skip to content
Permalink
Browse files

fix(upgrade): allow nesting components from different downgraded modu…

…les (#27217)

PR Close #27217
  • Loading branch information...
gkalpak authored and matsko committed Nov 20, 2018
1 parent 326b464 commit bc0ee01d09ad91ac6b5ce9b3ad3efb50d9a4011b
@@ -11,7 +11,7 @@ import {ComponentFactory, ComponentFactoryResolver, Injector, NgZone, Type} from
import * as angular from './angular1';
import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, LAZY_MODULE_REF, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants';
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
import {LazyModuleRef, UpgradeAppType, controllerKey, getTypeName, getUpgradeAppType, isFunction, validateInjectionKey} from './util';
import {LazyModuleRef, UpgradeAppType, controllerKey, getDowngradedModuleCount, getTypeName, getUpgradeAppType, isFunction, validateInjectionKey} from './util';


interface Thenable<T> {
@@ -91,6 +91,10 @@ export function downgradeComponent(info: {
!isNgUpgradeLite ? cb => cb : cb => () => NgZone.isInAngularZone() ? cb() : ngZone.run(cb);
let ngZone: NgZone;

// When downgrading multiple modules, special handling is needed wrt injectors.
const hasMultipleDowngradedModules =
isNgUpgradeLite && (getDowngradedModuleCount($injector) > 1);

return {
restrict: 'E',
terminal: true,
@@ -102,23 +106,67 @@ export function downgradeComponent(info: {
// been compiled.

const ngModel: angular.INgModelController = required[1];
let parentInjector: Injector|Thenable<Injector>|undefined = required[0];
const parentInjector: Injector|Thenable<Injector>|undefined = required[0];
let moduleInjector: Injector|Thenable<Injector>|undefined = undefined;
let ranAsync = false;

if (!parentInjector) {
if (!parentInjector || hasMultipleDowngradedModules) {
const downgradedModule = info.downgradedModule || '';
const lazyModuleRefKey = `${LAZY_MODULE_REF}${downgradedModule}`;
const attemptedAction = `instantiating component '${getTypeName(info.component)}'`;

validateInjectionKey($injector, downgradedModule, lazyModuleRefKey, attemptedAction);

const lazyModuleRef = $injector.get(lazyModuleRefKey) as LazyModuleRef;
parentInjector = lazyModuleRef.injector || lazyModuleRef.promise as Promise<Injector>;
moduleInjector = lazyModuleRef.injector || lazyModuleRef.promise as Promise<Injector>;
}

const doDowngrade = (injector: Injector) => {
// Notes:
//
// There are two injectors: `finalModuleInjector` and `finalParentInjector` (they might be
// the same instance, but that is irrelevant):
// - `finalModuleInjector` is used to retrieve `ComponentFactoryResolver`, thus it must be
// on the same tree as the `NgModule` that declares this downgraded component.
// - `finalParentInjector` is used for all other injection purposes.
// (Note that Angular knows to only traverse the component-tree part of that injector,
// when looking for an injectable and then switch to the module injector.)
//
// There are basically three cases:
// - If there is no parent component (thus no `parentInjector`), we bootstrap the downgraded
// `NgModule` and use its injector as both `finalModuleInjector` and
// `finalParentInjector`.
// - If there is a parent component (and thus a `parentInjector`) and we are sure that it
// belongs to the same `NgModule` as this downgraded component (e.g. because there is only
// one downgraded module, we use that `parentInjector` as both `finalModuleInjector` and
// `finalParentInjector`.
// - If there is a parent component, but it may belong to a different `NgModule`, then we
// use the `parentInjector` as `finalParentInjector` and this downgraded component's
// declaring `NgModule`'s injector as `finalModuleInjector`.
// Note 1: If the `NgModule` is already bootstrapped, we just get its injector (we don't
// bootstrap again).
// Note 2: It is possible that (while there are multiple downgraded modules) this
// downgraded component and its parent component both belong to the same NgModule.
// In that case, we could have used the `parentInjector` as both
// `finalModuleInjector` and `finalParentInjector`, but (for simplicity) we are
// treating this case as if they belong to different `NgModule`s. That doesn't
// really affect anything, since `parentInjector` has `moduleInjector` as ancestor
// and trying to resolve `ComponentFactoryResolver` from either one will return
// the same instance.

// If there is a parent component, use its injector as parent injector.
// If this is a "top-level" Angular component, use the module injector.
const finalParentInjector = parentInjector || moduleInjector !;

// If this is a "top-level" Angular component or the parent component may belong to a
// different `NgModule`, use the module injector for module-specific dependencies.
// If there is a parent component that belongs to the same `NgModule`, use its injector.
const finalModuleInjector = moduleInjector || parentInjector !;

const doDowngrade = (injector: Injector, moduleInjector: Injector) => {
// Retrieve `ComponentFactoryResolver` from the injector tied to the `NgModule` this
// component belongs to.
const componentFactoryResolver: ComponentFactoryResolver =
injector.get(ComponentFactoryResolver);
moduleInjector.get(ComponentFactoryResolver);
const componentFactory: ComponentFactory<any> =
componentFactoryResolver.resolveComponentFactory(info.component) !;

@@ -146,18 +194,20 @@ export function downgradeComponent(info: {
}
};

const downgradeFn = !isNgUpgradeLite ? doDowngrade : (injector: Injector) => {
if (!ngZone) {
ngZone = injector.get(NgZone);
}
const downgradeFn =
!isNgUpgradeLite ? doDowngrade : (pInjector: Injector, mInjector: Injector) => {
if (!ngZone) {
ngZone = pInjector.get(NgZone);
}

wrapCallback(() => doDowngrade(injector))();
};
wrapCallback(() => doDowngrade(pInjector, mInjector))();
};

if (isThenable<Injector>(parentInjector)) {
parentInjector.then(downgradeFn);
if (isThenable(finalParentInjector) || isThenable(finalModuleInjector)) {
Promise.all([finalParentInjector, finalModuleInjector])
.then(([pInjector, mInjector]) => downgradeFn(pInjector, mInjector));
} else {
downgradeFn(parentInjector);
downgradeFn(finalParentInjector, finalModuleInjector);
}

ranAsync = true;
@@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/

import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ApplicationRef, Component, DoCheck, Inject, Injector, Input, NgModule, NgZone, OnChanges, OnDestroy, OnInit, StaticProvider, Type, ViewRef, destroyPlatform, getPlatform} from '@angular/core';
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ApplicationRef, Compiler, Component, Directive, DoCheck, ElementRef, Inject, Injectable, Injector, Input, NgModule, NgZone, OnChanges, OnDestroy, OnInit, StaticProvider, Type, ViewRef, destroyPlatform, getPlatform} from '@angular/core';
import {async, fakeAsync, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {fixmeIvy} from '@angular/private/testing';
import {downgradeComponent, downgradeModule} from '@angular/upgrade/static';
import {UpgradeComponent, downgradeComponent, downgradeModule} from '@angular/upgrade/static';
import * as angular from '@angular/upgrade/static/src/common/angular1';
import {$EXCEPTION_HANDLER, $ROOT_SCOPE, INJECTOR_KEY, LAZY_MODULE_REF} from '@angular/upgrade/static/src/common/constants';
import {LazyModuleRef} from '@angular/upgrade/static/src/common/util';
@@ -78,6 +78,259 @@ withEachNg1Version(() => {
setTimeout(() => expect(element.textContent).toBe('a | b'));
}));

it('should support nesting components from different downgraded modules', async(() => {
@Directive({selector: 'ng1A'})
class Ng1ComponentA extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1A', elementRef, injector);
}
}

@Component({
selector: 'ng2A',
template: 'ng2A(<ng1A></ng1A>)',
})
class Ng2ComponentA {
}

@Component({
selector: 'ng2B',
template: 'ng2B',
})
class Ng2ComponentB {
}

@NgModule({
declarations: [Ng1ComponentA, Ng2ComponentA],
entryComponents: [Ng2ComponentA],
imports: [BrowserModule],
})
class Ng2ModuleA {
ngDoBootstrap() {}
}

@NgModule({
declarations: [Ng2ComponentB],
entryComponents: [Ng2ComponentB],
imports: [BrowserModule],
})
class Ng2ModuleB {
ngDoBootstrap() {}
}

const doDowngradeModule = (module: Type<any>) => {
const bootstrapFn = (extraProviders: StaticProvider[]) => {
const platformRef = getPlatform() || platformBrowserDynamic(extraProviders);
return platformRef.bootstrapModule(module);
};
return downgradeModule(bootstrapFn);
};

const downModA = doDowngradeModule(Ng2ModuleA);
const downModB = doDowngradeModule(Ng2ModuleB);
const ng1Module =
angular.module('ng1', [downModA, downModB])
.directive('ng1A', () => ({template: 'ng1A(<ng2-b ng-if="showB"></ng2-b>)'}))
.directive('ng2A', downgradeComponent({
component: Ng2ComponentA,
downgradedModule: downModA, propagateDigest,
}))
.directive('ng2B', downgradeComponent({
component: Ng2ComponentB,
downgradedModule: downModB, propagateDigest,
}));

const element = html('<ng2-a></ng2-a>');
const $injector = angular.bootstrap(element, [ng1Module.name]);
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;

// Wait for module A to be bootstrapped.
setTimeout(() => {
// Wait for the upgraded component's `ngOnInit()`.
setTimeout(() => {
expect(element.textContent).toBe('ng2A(ng1A())');

$rootScope.$apply('showB = true');

// Wait for module B to be bootstrapped.
setTimeout(() => expect(element.textContent).toBe('ng2A(ng1A(ng2B))'));
});
});
}));

fixmeIvy('FW-714: ng1 projected content is not being rendered')
.it('should support nesting components from different downgraded modules (via projection)',
async(() => {
@Component({
selector: 'ng2A',
template: 'ng2A(<ng-content></ng-content>)',
})
class Ng2ComponentA {
}

@Component({
selector: 'ng2B',
template: 'ng2B',
})
class Ng2ComponentB {
}

@NgModule({
declarations: [Ng2ComponentA],
entryComponents: [Ng2ComponentA],
imports: [BrowserModule],
})
class Ng2ModuleA {
ngDoBootstrap() {}
}

@NgModule({
declarations: [Ng2ComponentB],
entryComponents: [Ng2ComponentB],
imports: [BrowserModule],
})
class Ng2ModuleB {
ngDoBootstrap() {}
}

const doDowngradeModule = (module: Type<any>) => {
const bootstrapFn = (extraProviders: StaticProvider[]) => {
const platformRef = getPlatform() || platformBrowserDynamic(extraProviders);
return platformRef.bootstrapModule(module);
};
return downgradeModule(bootstrapFn);
};

const downModA = doDowngradeModule(Ng2ModuleA);
const downModB = doDowngradeModule(Ng2ModuleB);
const ng1Module = angular.module('ng1', [downModA, downModB])
.directive('ng2A', downgradeComponent({
component: Ng2ComponentA,
downgradedModule: downModA, propagateDigest,
}))
.directive('ng2B', downgradeComponent({
component: Ng2ComponentB,
downgradedModule: downModB, propagateDigest,
}));

const element = html('<ng2-a><ng2-b ng-if="showB"></ng2-b></ng2-a>');
const $injector = angular.bootstrap(element, [ng1Module.name]);
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;

// Wait for module A to be bootstrapped.
setTimeout(() => {
expect(element.textContent).toBe('ng2A()');

$rootScope.$apply('showB = true');

// Wait for module B to be bootstrapped.
setTimeout(() => expect(element.textContent).toBe('ng2A(ng2B)'));
});
}));

fixmeIvy('FW-714: ng1 projected content is not being rendered')
.it('should support manually setting up a root module for all downgraded modules',
fakeAsync(() => {
@Injectable({providedIn: 'root'})
class CounterService {
private static counter = 0;
value = ++CounterService.counter;
}

@Component({
selector: 'ng2A',
template: 'ng2A(Counter:{{ counter.value }} | <ng-content></ng-content>)',
})
class Ng2ComponentA {
constructor(public counter: CounterService) {}
}

@Component({
selector: 'ng2B',
template: 'Counter:{{ counter.value }}',
})
class Ng2ComponentB {
constructor(public counter: CounterService) {}
}

@NgModule({
declarations: [Ng2ComponentA],
entryComponents: [Ng2ComponentA],
})
class Ng2ModuleA {
}

@NgModule({
declarations: [Ng2ComponentB],
entryComponents: [Ng2ComponentB],
})
class Ng2ModuleB {
}

// "Empty" module that will serve as root for all downgraded modules,
// ensuring there will only be one instance for all injectables provided in "root".
@NgModule({
imports: [BrowserModule],
})
class Ng2ModuleRoot {
ngDoBootstrap() {}
}

let rootInjectorPromise: Promise<Injector>|null = null;
const doDowngradeModule = (module: Type<any>) => {
const bootstrapFn = (extraProviders: StaticProvider[]) => {
if (!rootInjectorPromise) {
rootInjectorPromise = platformBrowserDynamic(extraProviders)
.bootstrapModule(Ng2ModuleRoot)
.then(ref => ref.injector);
}

return rootInjectorPromise.then(rootInjector => {
const compiler = rootInjector.get(Compiler);
const moduleFactory = compiler.compileModuleSync(module);

return moduleFactory.create(rootInjector);
});
};
return downgradeModule(bootstrapFn);
};

const downModA = doDowngradeModule(Ng2ModuleA);
const downModB = doDowngradeModule(Ng2ModuleB);
const ng1Module = angular.module('ng1', [downModA, downModB])
.directive('ng2A', downgradeComponent({
component: Ng2ComponentA,
downgradedModule: downModA, propagateDigest,
}))
.directive('ng2B', downgradeComponent({
component: Ng2ComponentB,
downgradedModule: downModB, propagateDigest,
}));

const element = html(`
<ng2-a><ng2-b ng-if="showB1"></ng2-b></ng2-a>
<ng2-b ng-if="showB2"></ng2-b>
`);
const $injector = angular.bootstrap(element, [ng1Module.name]);
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;

tick(); // Wait for module A to be bootstrapped.
expect(multiTrim(element.textContent)).toBe('ng2A(Counter:1 | )');

// Nested component B should use the same `CounterService` instance.
$rootScope.$apply('showB1 = true');

tick(); // Wait for module B to be bootstrapped.
expect(multiTrim(element.children[0].textContent))
.toBe('ng2A(Counter:1 | Counter:1)');

// Top-level component B should use the same `CounterService` instance.
$rootScope.$apply('showB2 = true');
tick();

expect(multiTrim(element.children[1].textContent)).toBe('Counter:1');
}));

it('should support downgrading a component and propagate inputs', async(() => {
@Component(
{selector: 'ng2A', template: 'a({{ value }}) | <ng2B [value]="value"></ng2B>'})

0 comments on commit bc0ee01

Please sign in to comment.
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.