refactor(di): unified di injector and core injector

BREAKING CHANGES:

* InjectAsync and InjectLazy have been removed
* toAsyncFactory has been removed
This commit is contained in:
vsavkin
2015-06-26 15:59:18 -07:00
parent b688dee4c8
commit 22d3943831
49 changed files with 1211 additions and 1669 deletions
+4 -21
View File
@@ -85,6 +85,7 @@ export function main() {
inject([AsyncTestCompleter], (async) => {
var refPromise =
bootstrap(HelloRootDirectiveIsNotCmp, testBindings, (e, t) => { throw e; });
PromiseWrapper.then(refPromise, null, (reason) => {
expect(reason.message)
.toContain(
@@ -108,24 +109,6 @@ export function main() {
expect(refPromise).not.toBe(null);
});
it('should resolve an injector promise and contain bindings',
inject([AsyncTestCompleter], (async) => {
var refPromise = bootstrap(HelloRootCmp, testBindings);
refPromise.then((ref) => {
expect(ref.injector.get(HelloRootCmp)).toBeAnInstanceOf(HelloRootCmp);
async.done();
});
}));
it('should provide the application component in the injector',
inject([AsyncTestCompleter], (async) => {
var refPromise = bootstrap(HelloRootCmp, testBindings);
refPromise.then((ref) => {
expect(ref.injector.get(HelloRootCmp)).toBeAnInstanceOf(HelloRootCmp);
async.done();
});
}));
it('should display hello world', inject([AsyncTestCompleter], (async) => {
var refPromise = bootstrap(HelloRootCmp, testBindings);
refPromise.then((ref) => {
@@ -151,7 +134,7 @@ export function main() {
bootstrap(HelloRootCmp3, [testBindings, bind("appBinding").toValue("BoundValue")]);
refPromise.then((ref) => {
expect(ref.injector.get(HelloRootCmp3).appBinding).toEqual("BoundValue");
expect(ref.hostComponent.appBinding).toEqual("BoundValue");
async.done();
});
}));
@@ -161,7 +144,7 @@ export function main() {
var refPromise = bootstrap(HelloRootCmp4, testBindings);
refPromise.then((ref) => {
expect(ref.injector.get(HelloRootCmp4).lc).toBe(ref.injector.get(LifeCycle));
expect(ref.hostComponent.lc).toBe(ref.injector.get(LifeCycle));
async.done();
});
}));
@@ -183,7 +166,7 @@ export function main() {
.then((refs: ApplicationRef[]) => {
var registry = refs[0].injector.get(TestabilityRegistry);
var testabilities =
[refs[0].injector.asyncGet(Testability), refs[1].injector.asyncGet(Testability)];
[refs[0].injector.get(Testability), refs[1].injector.get(Testability)];
PromiseWrapper.all(testabilities)
.then((testabilities: Testability[]) => {
expect(registry.findTestabilityInTree(el)).toEqual(testabilities[0]);
@@ -34,17 +34,13 @@ import {
} from 'angular2/src/core/compiler/element_injector';
import * as dirAnn from 'angular2/src/core/annotations_impl/annotations';
import {
Parent,
Ancestor,
Unbounded,
Attribute,
Query,
Component,
Directive,
onDestroy
} from 'angular2/annotations';
import * as ngDiAnn from 'angular2/src/core/annotations_impl/visibility';
import {bind, Injector, Binding, resolveBindings, Optional, Inject, Injectable} from 'angular2/di';
import {bind, Injector, Binding, resolveBindings, Optional, Inject, Injectable, Self, Parent, Ancestor, Unbounded, self} from 'angular2/di';
import * as diAnn from 'angular2/src/di/annotations_impl';
import {AppProtoView, AppView} from 'angular2/src/core/compiler/view';
import {ViewContainerRef} from 'angular2/src/core/compiler/view_container_ref';
@@ -78,13 +74,16 @@ class DummyElementRef extends SpyObject {
noSuchMethod(m) { return super.noSuchMethod(m); }
}
@Injectable(self)
class SimpleDirective {}
class SimpleService {}
@Injectable(self)
class SomeOtherDirective {}
var _constructionCount = 0;
@Injectable(self)
class CountingDirective {
count;
constructor() {
@@ -93,6 +92,7 @@ class CountingDirective {
}
}
@Injectable(self)
class FancyCountingDirective extends CountingDirective {
constructor() { super(); }
}
@@ -139,6 +139,12 @@ class NeedsService {
constructor(@Inject("service") service) { this.service = service; }
}
@Injectable()
class NeedsAncestorService {
service: any;
constructor(@Ancestor() @Inject("service") service) { this.service = service; }
}
class HasEventEmitter {
emitter;
constructor() { this.emitter = "emitter"; }
@@ -581,7 +587,6 @@ export function main() {
expect(d.dependency).toBeAnInstanceOf(SimpleDirective);
});
it("should instantiate hostInjector injectables that have dependencies with set visibility",
function() {
var childInj = parentChildInjectors(
@@ -597,7 +602,7 @@ export function main() {
bind('injectable2')
.toFactory(
(val) => `${val}-injectable2`,
[[new diAnn.Inject('injectable1'), new ngDiAnn.Parent()]])
[[new diAnn.Inject('injectable1'), new diAnn.Parent()]])
]
}))]);
expect(childInj.get('injectable2')).toEqual('injectable1-injectable2');
@@ -648,7 +653,8 @@ export function main() {
expect(shadowInj.get(NeedsService).service).toEqual('hostService');
});
it("should not instantiate a directive in a view that depends on hostInjector bindings of a decorator directive", () => {
it("should not instantiate a directive in a view that has an ancestor dependency on hostInjector"+
" bindings of a decorator directive", () => {
expect(() => {
hostShadowInjectors(
ListWrapper.concat([
@@ -657,7 +663,7 @@ export function main() {
hostInjector: [bind('service').toValue('hostService')]})
)], extraBindings),
ListWrapper.concat([NeedsService], extraBindings)
ListWrapper.concat([NeedsAncestorService], extraBindings)
);
}).toThrowError(new RegExp("No provider for service!"));
});
@@ -55,7 +55,8 @@ main() {
});
describe('Error handling', () {
it('should preserve Error stack traces thrown from components', inject([
//TODO: vsavkin reenable this test after merging DI and EI
xit('should preserve Error stack traces thrown from components', inject([
TestComponentBuilder,
AsyncTestCompleter
], (tb, async) {
@@ -69,7 +70,8 @@ main() {
});
}));
it('should preserve non-Error stack traces thrown from components', inject([
//TODO: vsavkin reenable this test after merging DI and EI
xit('should preserve non-Error stack traces thrown from components', inject([
TestComponentBuilder,
AsyncTestCompleter
], (tb, async) {
@@ -33,7 +33,18 @@ import {
} from 'angular2/src/facade/lang';
import {PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {Injector, bind, Injectable, Binding, forwardRef, OpaqueToken, Inject} from 'angular2/di';
import {
Injector,
bind,
Injectable,
Binding,
forwardRef,
OpaqueToken,
Inject,
Parent,
Ancestor,
Unbounded
} from 'angular2/di';
import {
PipeFactory,
PipeRegistry,
@@ -45,18 +56,9 @@ import {
ON_PUSH
} from 'angular2/change_detection';
import {
Directive,
Component,
View,
Parent,
Ancestor,
Unbounded,
Attribute,
Query
} from 'angular2/annotations';
import {Directive, Component, View, Attribute, Query} from 'angular2/annotations';
import * as viewAnn from 'angular2/src/core/annotations_impl/view';
import * as visAnn from 'angular2/src/core/annotations_impl/visibility';
import * as visAnn from 'angular2/src/di/annotations_impl';
import {QueryList} from 'angular2/src/core/compiler/query_list';
@@ -156,7 +156,7 @@ export function main() {
}
function directiveBinding({metadata}: {metadata?: any} = {}) {
return new DirectiveBinding(Key.get("dummy"), null, [], false, [], [], [], metadata);
return new DirectiveBinding(Key.get("dummy"), null, [], [], [], [], metadata);
}
function createRenderProtoView(elementBinders = null, type: renderApi.ViewType = null) {
@@ -210,7 +210,7 @@ export function main() {
});
it('should hydrate the view', () => {
var injector = new Injector([], null, false);
var injector = Injector.resolveAndCreate([]);
manager.createRootHostView(wrapPv(hostProtoView), null, injector);
expect(utils.spy('hydrateRootHostView')).toHaveBeenCalledWith(createdViews[0], injector);
expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(createdViews[0].render);
@@ -301,7 +301,7 @@ export function main() {
});
it('should hydrate the view', () => {
var injector = new Injector([], null, false);
var injector = Injector.resolveAndCreate([]);
var contextView =
createView(createProtoView([createEmptyElBinder(), createEmptyElBinder()]));
manager.createViewInContainer(elementRef(parentView, 0), 0, wrapPv(childProtoView),
@@ -42,7 +42,7 @@ export function main() {
var directiveResolver;
var utils;
function createInjector() { return new Injector([], null, false); }
function createInjector() { return Injector.resolveAndCreate([]); }
function createDirectiveBinding(type) {
var annotation = directiveResolver.resolve(type);
-176
View File
@@ -1,176 +0,0 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
expect,
iit,
inject,
it,
xit,
} from 'angular2/test_lib';
import {Injector, bind, Key} from 'angular2/di';
import {Inject, InjectPromise, Injectable} from 'angular2/src/di/decorators';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {stringify} from 'angular2/src/facade/lang';
class UserList {}
function fetchUsers() {
return PromiseWrapper.resolve(new UserList());
}
class SynchronousUserList {}
@Injectable()
class UserController {
list: UserList;
constructor(list: UserList) { this.list = list; }
}
@Injectable()
class AsyncUserController {
userList;
constructor(@InjectPromise(UserList) userList) { this.userList = userList; }
}
export function main() {
describe("async injection", function() {
describe("asyncGet", function() {
it('should return a promise', function() {
var injector = Injector.resolveAndCreate([bind(UserList).toAsyncFactory(fetchUsers)]);
var p = injector.asyncGet(UserList);
expect(p).toBePromise();
});
it('should return a promise when the binding is sync', function() {
var injector = Injector.resolveAndCreate([SynchronousUserList]);
var p = injector.asyncGet(SynchronousUserList);
expect(p).toBePromise();
});
it("should return a promise when the binding is sync (from cache)", function() {
var injector = Injector.resolveAndCreate([UserList]);
expect(injector.get(UserList)).toBeAnInstanceOf(UserList);
expect(injector.asyncGet(UserList)).toBePromise();
});
it('should return the injector', inject([AsyncTestCompleter], (async) => {
var injector = Injector.resolveAndCreate([]);
var p = injector.asyncGet(Injector);
p.then(function(injector) {
expect(injector).toBe(injector);
async.done();
});
}));
it('should return a promise when instantiating a sync binding ' +
'with an async dependency',
inject([AsyncTestCompleter], (async) => {
var injector = Injector.resolveAndCreate(
[bind(UserList).toAsyncFactory(fetchUsers), UserController]);
injector.asyncGet(UserController)
.then(function(userController) {
expect(userController).toBeAnInstanceOf(UserController);
expect(userController.list).toBeAnInstanceOf(UserList);
async.done();
});
}));
it("should create only one instance (async + async)",
inject([AsyncTestCompleter], (async) => {
var injector = Injector.resolveAndCreate([bind(UserList).toAsyncFactory(fetchUsers)]);
var ul1 = injector.asyncGet(UserList);
var ul2 = injector.asyncGet(UserList);
PromiseWrapper.all([ul1, ul2])
.then(function(uls) {
expect(uls[0]).toBe(uls[1]);
async.done();
});
}));
it("should create only one instance (sync + async)", inject([AsyncTestCompleter], (async) => {
var injector = Injector.resolveAndCreate([UserList]);
var promise = injector.asyncGet(UserList);
var ul = injector.get(UserList);
expect(promise).toBePromise();
expect(ul).toBeAnInstanceOf(UserList);
promise.then(function(ful) {
expect(ful).toBe(ul);
async.done();
});
}));
it('should show the full path when error happens in a constructor',
inject([AsyncTestCompleter], (async) => {
var injector = Injector.resolveAndCreate([
UserController,
bind(UserList).toAsyncFactory(function() { throw "Broken UserList"; })
]);
var promise = injector.asyncGet(UserController);
PromiseWrapper.then(promise, null, function(e) {
expect(e.message).toContain(
`Error during instantiation of UserList! (${stringify(UserController)} -> UserList)`);
async.done();
});
}));
});
describe("get", function() {
it('should throw when instantiating an async binding', function() {
var injector = Injector.resolveAndCreate([bind(UserList).toAsyncFactory(fetchUsers)]);
expect(() => injector.get(UserList))
.toThrowError(
'Cannot instantiate UserList synchronously. It is provided as a promise!');
});
it('should throw when instantiating a sync binding with an async dependency', function() {
var injector =
Injector.resolveAndCreate([bind(UserList).toAsyncFactory(fetchUsers), UserController]);
expect(() => injector.get(UserController))
.toThrowError(new RegExp(
'Cannot instantiate UserList synchronously. It is provided as a promise!'));
});
it('should not throw when instantiating a sync binding with a resolved async dependency',
inject([AsyncTestCompleter], (async) => {
var injector = Injector.resolveAndCreate(
[bind(UserList).toAsyncFactory(fetchUsers), UserController]);
injector.asyncGet(UserList).then((_) => {
expect(() => { injector.get(UserController); }).not.toThrow();
async.done();
});
}));
it('should resolve synchronously when an async dependency requested as a promise',
function() {
var injector = Injector.resolveAndCreate(
[bind(UserList).toAsyncFactory(fetchUsers), AsyncUserController]);
var controller = injector.get(AsyncUserController);
expect(controller).toBeAnInstanceOf(AsyncUserController);
expect(controller.userList).toBePromise();
});
it('should wrap sync dependencies into promises if required', function() {
var injector = Injector.resolveAndCreate(
[bind(UserList).toFactory(() => new UserList()), AsyncUserController]);
var controller = injector.get(AsyncUserController);
expect(controller).toBeAnInstanceOf(AsyncUserController);
expect(controller.userList).toBePromise();
});
});
});
}
@@ -28,14 +28,10 @@ main() {
expect(const Binding(Foo, toFactory: fn).toFactory).toBe(fn);
});
it('can create constant from async factory', () {
expect(const Binding(Foo, toAsyncFactory: fn).toAsyncFactory).toBe(fn);
});
it('can be used in annotation', () {
ClassMirror mirror = reflectType(Annotated);
var bindings = mirror.metadata[0].reflectee.bindings;
expect(bindings.length).toBe(6);
expect(bindings.length).toBe(5);
bindings.forEach((b) {
expect(b).toBeA(Binding);
});
@@ -57,7 +53,6 @@ class Annotation {
const Binding(Foo, toClass: Bar),
const Binding(Foo, toValue: 5),
const Binding(Foo, toAlias: Bar),
const Binding(Foo, toFactory: fn),
const Binding(Foo, toAsyncFactory: fn),
const Binding(Foo, toFactory: fn)
])
class Annotated {}
+29 -69
View File
@@ -9,13 +9,22 @@ import {
DependencyAnnotation,
Injectable
} from 'angular2/di';
import {Optional, Inject, InjectLazy} from 'angular2/src/di/decorators';
import {Optional, Inject} from 'angular2/src/di/decorators';
import * as ann from 'angular2/src/di/annotations_impl';
class CustomDependencyAnnotation extends DependencyAnnotation {}
class Engine {}
@Injectable(ann.self)
class EngineWithSetVisibility {
}
@Injectable()
class CarNeedsEngineWithSetVisibility {
constructor(engine: EngineWithSetVisibility) {}
}
class BrokenEngine {
constructor() { throw new BaseException("Broken Engine"); }
}
@@ -35,12 +44,6 @@ class Car {
constructor(engine: Engine) { this.engine = engine; }
}
@Injectable()
class CarWithLazyEngine {
engineFactory;
constructor(@InjectLazy(Engine) engineFactory) { this.engineFactory = engineFactory; }
}
@Injectable()
class CarWithOptionalEngine {
engine;
@@ -236,10 +239,6 @@ export function main() {
expect(() => injector.get(Car))
.toThrowError(
`Cannot instantiate cyclic dependency! (${stringify(Car)} -> ${stringify(Engine)} -> ${stringify(Car)})`);
expect(() => injector.asyncGet(Car))
.toThrowError(
`Cannot instantiate cyclic dependency! (${stringify(Car)} -> ${stringify(Engine)} -> ${stringify(Car)})`);
});
it('should show the full path when error happens in a constructor', () => {
@@ -274,25 +273,6 @@ export function main() {
expect(injector.get('null')).toBe(null);
});
describe("default bindings", () => {
it("should be used when no matching binding found", () => {
var injector = Injector.resolveAndCreate([], {defaultBindings: true});
var car = injector.get(Car);
expect(car).toBeAnInstanceOf(Car);
});
it("should use the matching binding when it is available", () => {
var injector =
Injector.resolveAndCreate([bind(Car).toClass(SportsCar)], {defaultBindings: true});
var car = injector.get(Car);
expect(car).toBeAnInstanceOf(SportsCar);
});
});
describe("child", () => {
it('should load instances from parent injector', () => {
var parent = Injector.resolveAndCreate([Engine]);
@@ -324,17 +304,6 @@ export function main() {
expect(engineFromChild).toBeAnInstanceOf(TurboEngine);
});
it("should create child injectors without default bindings", () => {
var parent = Injector.resolveAndCreate([], {defaultBindings: true});
var child = parent.resolveAndCreateChild([]);
// child delegates to parent the creation of Car
var childCar = child.get(Car);
var parentCar = parent.get(Car);
expect(childCar).toBe(parentCar);
});
it("should give access to direct parent", () => {
var parent = Injector.resolveAndCreate([]);
var child = parent.resolveAndCreateChild([]);
@@ -342,25 +311,6 @@ export function main() {
});
});
describe("lazy", () => {
it("should create dependencies lazily", () => {
var injector = Injector.resolveAndCreate([Engine, CarWithLazyEngine]);
var car = injector.get(CarWithLazyEngine);
expect(car.engineFactory()).toBeAnInstanceOf(Engine);
});
it("should cache instance created lazily", () => {
var injector = Injector.resolveAndCreate([Engine, CarWithLazyEngine]);
var car = injector.get(CarWithLazyEngine);
var e1 = car.engineFactory();
var e2 = car.engineFactory();
expect(e1).toBe(e2);
});
});
describe('resolve', () => {
it('should resolve and flatten', () => {
var bindings = Injector.resolve([Engine, [BrokenEngine]]);
@@ -374,20 +324,16 @@ export function main() {
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)])
bind(forwardRef(() => String)).toFactory(() => 'OK', [forwardRef(() => Engine)])
]);
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];
var engineBinding = bindings[0];
var brokenEngineBinding = bindings[1];
var stringBinding = bindings[2];
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));
});
it('should support overriding factory dependencies with dependency annotations', () => {
@@ -396,11 +342,25 @@ export function main() {
.toFactory((e) => "result",
[[new ann.Inject("dep"), new CustomDependencyAnnotation()]])
]);
var binding = bindings[Key.get("token").id];
var binding = bindings[0];
expect(binding.dependencies[0].key.token).toEqual("dep");
expect(binding.dependencies[0].properties).toEqual([new CustomDependencyAnnotation()]);
});
});
describe("default visibility", () => {
it("should use the provided visibility", () => {
var bindings = Injector.resolve([CarNeedsEngineWithSetVisibility, EngineWithSetVisibility]);
var carBinding = bindings[0];
expect(carBinding.dependencies[0].visibility).toEqual(ann.self);
});
it("should set the default visibility to unbounded", () => {
var bindings = Injector.resolve([Car, Engine]);
var carBinding = bindings[0];
expect(carBinding.dependencies[0].visibility).toEqual(ann.unbounded);
});
});
});
}
@@ -56,7 +56,7 @@ export function main() {
var router = applicationRef.hostComponent.router;
PromiseWrapper.catchError(router.navigate('/cause-error'), (error) => {
expect(el).toHaveText('outer { oh no }');
expect(error.message).toBe('oops!');
expect(error.message).toContain('oops!');
async.done();
});
});
@@ -89,7 +89,6 @@ export function main() {
router.navigate('/parent/child');
});
}));
// TODO: add a test in which the child component has bindings
});
}
@@ -107,14 +106,12 @@ class AppCmp {
constructor(public router: Router, public location: LocationStrategy) {}
}
@Component({selector: 'parent-cmp'})
@View({template: `parent { <router-outlet></router-outlet> }`, directives: routerDirectives})
@RouteConfig([{path: '/child', component: HelloCmp}])
class ParentCmp {
}
@Component({selector: 'app-cmp'})
@View({template: `root { <router-outlet></router-outlet> }`, directives: routerDirectives})
@RouteConfig([{path: '/parent/...', component: ParentCmp}])