diff --git a/modules/@angular/compiler/src/ng_module_compiler.ts b/modules/@angular/compiler/src/ng_module_compiler.ts index 38f1f71227..5a113508c0 100644 --- a/modules/@angular/compiler/src/ng_module_compiler.ts +++ b/modules/@angular/compiler/src/ng_module_compiler.ts @@ -8,6 +8,8 @@ import {Injectable} from '@angular/core'; +import {LifecycleHooks} from '../core_private'; + import {CompileDiDependencyMetadata, CompileIdentifierMap, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileProviderMetadata, CompileTokenMetadata} from './compile_metadata'; import {isBlank, isPresent} from './facade/lang'; import {Identifiers, identifierToken} from './identifiers'; @@ -71,6 +73,7 @@ class _InjectorBuilder { private _instances = new CompileIdentifierMap(); private _fields: o.ClassField[] = []; private _createStmts: o.Statement[] = []; + private _destroyStmts: o.Statement[] = []; private _getters: o.ClassGetter[] = []; constructor( @@ -85,6 +88,9 @@ class _InjectorBuilder { var instance = this._createProviderProperty( propName, resolvedProvider, providerValueExpressions, resolvedProvider.multiProvider, resolvedProvider.eager); + if (resolvedProvider.lifecycleHooks.indexOf(LifecycleHooks.OnDestroy) !== -1) { + this._destroyStmts.push(instance.callMethod('ngOnDestroy', []).toStmt()); + } this._instances.add(resolvedProvider.token, instance); } @@ -108,7 +114,10 @@ class _InjectorBuilder { new o.FnParam(InjectMethodVars.notFoundResult.name, o.DYNAMIC_TYPE) ], getMethodStmts.concat([new o.ReturnStatement(InjectMethodVars.notFoundResult)]), - o.DYNAMIC_TYPE) + o.DYNAMIC_TYPE), + new o.ClassMethod( + 'destroyInternal', [], this._destroyStmts + ), ]; var ctor = new o.ClassMethod( diff --git a/modules/@angular/core/src/linker/ng_module_factory.ts b/modules/@angular/core/src/linker/ng_module_factory.ts index 5810e90f8c..d07a9ebeae 100644 --- a/modules/@angular/core/src/linker/ng_module_factory.ts +++ b/modules/@angular/core/src/linker/ng_module_factory.ts @@ -7,12 +7,14 @@ */ import {Injector, THROW_IF_NOT_FOUND} from '../di/injector'; -import {unimplemented} from '../facade/exceptions'; -import {ConcreteType} from '../facade/lang'; +import {BaseException, unimplemented} from '../facade/exceptions'; +import {ConcreteType, stringify} from '../facade/lang'; + import {ComponentFactory} from './component_factory'; import {CodegenComponentFactoryResolver, ComponentFactoryResolver} from './component_factory_resolver'; + /** * Represents an instance of an NgModule created via a {@link NgModuleFactory}. * @@ -37,6 +39,16 @@ export abstract class NgModuleRef { * The NgModule instance. */ get instance(): T { return unimplemented(); } + + /** + * Destroys the module instance and all of the data structures associated with it. + */ + abstract destroy(): void; + + /** + * Allows to register a callback that will be called when the module is destroyed. + */ + abstract onDestroy(callback: () => void): void; } /** @@ -64,6 +76,9 @@ const _UNDEFINED = new Object(); export abstract class NgModuleInjector extends CodegenComponentFactoryResolver implements Injector, NgModuleRef { + private _destroyListeners: (() => void)[] = []; + private _destroyed: boolean = false; + public instance: T; constructor(public parent: Injector, factories: ComponentFactory[]) { @@ -87,4 +102,18 @@ export abstract class NgModuleInjector extends CodegenComponentFactoryResolve get injector(): Injector { return this; } get componentFactoryResolver(): ComponentFactoryResolver { return this; } + + destroy(): void { + if (this._destroyed) { + throw new BaseException( + `This module is already destroyed (${stringify(this.instance.constructor)})`); + } + this._destroyed = true; + this.destroyInternal(); + this._destroyListeners.forEach((listener) => listener()); + } + + onDestroy(callback: () => void): void { this._destroyListeners.push(callback); } + + abstract destroyInternal(): void; } diff --git a/modules/@angular/core/test/linker/ng_module_integration_spec.ts b/modules/@angular/core/test/linker/ng_module_integration_spec.ts index cff92c2302..d666f6c1a5 100644 --- a/modules/@angular/core/test/linker/ng_module_integration_spec.ts +++ b/modules/@angular/core/test/linker/ng_module_integration_spec.ts @@ -917,6 +917,69 @@ function declareTests({useJit}: {useJit: boolean}) { }); }); + describe('lifecycle', () => { + it('should instantiate modules eagerly', () => { + let created = false; + + @NgModule() + class ImportedModule { + constructor() { created = true; } + } + + @NgModule({imports: [ImportedModule]}) + class SomeModule { + } + + createModule(SomeModule); + + expect(created).toBe(true); + }); + + it('should instantiate providers that are not used by a module lazily', () => { + let created = false; + + createInjector([{ + provide: 'someToken', + useFactory: () => { + created = true; + return true; + } + }]); + + expect(created).toBe(false); + }); + + it('should support ngOnDestroy on any provider', () => { + let destroyed = false; + + class SomeInjectable { + ngOnDestroy() { destroyed = true; } + } + + @NgModule({providers: [SomeInjectable]}) + class SomeModule { + } + + const moduleRef = createModule(SomeModule); + expect(destroyed).toBe(false); + moduleRef.destroy(); + expect(destroyed).toBe(true); + }); + + it('should instantiate providers with lifecycle eagerly', () => { + let created = false; + + class SomeInjectable { + constructor() { created = true; } + ngOnDestroy() {} + } + + createInjector([SomeInjectable]); + + expect(created).toBe(true); + }); + }); + describe('imported and exported modules', () => { it('should add the providers of imported modules', () => { @NgModule({providers: [{provide: 'token1', useValue: 'imported'}]})