From ba0a1ec459cbf03879084e91766d28cd86e14ee2 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Fri, 27 Feb 2015 07:42:51 -0800 Subject: [PATCH] feat(di): add support for optional dependencies --- modules/angular2/di.js | 2 +- .../src/core/compiler/element_injector.js | 17 ++++---- modules/angular2/src/di/annotations.js | 18 +++++++++ modules/angular2/src/di/binding.js | 40 +++++++++++++------ modules/angular2/src/di/injector.js | 23 +++++++---- .../core/compiler/element_injector_spec.js | 15 ++++++- modules/angular2/test/di/injector_spec.js | 18 ++++++++- 7 files changed, 102 insertions(+), 31 deletions(-) diff --git a/modules/angular2/di.js b/modules/angular2/di.js index ae6bff62cc..f98c12fc4b 100644 --- a/modules/angular2/di.js +++ b/modules/angular2/di.js @@ -1,4 +1,4 @@ -export {Inject, InjectPromise, InjectLazy, DependencyAnnotation} from './src/di/annotations'; +export {Inject, InjectPromise, InjectLazy, Optional, DependencyAnnotation} from './src/di/annotations'; export {Injector} from './src/di/injector'; export {Binding, Dependency, bind} from './src/di/binding'; export {Key, KeyRegistry} from './src/di/key'; diff --git a/modules/angular2/src/core/compiler/element_injector.js b/modules/angular2/src/core/compiler/element_injector.js index ad0471b8f8..2ab87348da 100644 --- a/modules/angular2/src/core/compiler/element_injector.js +++ b/modules/angular2/src/core/compiler/element_injector.js @@ -91,14 +91,15 @@ export class DirectiveDependency extends Dependency { depth:int; eventEmitterName:string; - constructor(key:Key, asPromise:boolean, lazy:boolean, properties:List, depth:int, eventEmitterName: string) { - super(key, asPromise, lazy, properties); + constructor(key:Key, asPromise:boolean, lazy:boolean, optional:boolean, + properties:List, depth:int, eventEmitterName: string) { + super(key, asPromise, lazy, optional, properties); this.depth = depth; this.eventEmitterName = eventEmitterName; } static createFrom(d:Dependency):Dependency { - return new DirectiveDependency(d.key, d.asPromise, d.lazy, + return new DirectiveDependency(d.key, d.asPromise, d.lazy, d.optional, d.properties, DirectiveDependency._depth(d.properties), DirectiveDependency._eventEmitterName(d.properties)); } @@ -123,13 +124,11 @@ export class DirectiveDependency extends Dependency { export class DirectiveBinding extends Binding { callOnDestroy:boolean; callOnChange:boolean; - onCheck:boolean; constructor(key:Key, factory:Function, dependencies:List, providedAsPromise:boolean, annotation:Directive) { super(key, factory, dependencies, providedAsPromise); this.callOnDestroy = isPresent(annotation) && annotation.hasLifecycleHook(onDestroy); this.callOnChange = isPresent(annotation) && annotation.hasLifecycleHook(onChange); - //this.onCheck = isPresent(annotation) && annotation.hasLifecycleHook(onCheck); } static createFromBinding(b:Binding, annotation:Directive):Binding { @@ -405,7 +404,7 @@ export class ElementInjector extends TreeNode { } get(token) { - return this._getByKey(Key.get(token), 0, null); + return this._getByKey(Key.get(token), 0, false, null); } hasDirective(type:Type):boolean { @@ -489,7 +488,7 @@ export class ElementInjector extends TreeNode { _getByDependency(dep:DirectiveDependency, requestor:Key) { if (isPresent(dep.eventEmitterName)) return this._buildEventEmitter(dep); - return this._getByKey(dep.key, dep.depth, requestor); + return this._getByKey(dep.key, dep.depth, dep.optional, requestor); } _buildEventEmitter(dep) { @@ -515,7 +514,7 @@ export class ElementInjector extends TreeNode { * * Write benchmarks before doing this optimization. */ - _getByKey(key:Key, depth:number, requestor:Key) { + _getByKey(key:Key, depth:number, optional:boolean, requestor:Key) { var ei = this; if (! this._shouldIncludeSelf(depth)) { @@ -536,6 +535,8 @@ export class ElementInjector extends TreeNode { if (isPresent(this._host) && this._host._isComponentKey(key)) { return this._host.getComponent(); + } else if (optional) { + return this._appInjector(requestor).getOptional(key); } else { return this._appInjector(requestor).get(key); } diff --git a/modules/angular2/src/di/annotations.js b/modules/angular2/src/di/annotations.js index f24930811c..425b3991bb 100644 --- a/modules/angular2/src/di/annotations.js +++ b/modules/angular2/src/di/annotations.js @@ -58,6 +58,24 @@ export class InjectLazy { } } +/** + * A parameter annotation that marks a dependency as optional. + * + * ``` + * class AComponent { + * constructor(@Optional() dp:Dependency) { + * this.dp = dp; + * } + * } + * ``` + * + */ +export class Optional { + @CONST() + constructor() { + } +} + /** * `DependencyAnnotation` is used by the framework to extend DI. * diff --git a/modules/angular2/src/di/binding.js b/modules/angular2/src/di/binding.js index c89af011e6..49fecc3d0d 100644 --- a/modules/angular2/src/di/binding.js +++ b/modules/angular2/src/di/binding.js @@ -2,20 +2,26 @@ import {FIELD, Type, isBlank, isPresent} from 'angular2/src/facade/lang'; import {List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection'; import {reflector} from 'angular2/src/reflection/reflection'; import {Key} from './key'; -import {Inject, InjectLazy, InjectPromise, DependencyAnnotation} from './annotations'; +import {Inject, InjectLazy, InjectPromise, Optional, DependencyAnnotation} from './annotations'; import {NoAnnotationError} from './exceptions'; export class Dependency { key:Key; asPromise:boolean; lazy:boolean; + optional:boolean; properties:List; - constructor(key:Key, asPromise:boolean, lazy:boolean, properties:List) { + constructor(key:Key, asPromise:boolean, lazy:boolean, optional:boolean, properties:List) { this.key = key; this.asPromise = asPromise; this.lazy = lazy; + this.optional = optional; this.properties = properties; } + + static fromKey(key:Key) { + return new Dependency(key, false, false, false, []); + } } export class Binding { @@ -64,7 +70,7 @@ export class BindingBuilder { return new Binding( Key.get(this.token), (aliasInstance) => aliasInstance, - [new Dependency(Key.get(aliasToken), false, false, [])], + [Dependency.fromKey(Key.get(aliasToken))], false ); } @@ -90,7 +96,7 @@ export class BindingBuilder { _constructDependencies(factoryFunction:Function, dependencies:List) { return isBlank(dependencies) ? _dependenciesFor(factoryFunction) : - ListWrapper.map(dependencies, (t) => new Dependency(Key.get(t), false, false, [])); + ListWrapper.map(dependencies, (t) => Dependency.fromKey(Key.get(t))); } } @@ -102,36 +108,44 @@ function _dependenciesFor(typeOrFunc):List { } function _extractToken(typeOrFunc, annotations) { - var type; var depProps = []; + var token = null; + var optional = false; + var lazy = false; + var asPromise = false; for (var i = 0; i < annotations.length; ++i) { var paramAnnotation = annotations[i]; if (paramAnnotation instanceof Type) { - type = paramAnnotation; + token = paramAnnotation; } else if (paramAnnotation instanceof Inject) { - return _createDependency(paramAnnotation.token, false, false, []); + token = paramAnnotation.token; } else if (paramAnnotation instanceof InjectPromise) { - return _createDependency(paramAnnotation.token, true, false, []); + token = paramAnnotation.token; + asPromise = true; } else if (paramAnnotation instanceof InjectLazy) { - return _createDependency(paramAnnotation.token, false, true, []); + token = paramAnnotation.token; + lazy = true; + + } else if (paramAnnotation instanceof Optional) { + optional = true; } else if (paramAnnotation instanceof DependencyAnnotation) { ListWrapper.push(depProps, paramAnnotation); } } - if (isPresent(type)) { - return _createDependency(type, false, false, depProps); + if (isPresent(token)) { + return _createDependency(token, asPromise, lazy, optional, depProps); } else { throw new NoAnnotationError(typeOrFunc); } } -function _createDependency(token, asPromise, lazy, depProps):Dependency { - return new Dependency(Key.get(token), asPromise, lazy, depProps); +function _createDependency(token, asPromise, lazy, optional, depProps):Dependency { + return new Dependency(Key.get(token), asPromise, lazy, optional, depProps); } diff --git a/modules/angular2/src/di/injector.js b/modules/angular2/src/di/injector.js index 7ac1c231e8..4d62203ea0 100644 --- a/modules/angular2/src/di/injector.js +++ b/modules/angular2/src/di/injector.js @@ -38,11 +38,15 @@ export class Injector { } get(token) { - return this._getByKey(Key.get(token), false, false); + return this._getByKey(Key.get(token), false, false, false); + } + + getOptional(token) { + return this._getByKey(Key.get(token), false, false, true); } asyncGet(token) { - return this._getByKey(Key.get(token), true, false); + return this._getByKey(Key.get(token), true, false, false); } createChild(bindings:List):Injector { @@ -60,9 +64,9 @@ export class Injector { return ListWrapper.createFixedSize(Key.numberOfKeys + 1); } - _getByKey(key:Key, returnPromise:boolean, returnLazy:boolean) { + _getByKey(key:Key, returnPromise:boolean, returnLazy:boolean, optional:boolean) { if (returnLazy) { - return () => this._getByKey(key, returnPromise, false); + return () => this._getByKey(key, returnPromise, false, optional); } var strategy = returnPromise ? this._asyncStrategy : this._syncStrategy; @@ -74,14 +78,19 @@ export class Injector { if (isPresent(instance)) return instance; if (isPresent(this._parent)) { - return this._parent._getByKey(key, returnPromise, returnLazy); + return this._parent._getByKey(key, returnPromise, returnLazy, optional); + } + + if (optional) { + return null; + } else { + throw new NoProviderError(key); } - throw new NoProviderError(key); } _resolveDependencies(key:Key, binding:Binding, forceAsync:boolean):List { try { - var getDependency = d => this._getByKey(d.key, forceAsync || d.asPromise, d.lazy); + var getDependency = d => this._getByKey(d.key, forceAsync || d.asPromise, d.lazy, d.optional); return ListWrapper.map(binding.dependencies, getDependency); } catch (e) { this._clear(key); diff --git a/modules/angular2/test/core/compiler/element_injector_spec.js b/modules/angular2/test/core/compiler/element_injector_spec.js index 020575ab0a..490397e4e2 100644 --- a/modules/angular2/test/core/compiler/element_injector_spec.js +++ b/modules/angular2/test/core/compiler/element_injector_spec.js @@ -5,7 +5,7 @@ import {ProtoElementInjector, PreBuiltObjects, DirectiveBinding} from 'angular2/ import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility'; import {EventEmitter} from 'angular2/src/core/annotations/events'; import {onDestroy} from 'angular2/src/core/annotations/annotations'; -import {Injector, Inject, bind} from 'angular2/di'; +import {Optional, Injector, Inject, bind} from 'angular2/di'; import {View} from 'angular2/src/core/compiler/view'; import {ViewContainer} from 'angular2/src/core/compiler/view_container'; import {NgElement} from 'angular2/src/core/dom/element'; @@ -36,6 +36,13 @@ class NeedsDirective { } } +class OptionallyNeedsDirective { + dependency:SimpleDirective; + constructor(@Optional() dependency:SimpleDirective){ + this.dependency = dependency; + } +} + class NeedDirectiveFromParent { dependency:SimpleDirective; constructor(@Parent() dependency:SimpleDirective){ @@ -342,6 +349,12 @@ export function main() { toThrowError('No provider for SimpleDirective! (NeedDirectiveFromParent -> SimpleDirective)'); }); + it("should inject null when no directive found", function () { + var inj = injector([OptionallyNeedsDirective]); + var d = inj.get(OptionallyNeedsDirective); + expect(d.dependency).toEqual(null); + }); + it("should accept SimpleDirective bindings instead of SimpleDirective types", function () { var inj = injector([ DirectiveBinding.createFromBinding(bind(SimpleDirective).toClass(SimpleDirective), null) diff --git a/modules/angular2/test/di/injector_spec.js b/modules/angular2/test/di/injector_spec.js index 03c7de1edb..404a338d76 100644 --- a/modules/angular2/test/di/injector_spec.js +++ b/modules/angular2/test/di/injector_spec.js @@ -1,5 +1,5 @@ import {describe, ddescribe, it, iit, expect, beforeEach} from 'angular2/test_lib'; -import {Injector, Inject, InjectLazy, bind} from 'angular2/di'; +import {Injector, Inject, InjectLazy, Optional, bind} from 'angular2/di'; class Engine { } @@ -34,6 +34,13 @@ class CarWithLazyEngine { } } +class CarWithOptionalEngine { + engine; + constructor(@Optional() engine:Engine) { + this.engine = engine; + } +} + class CarWithDashboard { engine:Engine; dashboard:Dashboard; @@ -159,6 +166,15 @@ export function main() { expect(car.engine).toBeAnInstanceOf(Engine); }); + it('should support optional dependencies', function () { + var injector = new Injector([ + CarWithOptionalEngine + ]); + + var car = injector.get(CarWithOptionalEngine); + expect(car.engine).toEqual(null); + }); + it("should flatten passed-in bindings", function () { var injector = new Injector([ [[Engine, Car]]