fix(ivy): destroy injector when module is destroyed (#27793)

Destroys the module's injector when an `NgModule` is destroyed which in turn calls the `ngOnDestroy` methods on the instantiated providers.

This PR resolves FW-739.

PR Close #27793
This commit is contained in:
Kristiyan Kostadinov 2019-01-16 20:35:56 +01:00 committed by Alex Rickabaugh
parent 2b9cc8503d
commit ab2bf83398
5 changed files with 50 additions and 49 deletions

View File

@ -102,7 +102,8 @@ export class R3Injector {
/** /**
* Flag indicating that this injector was previously destroyed. * Flag indicating that this injector was previously destroyed.
*/ */
private destroyed = false; get destroyed(): boolean { return this._destroyed; }
private _destroyed = false;
constructor( constructor(
def: InjectorType<any>, additionalProviders: StaticProvider[]|null, def: InjectorType<any>, additionalProviders: StaticProvider[]|null,
@ -138,7 +139,7 @@ export class R3Injector {
this.assertNotDestroyed(); this.assertNotDestroyed();
// Set destroyed = true first, in case lifecycle hooks re-enter destroy(). // Set destroyed = true first, in case lifecycle hooks re-enter destroy().
this.destroyed = true; this._destroyed = true;
try { try {
// Call all the lifecycle hooks. // Call all the lifecycle hooks.
this.onDestroy.forEach(service => service.ngOnDestroy()); this.onDestroy.forEach(service => service.ngOnDestroy());
@ -189,7 +190,7 @@ export class R3Injector {
} }
private assertNotDestroyed(): void { private assertNotDestroyed(): void {
if (this.destroyed) { if (this._destroyed) {
throw new Error('Injector has already been destroyed.'); throw new Error('Injector has already been destroyed.');
} }
} }

View File

@ -273,7 +273,7 @@ export class ComponentRef<T> extends viewEngine_ComponentRef<T> {
ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed'); ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed');
this.destroyCbs !.forEach(fn => fn()); this.destroyCbs !.forEach(fn => fn());
this.destroyCbs = null; this.destroyCbs = null;
this.hostView.destroy(); !this.hostView.destroyed && this.hostView.destroy();
} }
onDestroy(callback: () => void): void { onDestroy(callback: () => void): void {
ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed'); ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed');

View File

@ -9,7 +9,7 @@
import {INJECTOR, Injector} from '../di/injector'; import {INJECTOR, Injector} from '../di/injector';
import {InjectFlags} from '../di/interface/injector'; import {InjectFlags} from '../di/interface/injector';
import {StaticProvider} from '../di/interface/provider'; import {StaticProvider} from '../di/interface/provider';
import {createInjector} from '../di/r3_injector'; import {R3Injector, createInjector} from '../di/r3_injector';
import {Type} from '../interface/type'; import {Type} from '../interface/type';
import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver'; import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver';
import {InternalNgModuleRef, NgModuleFactory as viewEngine_NgModuleFactory, NgModuleRef as viewEngine_NgModuleRef} from '../linker/ng_module_factory'; import {InternalNgModuleRef, NgModuleFactory as viewEngine_NgModuleFactory, NgModuleRef as viewEngine_NgModuleRef} from '../linker/ng_module_factory';
@ -32,7 +32,7 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
// tslint:disable-next-line:require-internal-with-underscore // tslint:disable-next-line:require-internal-with-underscore
_bootstrapComponents: Type<any>[] = []; _bootstrapComponents: Type<any>[] = [];
// tslint:disable-next-line:require-internal-with-underscore // tslint:disable-next-line:require-internal-with-underscore
_r3Injector: Injector; _r3Injector: R3Injector;
injector: Injector = this; injector: Injector = this;
instance: T; instance: T;
destroyCbs: (() => void)[]|null = []; destroyCbs: (() => void)[]|null = [];
@ -52,7 +52,7 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
}, },
COMPONENT_FACTORY_RESOLVER COMPONENT_FACTORY_RESOLVER
]; ];
this._r3Injector = createInjector(ngModuleType, _parent, additionalProviders); this._r3Injector = createInjector(ngModuleType, _parent, additionalProviders) as R3Injector;
this.instance = this.get(ngModuleType); this.instance = this.get(ngModuleType);
} }
@ -70,6 +70,8 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
destroy(): void { destroy(): void {
ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed'); ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed');
const injector = this._r3Injector;
!injector.destroyed && injector.destroy();
this.destroyCbs !.forEach(fn => fn()); this.destroyCbs !.forEach(fn => fn());
this.destroyCbs = null; this.destroyCbs = null;
} }

View File

@ -1045,55 +1045,53 @@ function declareTests(config?: {useJit: boolean}) {
expect(created).toBe(false); expect(created).toBe(false);
}); });
fixmeIvy('FW-739: TestBed: destroy on NgModuleRef is not being called') it('should support ngOnDestroy on any provider', () => {
.it('should support ngOnDestroy on any provider', () => { let destroyed = false;
let destroyed = false;
class SomeInjectable { class SomeInjectable {
ngOnDestroy() { destroyed = true; } ngOnDestroy() { destroyed = true; }
} }
@NgModule({providers: [SomeInjectable]}) @NgModule({providers: [SomeInjectable]})
class SomeModule { class SomeModule {
// Inject SomeInjectable to make it eager... // Inject SomeInjectable to make it eager...
constructor(i: SomeInjectable) {} constructor(i: SomeInjectable) {}
} }
const moduleRef = createModule(SomeModule); const moduleRef = createModule(SomeModule);
expect(destroyed).toBe(false); expect(destroyed).toBe(false);
moduleRef.destroy(); moduleRef.destroy();
expect(destroyed).toBe(true); expect(destroyed).toBe(true);
}); });
fixmeIvy('FW-739: TestBed: destroy on NgModuleRef is not being called') it('should support ngOnDestroy for lazy providers', () => {
.it('should support ngOnDestroy for lazy providers', () => { let created = false;
let created = false; let destroyed = false;
let destroyed = false;
class SomeInjectable { class SomeInjectable {
constructor() { created = true; } constructor() { created = true; }
ngOnDestroy() { destroyed = true; } ngOnDestroy() { destroyed = true; }
} }
@NgModule({providers: [SomeInjectable]}) @NgModule({providers: [SomeInjectable]})
class SomeModule { class SomeModule {
} }
let moduleRef = createModule(SomeModule); let moduleRef = createModule(SomeModule);
expect(created).toBe(false); expect(created).toBe(false);
expect(destroyed).toBe(false); expect(destroyed).toBe(false);
// no error if the provider was not yet created // no error if the provider was not yet created
moduleRef.destroy(); moduleRef.destroy();
expect(created).toBe(false); expect(created).toBe(false);
expect(destroyed).toBe(false); expect(destroyed).toBe(false);
moduleRef = createModule(SomeModule); moduleRef = createModule(SomeModule);
moduleRef.injector.get(SomeInjectable); moduleRef.injector.get(SomeInjectable);
expect(created).toBe(true); expect(created).toBe(true);
moduleRef.destroy(); moduleRef.destroy();
expect(destroyed).toBe(true); expect(destroyed).toBe(true);
}); });
}); });
describe('imported and exported modules', () => { describe('imported and exported modules', () => {

View File

@ -213,6 +213,7 @@ export class DowngradeComponentAdapter {
} }
registerCleanup() { registerCleanup() {
const testabilityRegistry = this.componentRef.injector.get(TestabilityRegistry);
const destroyComponentRef = this.wrapCallback(() => this.componentRef.destroy()); const destroyComponentRef = this.wrapCallback(() => this.componentRef.destroy());
let destroyed = false; let destroyed = false;
@ -220,8 +221,7 @@ export class DowngradeComponentAdapter {
this.componentScope.$on('$destroy', () => { this.componentScope.$on('$destroy', () => {
if (!destroyed) { if (!destroyed) {
destroyed = true; destroyed = true;
this.componentRef.injector.get(TestabilityRegistry) testabilityRegistry.unregisterApplication(this.componentRef.location.nativeElement);
.unregisterApplication(this.componentRef.location.nativeElement);
destroyComponentRef(); destroyComponentRef();
} }
}); });