feat(di): add support for optional dependencies

This commit is contained in:
vsavkin 2015-02-27 07:42:51 -08:00 committed by Misko Hevery
parent 23786aaa92
commit ba0a1ec459
7 changed files with 102 additions and 31 deletions

View File

@ -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 {Injector} from './src/di/injector';
export {Binding, Dependency, bind} from './src/di/binding'; export {Binding, Dependency, bind} from './src/di/binding';
export {Key, KeyRegistry} from './src/di/key'; export {Key, KeyRegistry} from './src/di/key';

View File

@ -91,14 +91,15 @@ export class DirectiveDependency extends Dependency {
depth:int; depth:int;
eventEmitterName:string; eventEmitterName:string;
constructor(key:Key, asPromise:boolean, lazy:boolean, properties:List, depth:int, eventEmitterName: string) { constructor(key:Key, asPromise:boolean, lazy:boolean, optional:boolean,
super(key, asPromise, lazy, properties); properties:List, depth:int, eventEmitterName: string) {
super(key, asPromise, lazy, optional, properties);
this.depth = depth; this.depth = depth;
this.eventEmitterName = eventEmitterName; this.eventEmitterName = eventEmitterName;
} }
static createFrom(d:Dependency):Dependency { 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), d.properties, DirectiveDependency._depth(d.properties),
DirectiveDependency._eventEmitterName(d.properties)); DirectiveDependency._eventEmitterName(d.properties));
} }
@ -123,13 +124,11 @@ export class DirectiveDependency extends Dependency {
export class DirectiveBinding extends Binding { export class DirectiveBinding extends Binding {
callOnDestroy:boolean; callOnDestroy:boolean;
callOnChange:boolean; callOnChange:boolean;
onCheck:boolean;
constructor(key:Key, factory:Function, dependencies:List, providedAsPromise:boolean, annotation:Directive) { constructor(key:Key, factory:Function, dependencies:List, providedAsPromise:boolean, annotation:Directive) {
super(key, factory, dependencies, providedAsPromise); super(key, factory, dependencies, providedAsPromise);
this.callOnDestroy = isPresent(annotation) && annotation.hasLifecycleHook(onDestroy); this.callOnDestroy = isPresent(annotation) && annotation.hasLifecycleHook(onDestroy);
this.callOnChange = isPresent(annotation) && annotation.hasLifecycleHook(onChange); this.callOnChange = isPresent(annotation) && annotation.hasLifecycleHook(onChange);
//this.onCheck = isPresent(annotation) && annotation.hasLifecycleHook(onCheck);
} }
static createFromBinding(b:Binding, annotation:Directive):Binding { static createFromBinding(b:Binding, annotation:Directive):Binding {
@ -405,7 +404,7 @@ export class ElementInjector extends TreeNode {
} }
get(token) { get(token) {
return this._getByKey(Key.get(token), 0, null); return this._getByKey(Key.get(token), 0, false, null);
} }
hasDirective(type:Type):boolean { hasDirective(type:Type):boolean {
@ -489,7 +488,7 @@ export class ElementInjector extends TreeNode {
_getByDependency(dep:DirectiveDependency, requestor:Key) { _getByDependency(dep:DirectiveDependency, requestor:Key) {
if (isPresent(dep.eventEmitterName)) return this._buildEventEmitter(dep); 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) { _buildEventEmitter(dep) {
@ -515,7 +514,7 @@ export class ElementInjector extends TreeNode {
* *
* Write benchmarks before doing this optimization. * 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; var ei = this;
if (! this._shouldIncludeSelf(depth)) { if (! this._shouldIncludeSelf(depth)) {
@ -536,6 +535,8 @@ export class ElementInjector extends TreeNode {
if (isPresent(this._host) && this._host._isComponentKey(key)) { if (isPresent(this._host) && this._host._isComponentKey(key)) {
return this._host.getComponent(); return this._host.getComponent();
} else if (optional) {
return this._appInjector(requestor).getOptional(key);
} else { } else {
return this._appInjector(requestor).get(key); return this._appInjector(requestor).get(key);
} }

View File

@ -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. * `DependencyAnnotation` is used by the framework to extend DI.
* *

View File

@ -2,20 +2,26 @@ import {FIELD, Type, isBlank, isPresent} from 'angular2/src/facade/lang';
import {List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection'; import {List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {reflector} from 'angular2/src/reflection/reflection'; import {reflector} from 'angular2/src/reflection/reflection';
import {Key} from './key'; import {Key} from './key';
import {Inject, InjectLazy, InjectPromise, DependencyAnnotation} from './annotations'; import {Inject, InjectLazy, InjectPromise, Optional, DependencyAnnotation} from './annotations';
import {NoAnnotationError} from './exceptions'; import {NoAnnotationError} from './exceptions';
export class Dependency { export class Dependency {
key:Key; key:Key;
asPromise:boolean; asPromise:boolean;
lazy:boolean; lazy:boolean;
optional:boolean;
properties:List; 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.key = key;
this.asPromise = asPromise; this.asPromise = asPromise;
this.lazy = lazy; this.lazy = lazy;
this.optional = optional;
this.properties = properties; this.properties = properties;
} }
static fromKey(key:Key) {
return new Dependency(key, false, false, false, []);
}
} }
export class Binding { export class Binding {
@ -64,7 +70,7 @@ export class BindingBuilder {
return new Binding( return new Binding(
Key.get(this.token), Key.get(this.token),
(aliasInstance) => aliasInstance, (aliasInstance) => aliasInstance,
[new Dependency(Key.get(aliasToken), false, false, [])], [Dependency.fromKey(Key.get(aliasToken))],
false false
); );
} }
@ -90,7 +96,7 @@ export class BindingBuilder {
_constructDependencies(factoryFunction:Function, dependencies:List) { _constructDependencies(factoryFunction:Function, dependencies:List) {
return isBlank(dependencies) ? return isBlank(dependencies) ?
_dependenciesFor(factoryFunction) : _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) { function _extractToken(typeOrFunc, annotations) {
var type;
var depProps = []; var depProps = [];
var token = null;
var optional = false;
var lazy = false;
var asPromise = false;
for (var i = 0; i < annotations.length; ++i) { for (var i = 0; i < annotations.length; ++i) {
var paramAnnotation = annotations[i]; var paramAnnotation = annotations[i];
if (paramAnnotation instanceof Type) { if (paramAnnotation instanceof Type) {
type = paramAnnotation; token = paramAnnotation;
} else if (paramAnnotation instanceof Inject) { } else if (paramAnnotation instanceof Inject) {
return _createDependency(paramAnnotation.token, false, false, []); token = paramAnnotation.token;
} else if (paramAnnotation instanceof InjectPromise) { } else if (paramAnnotation instanceof InjectPromise) {
return _createDependency(paramAnnotation.token, true, false, []); token = paramAnnotation.token;
asPromise = true;
} else if (paramAnnotation instanceof InjectLazy) { } 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) { } else if (paramAnnotation instanceof DependencyAnnotation) {
ListWrapper.push(depProps, paramAnnotation); ListWrapper.push(depProps, paramAnnotation);
} }
} }
if (isPresent(type)) { if (isPresent(token)) {
return _createDependency(type, false, false, depProps); return _createDependency(token, asPromise, lazy, optional, depProps);
} else { } else {
throw new NoAnnotationError(typeOrFunc); throw new NoAnnotationError(typeOrFunc);
} }
} }
function _createDependency(token, asPromise, lazy, depProps):Dependency { function _createDependency(token, asPromise, lazy, optional, depProps):Dependency {
return new Dependency(Key.get(token), asPromise, lazy, depProps); return new Dependency(Key.get(token), asPromise, lazy, optional, depProps);
} }

View File

@ -38,11 +38,15 @@ export class Injector {
} }
get(token) { 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) { asyncGet(token) {
return this._getByKey(Key.get(token), true, false); return this._getByKey(Key.get(token), true, false, false);
} }
createChild(bindings:List):Injector { createChild(bindings:List):Injector {
@ -60,9 +64,9 @@ export class Injector {
return ListWrapper.createFixedSize(Key.numberOfKeys + 1); return ListWrapper.createFixedSize(Key.numberOfKeys + 1);
} }
_getByKey(key:Key, returnPromise:boolean, returnLazy:boolean) { _getByKey(key:Key, returnPromise:boolean, returnLazy:boolean, optional:boolean) {
if (returnLazy) { if (returnLazy) {
return () => this._getByKey(key, returnPromise, false); return () => this._getByKey(key, returnPromise, false, optional);
} }
var strategy = returnPromise ? this._asyncStrategy : this._syncStrategy; var strategy = returnPromise ? this._asyncStrategy : this._syncStrategy;
@ -74,14 +78,19 @@ export class Injector {
if (isPresent(instance)) return instance; if (isPresent(instance)) return instance;
if (isPresent(this._parent)) { 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 { _resolveDependencies(key:Key, binding:Binding, forceAsync:boolean):List {
try { 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); return ListWrapper.map(binding.dependencies, getDependency);
} catch (e) { } catch (e) {
this._clear(key); this._clear(key);

View File

@ -5,7 +5,7 @@ import {ProtoElementInjector, PreBuiltObjects, DirectiveBinding} from 'angular2/
import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility'; import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility';
import {EventEmitter} from 'angular2/src/core/annotations/events'; import {EventEmitter} from 'angular2/src/core/annotations/events';
import {onDestroy} from 'angular2/src/core/annotations/annotations'; 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 {View} from 'angular2/src/core/compiler/view';
import {ViewContainer} from 'angular2/src/core/compiler/view_container'; import {ViewContainer} from 'angular2/src/core/compiler/view_container';
import {NgElement} from 'angular2/src/core/dom/element'; 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 { class NeedDirectiveFromParent {
dependency:SimpleDirective; dependency:SimpleDirective;
constructor(@Parent() dependency:SimpleDirective){ constructor(@Parent() dependency:SimpleDirective){
@ -342,6 +349,12 @@ export function main() {
toThrowError('No provider for SimpleDirective! (NeedDirectiveFromParent -> SimpleDirective)'); 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 () { it("should accept SimpleDirective bindings instead of SimpleDirective types", function () {
var inj = injector([ var inj = injector([
DirectiveBinding.createFromBinding(bind(SimpleDirective).toClass(SimpleDirective), null) DirectiveBinding.createFromBinding(bind(SimpleDirective).toClass(SimpleDirective), null)

View File

@ -1,5 +1,5 @@
import {describe, ddescribe, it, iit, expect, beforeEach} from 'angular2/test_lib'; 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 { class Engine {
} }
@ -34,6 +34,13 @@ class CarWithLazyEngine {
} }
} }
class CarWithOptionalEngine {
engine;
constructor(@Optional() engine:Engine) {
this.engine = engine;
}
}
class CarWithDashboard { class CarWithDashboard {
engine:Engine; engine:Engine;
dashboard:Dashboard; dashboard:Dashboard;
@ -159,6 +166,15 @@ export function main() {
expect(car.engine).toBeAnInstanceOf(Engine); 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 () { it("should flatten passed-in bindings", function () {
var injector = new Injector([ var injector = new Injector([
[[Engine, Car]] [[Engine, Car]]