feat: allow for forward references in injection
It is possible for a class defined first to be referencing a class defined later, and as a result at the time of the definition it is not possible to access the later's class reference. This allows to refer to the later defined class through a closure.Closes #1891
This commit is contained in:
parent
0e04467b8a
commit
1eea2b254e
|
@ -3,6 +3,7 @@ export * from './core';
|
||||||
export * from './annotations';
|
export * from './annotations';
|
||||||
export * from './directives';
|
export * from './directives';
|
||||||
export * from './forms';
|
export * from './forms';
|
||||||
|
export * from './di';
|
||||||
export {Observable, EventEmitter} from 'angular2/src/facade/async';
|
export {Observable, EventEmitter} from 'angular2/src/facade/async';
|
||||||
export * from 'angular2/src/render/api';
|
export * from 'angular2/src/render/api';
|
||||||
export {DomRenderer, DOCUMENT_TOKEN} from 'angular2/src/render/dom/dom_renderer';
|
export {DomRenderer, DOCUMENT_TOKEN} from 'angular2/src/render/dom/dom_renderer';
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
export * from './src/di/annotations';
|
export * from './src/di/annotations';
|
||||||
export * from './src/di/decorators';
|
export * from './src/di/decorators';
|
||||||
|
export * from './src/di/forward_ref';
|
||||||
export {Injector} from './src/di/injector';
|
export {Injector} from './src/di/injector';
|
||||||
export {Binding, ResolvedBinding, Dependency, bind} from './src/di/binding';
|
export {Binding, ResolvedBinding, Dependency, bind} from './src/di/binding';
|
||||||
export {Key, KeyRegistry, TypeLiteral} from './src/di/key';
|
export {Key, KeyRegistry, TypeLiteral} from './src/di/key';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {Binding} from 'angular2/di';
|
import {Binding, resolveForwardRef} from 'angular2/di';
|
||||||
import {Injectable} from 'angular2/src/di/annotations_impl';
|
import {Injectable} from 'angular2/src/di/annotations_impl';
|
||||||
import {Type, isBlank, isPresent, BaseException, normalizeBlank, stringify} from 'angular2/src/facade/lang';
|
import {Type, isBlank, isPresent, BaseException, normalizeBlank, stringify} from 'angular2/src/facade/lang';
|
||||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||||
|
@ -237,7 +237,7 @@ export class Compiler {
|
||||||
|
|
||||||
_flattenList(tree:List<any>, out:List<any> /*<Type|Binding>*/):void {
|
_flattenList(tree:List<any>, out:List<any> /*<Type|Binding>*/):void {
|
||||||
for (var i = 0; i < tree.length; i++) {
|
for (var i = 0; i < tree.length; i++) {
|
||||||
var item = tree[i];
|
var item = resolveForwardRef(tree[i]);
|
||||||
if (ListWrapper.isList(item)) {
|
if (ListWrapper.isList(item)) {
|
||||||
this._flattenList(item, out);
|
this._flattenList(item, out);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {Injectable} from 'angular2/src/di/annotations_impl';
|
import {Injectable} from 'angular2/src/di/annotations_impl';
|
||||||
|
import {resolveForwardRef} from 'angular2/di';
|
||||||
import {Type, isPresent, BaseException, stringify} from 'angular2/src/facade/lang';
|
import {Type, isPresent, BaseException, stringify} from 'angular2/src/facade/lang';
|
||||||
import {Directive} from '../annotations_impl/annotations';
|
import {Directive} from '../annotations_impl/annotations';
|
||||||
import {reflector} from 'angular2/src/reflection/reflection';
|
import {reflector} from 'angular2/src/reflection/reflection';
|
||||||
|
@ -6,7 +7,7 @@ import {reflector} from 'angular2/src/reflection/reflection';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DirectiveResolver {
|
export class DirectiveResolver {
|
||||||
resolve(type:Type):Directive {
|
resolve(type:Type):Directive {
|
||||||
var annotations = reflector.annotations(type);
|
var annotations = reflector.annotations(resolveForwardRef(type));
|
||||||
if (isPresent(annotations)) {
|
if (isPresent(annotations)) {
|
||||||
for (var i=0; i<annotations.length; i++) {
|
for (var i=0; i<annotations.length; i++) {
|
||||||
var annotation = annotations[i];
|
var annotation = annotations[i];
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||||
import {Math} from 'angular2/src/facade/math';
|
import {Math} from 'angular2/src/facade/math';
|
||||||
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||||
import {Injector, Key, Dependency, bind, Binding, ResolvedBinding, NoBindingError,
|
import {Injector, Key, Dependency, bind, Binding, ResolvedBinding, NoBindingError,
|
||||||
AbstractBindingError, CyclicDependencyError} from 'angular2/di';
|
AbstractBindingError, CyclicDependencyError, resolveForwardRef} from 'angular2/di';
|
||||||
import {Parent, Ancestor} from 'angular2/src/core/annotations_impl/visibility';
|
import {Parent, Ancestor} from 'angular2/src/core/annotations_impl/visibility';
|
||||||
import {Attribute, Query} from 'angular2/src/core/annotations_impl/di';
|
import {Attribute, Query} from 'angular2/src/core/annotations_impl/di';
|
||||||
import * as viewModule from './view';
|
import * as viewModule from './view';
|
||||||
|
@ -220,7 +220,7 @@ export class DirectiveDependency extends Dependency {
|
||||||
|
|
||||||
static _query(properties) {
|
static _query(properties) {
|
||||||
var p = ListWrapper.find(properties, (p) => p instanceof Query);
|
var p = ListWrapper.find(properties, (p) => p instanceof Query);
|
||||||
return isPresent(p) ? p.directive : null;
|
return isPresent(p) ? resolveForwardRef(p.directive) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
DependencyAnnotation
|
DependencyAnnotation
|
||||||
} from './annotations_impl';
|
} from './annotations_impl';
|
||||||
import {NoAnnotationError} from './exceptions';
|
import {NoAnnotationError} from './exceptions';
|
||||||
|
import {resolveForwardRef} from './forward_ref';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
|
@ -217,8 +218,9 @@ export class Binding {
|
||||||
var resolvedDeps;
|
var resolvedDeps;
|
||||||
var isAsync = false;
|
var isAsync = false;
|
||||||
if (isPresent(this.toClass)) {
|
if (isPresent(this.toClass)) {
|
||||||
factoryFn = reflector.factory(this.toClass);
|
var toClass = resolveForwardRef(this.toClass);
|
||||||
resolvedDeps = _dependenciesFor(this.toClass);
|
factoryFn = reflector.factory(toClass);
|
||||||
|
resolvedDeps = _dependenciesFor(toClass);
|
||||||
} else if (isPresent(this.toAlias)) {
|
} else if (isPresent(this.toAlias)) {
|
||||||
factoryFn = (aliasInstance) => aliasInstance;
|
factoryFn = (aliasInstance) => aliasInstance;
|
||||||
resolvedDeps = [Dependency.fromKey(Key.get(this.toAlias))];
|
resolvedDeps = [Dependency.fromKey(Key.get(this.toAlias))];
|
||||||
|
@ -234,7 +236,8 @@ export class Binding {
|
||||||
resolvedDeps = _EMPTY_LIST;
|
resolvedDeps = _EMPTY_LIST;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ResolvedBinding(Key.get(this.token), factoryFn, resolvedDeps, isAsync);
|
return new ResolvedBinding(Key.get(resolveForwardRef(this.token)), factoryFn, resolvedDeps,
|
||||||
|
isAsync);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -428,7 +431,8 @@ export class BindingBuilder {
|
||||||
function _constructDependencies(factoryFunction: Function, dependencies: List<any>) {
|
function _constructDependencies(factoryFunction: Function, dependencies: List<any>) {
|
||||||
return isBlank(dependencies) ?
|
return isBlank(dependencies) ?
|
||||||
_dependenciesFor(factoryFunction) :
|
_dependenciesFor(factoryFunction) :
|
||||||
ListWrapper.map(dependencies, (t) => Dependency.fromKey(Key.get(t)));
|
ListWrapper.map(dependencies,
|
||||||
|
(t) => Dependency.fromKey(Key.get(resolveForwardRef(t))));
|
||||||
}
|
}
|
||||||
|
|
||||||
function _dependenciesFor(typeOrFunc): List<any> {
|
function _dependenciesFor(typeOrFunc): List<any> {
|
||||||
|
@ -475,6 +479,8 @@ function _extractToken(typeOrFunc, annotations) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token = resolveForwardRef(token);
|
||||||
|
|
||||||
if (isPresent(token)) {
|
if (isPresent(token)) {
|
||||||
return _createDependency(token, asPromise, lazy, optional, depProps);
|
return _createDependency(token, asPromise, lazy, optional, depProps);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
library angular2.di.forward_ref;
|
||||||
|
|
||||||
|
typedef Type ForwardRefFn();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dart does not have the forward ref problem, so this function is a noop.
|
||||||
|
*/
|
||||||
|
forwardRef(ForwardRefFn forwardRefFn) => forwardRefFn();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily retrieve the reference value.
|
||||||
|
*
|
||||||
|
* See: {@link forwardRef}
|
||||||
|
*
|
||||||
|
* @exportedAs angular2/di
|
||||||
|
*/
|
||||||
|
resolveForwardRef(type) => type;
|
|
@ -0,0 +1,50 @@
|
||||||
|
import {Type} from 'angular2/src/facade/lang';
|
||||||
|
|
||||||
|
export interface ForwardRefFn { (): Type; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to refer to references which are not yet defined.
|
||||||
|
*
|
||||||
|
* This situation arises when the key which we need te refer to for the purposes of DI is declared,
|
||||||
|
* but not yet defined.
|
||||||
|
*
|
||||||
|
* ## Example:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* class Door {
|
||||||
|
* // Incorrect way to refer to a reference which is defined later.
|
||||||
|
* // This fails because `Lock` is undefined at this point.
|
||||||
|
* constructor(lock:Lock) { }
|
||||||
|
*
|
||||||
|
* // Correct way to refer to a reference which is defined later.
|
||||||
|
* // The reference needs to be captured in a closure.
|
||||||
|
* constructor(@Inject(forwardRef(() => Lock)) lock:Lock) { }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Only at this point the lock is defined.
|
||||||
|
* class Lock {
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @exportedAs angular2/di
|
||||||
|
*/
|
||||||
|
export function forwardRef(forwardRefFn: ForwardRefFn): Type {
|
||||||
|
(<any>forwardRefFn).__forward_ref__ = forwardRef;
|
||||||
|
return (<Type><any>forwardRefFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily retrieve the reference value.
|
||||||
|
*
|
||||||
|
* See: {@link forwardRef}
|
||||||
|
*
|
||||||
|
* @exportedAs angular2/di
|
||||||
|
*/
|
||||||
|
export function resolveForwardRef(type: any): any {
|
||||||
|
if (typeof type == 'function' && type.hasOwnProperty('__forward_ref__') &&
|
||||||
|
type.__forward_ref__ === forwardRef) {
|
||||||
|
return (<ForwardRefFn>type)();
|
||||||
|
} else {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import {FunctionWrapper, Type, isPresent, isBlank} from 'angular2/src/facade/lang';
|
import {FunctionWrapper, Type, isPresent, isBlank} from 'angular2/src/facade/lang';
|
||||||
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
|
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
|
||||||
import {Key} from './key';
|
import {Key} from './key';
|
||||||
|
import {resolveForwardRef} from './forward_ref';
|
||||||
|
|
||||||
var _constructing = new Object();
|
var _constructing = new Object();
|
||||||
var _notFound = new Object();
|
var _notFound = new Object();
|
||||||
|
@ -370,7 +371,7 @@ class _AsyncInjectorStrategy {
|
||||||
function _resolveBindings(bindings: List<any>): List<ResolvedBinding> {
|
function _resolveBindings(bindings: List<any>): List<ResolvedBinding> {
|
||||||
var resolvedList = ListWrapper.createFixedSize(bindings.length);
|
var resolvedList = ListWrapper.createFixedSize(bindings.length);
|
||||||
for (var i = 0; i < bindings.length; i++) {
|
for (var i = 0; i < bindings.length; i++) {
|
||||||
var unresolved = bindings[i];
|
var unresolved = resolveForwardRef(bindings[i]);
|
||||||
var resolved;
|
var resolved;
|
||||||
if (unresolved instanceof ResolvedBinding) {
|
if (unresolved instanceof ResolvedBinding) {
|
||||||
resolved = unresolved; // ha-ha! I'm easily amused
|
resolved = unresolved; // ha-ha! I'm easily amused
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {MapWrapper} from 'angular2/src/facade/collection';
|
import {MapWrapper} from 'angular2/src/facade/collection';
|
||||||
import {stringify, CONST, Type} from 'angular2/src/facade/lang';
|
import {stringify, CONST, Type, isBlank, BaseException} from 'angular2/src/facade/lang';
|
||||||
import {TypeLiteral} from './type_literal';
|
import {TypeLiteral} from './type_literal';
|
||||||
|
|
||||||
export {TypeLiteral} from './type_literal';
|
export {TypeLiteral} from './type_literal';
|
||||||
|
@ -26,6 +26,9 @@ export class Key {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
constructor(token: Object, id: number) {
|
constructor(token: Object, id: number) {
|
||||||
|
if (isBlank(token)) {
|
||||||
|
throw new BaseException('Token must be defined!');
|
||||||
|
}
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
/// This file contains tests that make sense only in Dart
|
||||||
|
library angular2.test.core.forward_ref_integration_spec;
|
||||||
|
|
||||||
|
main() {
|
||||||
|
// Don't run in Dart as it is not relevant, and Dart const rules prevent us from expressing it.
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import {
|
||||||
|
AsyncTestCompleter,
|
||||||
|
beforeEach,
|
||||||
|
ddescribe,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
iit,
|
||||||
|
inject,
|
||||||
|
it,
|
||||||
|
xit
|
||||||
|
} from 'angular2/test_lib';
|
||||||
|
import {TestBed} from 'angular2/src/test_lib/test_bed';
|
||||||
|
import {Directive, Component} from 'angular2/src/core/annotations_impl/annotations';
|
||||||
|
import {Query} from 'angular2/src/core/annotations_impl/di';
|
||||||
|
import {View} from 'angular2/src/core/annotations_impl/view';
|
||||||
|
import {QueryList, NgFor} from 'angular2/angular2';
|
||||||
|
import {Inject} from 'angular2/src/di/annotations_impl';
|
||||||
|
import {forwardRef, resolveForwardRef, bind} from 'angular2/di';
|
||||||
|
import {Type} from 'angular2/src/facade/lang';
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
describe("forwardRef integration", function () {
|
||||||
|
it('should instantiate components which are declared using forwardRef', inject(
|
||||||
|
[TestBed, AsyncTestCompleter],
|
||||||
|
(tb, async) => {
|
||||||
|
tb.createView(App).then((view) => {
|
||||||
|
view.detectChanges();
|
||||||
|
expect(view.rootNodes).toHaveText('frame(lock)');
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app',
|
||||||
|
injectables: [
|
||||||
|
forwardRef(() => Frame)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
@View({
|
||||||
|
template: `<door><lock></lock></door>`,
|
||||||
|
directives: [
|
||||||
|
bind(forwardRef(() => Door)).toClass(forwardRef(() => Door)),
|
||||||
|
bind(forwardRef(() => Lock)).toClass(forwardRef(() => Lock))
|
||||||
|
]
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'Lock'
|
||||||
|
})
|
||||||
|
@View({
|
||||||
|
directives: [NgFor],
|
||||||
|
template: `{{frame.name}}(<span *ng-for="var lock of locks">{{lock.name}}</span>)`
|
||||||
|
})
|
||||||
|
class Door {
|
||||||
|
locks: QueryList;
|
||||||
|
frame: Frame;
|
||||||
|
|
||||||
|
constructor(@Query(forwardRef(() => Lock)) locks: QueryList, @Inject(forwardRef(() => Frame)) frame:Frame) {
|
||||||
|
this.frame = frame;
|
||||||
|
this.locks = locks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Frame {
|
||||||
|
name: string;
|
||||||
|
constructor() {
|
||||||
|
this.name = 'frame';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: 'lock'
|
||||||
|
})
|
||||||
|
class Lock {
|
||||||
|
name: string;
|
||||||
|
constructor() {
|
||||||
|
this.name = 'lock';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {
|
||||||
|
AsyncTestCompleter,
|
||||||
|
beforeEach,
|
||||||
|
ddescribe,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
iit,
|
||||||
|
inject,
|
||||||
|
it,
|
||||||
|
xit,
|
||||||
|
} from 'angular2/test_lib';
|
||||||
|
import {forwardRef, resolveForwardRef} from 'angular2/di';
|
||||||
|
import {Type} from 'angular2/src/facade/lang';
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
describe("forwardRef", function () {
|
||||||
|
it('should wrap and unwrap the reference', () => {
|
||||||
|
var ref = forwardRef(() => String);
|
||||||
|
expect(ref instanceof Type).toBe(true);
|
||||||
|
expect(resolveForwardRef(ref)).toBe(String);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import {isBlank, BaseException} from 'angular2/src/facade/lang';
|
import {isBlank, BaseException} from 'angular2/src/facade/lang';
|
||||||
import {describe, ddescribe, it, iit, expect, beforeEach} from 'angular2/test_lib';
|
import {describe, ddescribe, it, iit, expect, beforeEach} from 'angular2/test_lib';
|
||||||
import {Injector, bind, ResolvedBinding} from 'angular2/di';
|
import {Injector, bind, ResolvedBinding, Key, forwardRef} from 'angular2/di';
|
||||||
import {Optional, Inject, InjectLazy} from 'angular2/src/di/annotations_impl';
|
import {Optional, Inject, InjectLazy} from 'angular2/src/di/annotations_impl';
|
||||||
|
|
||||||
|
|
||||||
|
@ -382,13 +382,32 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resolve', function() {
|
describe('resolve', function() {
|
||||||
it('should resolve and flatten', function() {
|
it('should resolve and flatten', () => {
|
||||||
var bindings = Injector.resolve([Engine, [BrokenEngine]]);
|
var bindings = Injector.resolve([Engine, [BrokenEngine]]);
|
||||||
bindings.forEach(function(b) {
|
bindings.forEach(function(b) {
|
||||||
if (isBlank(b)) return; // the result is a sparse array
|
if (isBlank(b)) return; // the result is a sparse array
|
||||||
expect(b instanceof ResolvedBinding).toBe(true);
|
expect(b instanceof ResolvedBinding).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should resolve forward references', () => {
|
||||||
|
var bindings = Injector.resolve([
|
||||||
|
forwardRef(() => Engine),
|
||||||
|
[ bind(forwardRef(() => BrokenEngine)).toClass(forwardRef(() => Engine)) ],
|
||||||
|
bind(forwardRef(() => String)).toFactory(() => 'OK', [forwardRef(() => Engine)]),
|
||||||
|
bind(forwardRef(() => DashboardSoftware)).toAsyncFactory(() => 123, [forwardRef(() => BrokenEngine)])
|
||||||
|
]);
|
||||||
|
|
||||||
|
var engineBinding = bindings[Key.get(Engine).id];
|
||||||
|
var brokenEngineBinding = bindings[Key.get(BrokenEngine).id];
|
||||||
|
var stringBinding = bindings[Key.get(String).id];
|
||||||
|
var dashboardSoftwareBinding = bindings[Key.get(DashboardSoftware).id];
|
||||||
|
|
||||||
|
expect(engineBinding.factory() instanceof Engine).toBe(true);
|
||||||
|
expect(brokenEngineBinding.factory() instanceof Engine).toBe(true);
|
||||||
|
expect(stringBinding.dependencies[0].key).toEqual(Key.get(Engine));
|
||||||
|
expect(dashboardSoftwareBinding.dependencies[0].key).toEqual(Key.get(BrokenEngine));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue