From fb1b1da7b9fdeb3d82168e07328b9eafa9aba09f Mon Sep 17 00:00:00 2001 From: vsavkin Date: Tue, 13 Jan 2015 16:17:43 -0800 Subject: [PATCH] feat(directive): notify directive before they get destroyed --- modules/core/src/annotations/annotations.js | 44 ++++++--- modules/core/src/compiler/element_injector.js | 52 +++++++++-- .../proto_element_injector_builder.js | 13 ++- modules/core/src/compiler/view.js | 4 +- .../test/compiler/element_injector_spec.js | 93 ++++++++++++------- .../proto_element_injector_builder_spec.js | 8 +- 6 files changed, 155 insertions(+), 59 deletions(-) diff --git a/modules/core/src/annotations/annotations.js b/modules/core/src/annotations/annotations.js index 96501751d3..7155d0e77c 100644 --- a/modules/core/src/annotations/annotations.js +++ b/modules/core/src/annotations/annotations.js @@ -10,23 +10,27 @@ export class Directive { bind:any; lightDomServices:any; //List; implementsTypes:any; //List; + lifecycle:any; //List @CONST() constructor({ selector, bind, lightDomServices, - implementsTypes + implementsTypes, + lifecycle }:{ selector:string, bind:any, lightDomServices:List, - implementsTypes:List + implementsTypes:List, + lifecycle:List }={}) { this.selector = selector; this.lightDomServices = lightDomServices; this.implementsTypes = implementsTypes; this.bind = bind; + this.lifecycle = lifecycle; } } @@ -37,8 +41,9 @@ export class Component extends Directive { shadowDomServices:any; //List; componentServices:any; //List; shadowDom:any; //ShadowDomStrategy; + lifecycle:any; //List - @CONST() +@CONST() constructor({ selector, bind, @@ -47,29 +52,34 @@ export class Component extends Directive { shadowDomServices, componentServices, implementsTypes, - shadowDom + shadowDom, + lifecycle }:{ - selector:String, + selector:String, bind:Object, template:TemplateConfig, lightDomServices:List, shadowDomServices:List, componentServices:List, implementsTypes:List, - shadowDom:ShadowDomStrategy - }={}) + shadowDom:ShadowDomStrategy, + lifecycle:List + }={}) { super({ selector: selector, bind: bind, lightDomServices: lightDomServices, - implementsTypes: implementsTypes}); + implementsTypes: implementsTypes, + lifecycle: lifecycle + }); this.template = template; this.lightDomServices = lightDomServices; this.shadowDomServices = shadowDomServices; this.componentServices = componentServices; this.shadowDom = shadowDom; + this.lifecycle = lifecycle; } } @@ -81,12 +91,14 @@ export class Decorator extends Directive { bind, lightDomServices, implementsTypes, - compileChildren = true + lifecycle, + compileChildren = true, }:{ selector:string, bind:any, lightDomServices:List, implementsTypes:List, + lifecycle:List, compileChildren:boolean }={}) { @@ -95,7 +107,8 @@ export class Decorator extends Directive { selector: selector, bind: bind, lightDomServices: lightDomServices, - implementsTypes: implementsTypes + implementsTypes: implementsTypes, + lifecycle: lifecycle }); } } @@ -106,19 +119,24 @@ export class Template extends Directive { selector, bind, lightDomServices, - implementsTypes + implementsTypes, + lifecycle }:{ selector:string, bind:any, lightDomServices:List, - implementsTypes:List + implementsTypes:List, + lifecycle:List }={}) { super({ selector: selector, bind: bind, lightDomServices: lightDomServices, - implementsTypes: implementsTypes + implementsTypes: implementsTypes, + lifecycle: lifecycle }); } } + +export var onDestroy = "onDestroy"; diff --git a/modules/core/src/compiler/element_injector.js b/modules/core/src/compiler/element_injector.js index 51697086d5..1b8453329c 100644 --- a/modules/core/src/compiler/element_injector.js +++ b/modules/core/src/compiler/element_injector.js @@ -3,10 +3,12 @@ import {Math} from 'facade/math'; import {List, ListWrapper} from 'facade/collection'; import {Injector, Key, Dependency, bind, Binding, NoProviderError, ProviderError, CyclicDependencyError} from 'di/di'; import {Parent, Ancestor} from 'core/annotations/visibility'; +import {onDestroy} from 'core/annotations/annotations'; import {View} from 'core/compiler/view'; import {LightDom, SourceLightDom, DestinationLightDom} from 'core/compiler/shadow_dom_emulation/light_dom'; import {ViewPort} from 'core/compiler/viewport'; import {NgElement} from 'core/dom/element'; +import {Directive} from 'core/annotations/annotations' var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10; @@ -85,8 +87,9 @@ class TreeNode { } } -class DirectiveDependency extends Dependency { +export class DirectiveDependency extends Dependency { depth:int; + constructor(key:Key, asPromise:boolean, lazy:boolean, properties:List, depth:int) { super(key, asPromise, lazy, properties); this.depth = depth; @@ -105,6 +108,28 @@ class DirectiveDependency extends Dependency { } } +export class DirectiveBinding extends Binding { + callOnDestroy:boolean; + + constructor(key:Key, factory:Function, dependencies:List, providedAsPromise:boolean, callOnDestroy:boolean) { + super(key, factory, dependencies, providedAsPromise); + this.callOnDestroy = callOnDestroy; + } + + static createFromBinding(b:Binding, annotation:Directive):Binding { + var deps = ListWrapper.map(b.dependencies, DirectiveDependency.createFrom); + var callOnDestroy = isPresent(annotation) && isPresent(annotation.lifecycle) ? + ListWrapper.contains(annotation.lifecycle, onDestroy) : + false; + return new DirectiveBinding(b.key, b.factory, deps, b.providedAsPromise, callOnDestroy); + } + + static createFromType(type:Type, annotation:Directive):Binding { + var binding = bind(type).toClass(type); + return DirectiveBinding.createFromBinding(binding, annotation); + } +} + // TODO(rado): benchmark and consider rolling in as ElementInjector fields. export class PreBuiltObjects { @@ -205,11 +230,12 @@ export class ProtoElementInjector { } _createBinding(bindingOrType) { - var b = (bindingOrType instanceof Type) ? - bind(bindingOrType).toClass(bindingOrType) : - bindingOrType; - var deps = ListWrapper.map(b.dependencies, DirectiveDependency.createFrom); - return new Binding(b.key, b.factory, deps, b.providedAsPromise); + if (bindingOrType instanceof DirectiveBinding) { + return bindingOrType; + } else { + var b = bind(bindingOrType).toClass(bindingOrType); + return DirectiveBinding.createFromBinding(b, null); + } } get hasBindings():boolean { @@ -271,6 +297,19 @@ export class ElementInjector extends TreeNode { this._lightDomAppInjector = null; this._shadowDomAppInjector = null; + var p = this._proto; + + if (isPresent(p._binding0) && p._binding0.callOnDestroy) {this._obj0.onDestroy();} + if (isPresent(p._binding1) && p._binding1.callOnDestroy) {this._obj1.onDestroy();} + if (isPresent(p._binding2) && p._binding2.callOnDestroy) {this._obj2.onDestroy();} + if (isPresent(p._binding3) && p._binding3.callOnDestroy) {this._obj3.onDestroy();} + if (isPresent(p._binding4) && p._binding4.callOnDestroy) {this._obj4.onDestroy();} + if (isPresent(p._binding5) && p._binding5.callOnDestroy) {this._obj5.onDestroy();} + if (isPresent(p._binding6) && p._binding6.callOnDestroy) {this._obj6.onDestroy();} + if (isPresent(p._binding7) && p._binding7.callOnDestroy) {this._obj7.onDestroy();} + if (isPresent(p._binding8) && p._binding8.callOnDestroy) {this._obj8.onDestroy();} + if (isPresent(p._binding9) && p._binding9.callOnDestroy) {this._obj9.onDestroy();} + this._obj0 = null; this._obj1 = null; this._obj2 = null; @@ -281,6 +320,7 @@ export class ElementInjector extends TreeNode { this._obj7 = null; this._obj8 = null; this._obj9 = null; + this._constructionCounter = 0; } diff --git a/modules/core/src/compiler/pipeline/proto_element_injector_builder.js b/modules/core/src/compiler/pipeline/proto_element_injector_builder.js index aaecf5a9e5..d485e57887 100644 --- a/modules/core/src/compiler/pipeline/proto_element_injector_builder.js +++ b/modules/core/src/compiler/pipeline/proto_element_injector_builder.js @@ -2,11 +2,12 @@ import {isPresent, isBlank} from 'facade/lang'; import {ListWrapper} from 'facade/collection'; import {Key} from 'di/di'; -import {ProtoElementInjector, ComponentKeyMetaData} from '../element_injector'; +import {ProtoElementInjector, ComponentKeyMetaData, DirectiveBinding} from '../element_injector'; import {CompileStep} from './compile_step'; import {CompileElement} from './compile_element'; import {CompileControl} from './compile_control'; +import {DirectiveMetadata} from '../directive_metadata'; /** * Creates the ProtoElementInjectors. @@ -67,16 +68,20 @@ export class ProtoElementInjectorBuilder extends CompileStep { _collectDirectiveBindings(pipelineElement) { var directiveTypes = []; if (isPresent(pipelineElement.componentDirective)) { - ListWrapper.push(directiveTypes, pipelineElement.componentDirective.type); + ListWrapper.push(directiveTypes, this._createBinding(pipelineElement.componentDirective)); } if (isPresent(pipelineElement.templateDirective)) { - ListWrapper.push(directiveTypes, pipelineElement.templateDirective.type); + ListWrapper.push(directiveTypes, this._createBinding(pipelineElement.templateDirective)); } if (isPresent(pipelineElement.decoratorDirectives)) { for (var i=0; i { expect( () => { - hostShadowInjectors([SomeOtherDirective, Directive], [NeedsDirective]); - }).toThrowError('No provider for Directive! (NeedsDirective -> Directive)') + hostShadowInjectors([SomeOtherDirective, SimpleDirective], [NeedsDirective]); + }).toThrowError('No provider for SimpleDirective! (NeedsDirective -> SimpleDirective)') }); it("should instantiate component directives that depend on app services in the shadow app injector", () => { @@ -269,42 +283,46 @@ export function main() { }); it("should get directives from parent", function () { - var child = parentChildInjectors([Directive], [NeedDirectiveFromParent]); + var child = parentChildInjectors([SimpleDirective], [NeedDirectiveFromParent]); var d = child.get(NeedDirectiveFromParent); expect(d).toBeAnInstanceOf(NeedDirectiveFromParent); - expect(d.dependency).toBeAnInstanceOf(Directive); + expect(d.dependency).toBeAnInstanceOf(SimpleDirective); }); it("should not return parent's directives on self", function () { expect(() => { - injector([Directive, NeedDirectiveFromParent]); + injector([SimpleDirective, NeedDirectiveFromParent]); }).toThrowError(); }); it("should get directives from ancestor", function () { - var child = parentChildInjectors([Directive], [NeedDirectiveFromAncestor]); + var child = parentChildInjectors([SimpleDirective], [NeedDirectiveFromAncestor]); var d = child.get(NeedDirectiveFromAncestor); expect(d).toBeAnInstanceOf(NeedDirectiveFromAncestor); - expect(d.dependency).toBeAnInstanceOf(Directive); + expect(d.dependency).toBeAnInstanceOf(SimpleDirective); }); - it("should throw when no directive found", function () { + it("should throw when no SimpleDirective found", function () { expect(() => injector([NeedDirectiveFromParent])). - toThrowError('No provider for Directive! (NeedDirectiveFromParent -> Directive)'); + toThrowError('No provider for SimpleDirective! (NeedDirectiveFromParent -> SimpleDirective)'); }); - it("should accept bindings instead of directive types", function () { - var inj = injector([bind(Directive).toClass(Directive)]); - expect(inj.get(Directive)).toBeAnInstanceOf(Directive); + it("should accept SimpleDirective bindings instead of SimpleDirective types", function () { + var inj = injector([ + DirectiveBinding.createFromBinding(bind(SimpleDirective).toClass(SimpleDirective), null) + ]); + expect(inj.get(SimpleDirective)).toBeAnInstanceOf(SimpleDirective); }); it("should allow for direct access using getAtIndex", function () { - var inj = injector([bind(Directive).toClass(Directive)]); - expect(inj.getAtIndex(0)).toBeAnInstanceOf(Directive); + var inj = injector([ + DirectiveBinding.createFromBinding(bind(SimpleDirective).toClass(SimpleDirective), null) + ]); + expect(inj.getAtIndex(0)).toBeAnInstanceOf(SimpleDirective); expect(() => inj.getAtIndex(-1)).toThrowError( 'Index -1 is out-of-bounds.'); expect(() => inj.getAtIndex(10)).toThrowError( @@ -313,13 +331,24 @@ export function main() { it("should handle cyclic dependencies", function () { expect(() => { + var bAneedsB = bind(A_Needs_B).toFactory((a) => new A_Needs_B(a), [B_Needs_A]); + var bBneedsA = bind(B_Needs_A).toFactory((a) => new B_Needs_A(a), [A_Needs_B]); injector([ - bind(A_Needs_B).toFactory((a) => new A_Needs_B(a), [B_Needs_A]), - bind(B_Needs_A).toFactory((a) => new B_Needs_A(a), [A_Needs_B]) + DirectiveBinding.createFromBinding(bAneedsB, null), + DirectiveBinding.createFromBinding(bBneedsA, null) ]); }).toThrowError('Cannot instantiate cyclic dependency! ' + '(A_Needs_B -> B_Needs_A -> A_Needs_B)'); }); + + it("should call onDestroy on directives subscribed to this event", function () { + var inj = injector([ + DirectiveBinding.createFromType(DirectiveWithDestroy, new Directive({lifecycle: [onDestroy]})) + ]); + var destroy = inj.get(DirectiveWithDestroy); + inj.clearDirectives(); + expect(destroy.onDestroyCounter).toBe(1); + }); }); describe("pre built objects", function () { diff --git a/modules/core/test/compiler/pipeline/proto_element_injector_builder_spec.js b/modules/core/test/compiler/pipeline/proto_element_injector_builder_spec.js index ad485b2285..7f04969cc2 100644 --- a/modules/core/test/compiler/pipeline/proto_element_injector_builder_spec.js +++ b/modules/core/test/compiler/pipeline/proto_element_injector_builder_spec.js @@ -52,7 +52,8 @@ export function main() { var directives = [SomeComponentDirective, SomeTemplateDirective, SomeDecoratorDirective]; var results = createPipeline(directives).process(el('
')); var creationArgs = getCreationArgs(results[0].inheritedProtoElementInjector); - expect(creationArgs['bindings']).toEqual(directives); + var boundDirectives = creationArgs['bindings'].map((b) => b.key.token); + expect(boundDirectives).toEqual(directives); }); it('should mark ProtoElementInjector for elements with component directives and use the ComponentDirective as first binding', () => { @@ -60,7 +61,8 @@ export function main() { var results = createPipeline(directives).process(el('
')); var creationArgs = getCreationArgs(results[0].inheritedProtoElementInjector); expect(creationArgs['firstBindingIsComponent']).toBe(true); - expect(creationArgs['bindings']).toEqual([SomeComponentDirective, SomeDecoratorDirective]); + var boundDirectives = creationArgs['bindings'].map((b) => b.key.token); + expect(boundDirectives).toEqual([SomeComponentDirective, SomeDecoratorDirective]); }); it('should use the next ElementBinder index as index of the ProtoElementInjector', () => { @@ -171,4 +173,4 @@ class SomeTemplateDirective {} class SomeComponentDirective {} @Decorator() -class SomeDecoratorDirective {} \ No newline at end of file +class SomeDecoratorDirective {}