feat(view): implemented loading component next to existing location

This commit is contained in:
vsavkin 2015-04-18 11:18:03 -07:00
parent 77b31ab42f
commit 681d06386d
8 changed files with 222 additions and 204 deletions

View File

@ -75,7 +75,7 @@ function _injectorBindings(appComponentType): List<Binding> {
// We need to do this here to ensure that we create Testability and // We need to do this here to ensure that we create Testability and
// it's ready on the window for users. // it's ready on the window for users.
registry.registerApplication(appElement, testability); registry.registerApplication(appElement, testability);
return dynamicComponentLoader.loadIntoNewLocation(appElement, appComponentAnnotatedType.type, null, injector); return dynamicComponentLoader.loadIntoNewLocation(appElement, appComponentAnnotatedType.type, injector);
}, [DynamicComponentLoader, Injector, appElementToken, appComponentAnnotatedTypeToken, }, [DynamicComponentLoader, Injector, appElementToken, appComponentAnnotatedTypeToken,
Testability, TestabilityRegistry]), Testability, TestabilityRegistry]),

View File

@ -16,11 +16,13 @@ export class ComponentRef {
location:ElementRef; location:ElementRef;
instance:any; instance:any;
componentView:AppView; componentView:AppView;
_dispose:Function;
constructor(location:ElementRef, instance:any, componentView:AppView){ constructor(location:ElementRef, instance:any, componentView:AppView, dispose:Function){
this.location = location; this.location = location;
this.instance = instance; this.instance = instance;
this.componentView = componentView; this.componentView = componentView;
this._dispose = dispose;
} }
get injector() { get injector() {
@ -30,6 +32,10 @@ export class ComponentRef {
get hostView() { get hostView() {
return this.location.hostView; return this.location.hostView;
} }
dispose() {
this._dispose();
}
} }
/** /**
@ -72,16 +78,16 @@ export class DynamicComponentLoader {
// from the component child views // from the component child views
// See ViewFactory.returnView // See ViewFactory.returnView
// See AppViewHydrator.dehydrateDynamicComponentView // See AppViewHydrator.dehydrateDynamicComponentView
return new ComponentRef(location, location.elementInjector.getDynamicallyLoadedComponent(), componentView); var dispose = () => {throw "Not implemented";};
return new ComponentRef(location, location.elementInjector.getDynamicallyLoadedComponent(), componentView, dispose);
}); });
} }
/** /**
* Loads a component as a child of the View given by the provided ElementRef. The loaded * Loads a component in the element specified by elementOrSelector. The loaded component receives
* component receives injection normally as a hosted view. * injection normally as a hosted view.
*/ */
loadIntoNewLocation(elementOrSelector:any, type:Type, location:ElementRef, loadIntoNewLocation(elementOrSelector:any, type:Type, injector:Injector = null):Promise<ComponentRef> {
injector:Injector = null):Promise<ComponentRef> {
this._assertTypeIsComponent(type); this._assertTypeIsComponent(type);
return this._compiler.compileInHost(type).then(hostProtoView => { return this._compiler.compileInHost(type).then(hostProtoView => {
@ -91,9 +97,30 @@ export class DynamicComponentLoader {
// TODO(vsavkin): return a component ref that dehydrates the host view // TODO(vsavkin): return a component ref that dehydrates the host view
// See ViewFactory.returnView // See ViewFactory.returnView
// See AppViewHydrator.dehydrateInPlaceHostView // See AppViewHydrator.dehydrateInPlaceHostView
var newLocation = new ElementRef(hostView.elementInjectors[0]); var newLocation = hostView.elementInjectors[0].getElementRef();
var component = hostView.elementInjectors[0].getComponent(); var component = hostView.elementInjectors[0].getComponent();
return new ComponentRef(newLocation, component, hostView.componentChildViews[0]); var dispose = () => {throw "Not implemented";};
return new ComponentRef(newLocation, component, hostView.componentChildViews[0], dispose);
});
}
/**
* Loads a component next to the provided ElementRef. The loaded component receives
* injection normally as a hosted view.
*/
loadNextToExistingLocation(type:Type, location:ElementRef, injector:Injector = null):Promise<ComponentRef> {
this._assertTypeIsComponent(type);
return this._compiler.compileInHost(type).then(hostProtoView => {
var hostView = location.viewContainer.create(-1, hostProtoView, injector);
var newLocation = hostView.elementInjectors[0].getElementRef();
var component = hostView.elementInjectors[0].getComponent();
var dispose = () => {
var index = location.viewContainer.indexOf(hostView);
location.viewContainer.remove(index);
};
return new ComponentRef(newLocation, component, hostView.componentChildViews[0], dispose);
}); });
} }

View File

@ -672,6 +672,10 @@ export class ElementInjector extends TreeNode {
} }
} }
getElementRef() {
return new ElementRef(this);
}
getDynamicallyLoadedComponent() { getDynamicallyLoadedComponent() {
return this._dynamicallyCreatedComponent; return this._dynamicallyCreatedComponent;
} }
@ -741,7 +745,7 @@ export class ElementInjector extends TreeNode {
if (isPresent(dep.attributeName)) return this._buildAttribute(dep); if (isPresent(dep.attributeName)) return this._buildAttribute(dep);
if (isPresent(dep.queryDirective)) return this._findQuery(dep.queryDirective).list; if (isPresent(dep.queryDirective)) return this._findQuery(dep.queryDirective).list;
if (dep.key.id === StaticKeys.instance().elementRefId) { if (dep.key.id === StaticKeys.instance().elementRefId) {
return new ElementRef(this); return this.getElementRef();
} }
return this._getByKey(dep.key, dep.depth, dep.optional, requestor); return this._getByKey(dep.key, dep.depth, dep.optional, requestor);
} }

View File

@ -92,6 +92,10 @@ export class ViewContainer {
return view; return view;
} }
indexOf(view:viewModule.AppView) {
return ListWrapper.indexOf(this._views, view);
}
remove(atIndex=-1) { remove(atIndex=-1) {
if (atIndex == -1) atIndex = this._views.length - 1; if (atIndex == -1) atIndex = this._views.length - 1;
var view = this._views[atIndex]; var view = this._views[atIndex];

View File

@ -129,6 +129,9 @@ export class ListWrapper {
static filter(array, pred:Function) { static filter(array, pred:Function) {
return array.filter(pred); return array.filter(pred);
} }
static indexOf(array, value, startIndex = -1) {
return array.indexOf(value, startIndex);
}
static any(list:List, pred:Function) { static any(list:List, pred:Function) {
for (var i = 0 ; i < list.length; ++i) { for (var i = 0 ; i < list.length; ++i) {
if (pred(list[i])) return true; if (pred(list[i])) return true;

View File

@ -114,6 +114,9 @@ export class ListWrapper {
} }
return null; return null;
} }
static indexOf(array: List<any>, value, startIndex = -1) {
return array.indexOf(value, startIndex);
}
static reduce<T>(list: List<T>, static reduce<T>(list: List<T>,
fn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, fn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T,
init: T) { init: T) {

View File

@ -11,118 +11,194 @@ import {
inject, inject,
beforeEachBindings, beforeEachBindings,
it, it,
xit, xit
SpyObject, proxy } from 'angular2/test_lib';
} from 'angular2/test_lib';
import {IMPLEMENTS} from 'angular2/src/facade/lang'; import {TestBed} from 'angular2/src/test_lib/test_bed';
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {Decorator, Component, Viewport, DynamicComponent} from 'angular2/src/core/annotations/annotations';
import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader'; import {View} from 'angular2/src/core/annotations/view';
import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader'; import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader';
import {Decorator, Viewport, Component} from 'angular2/src/core/annotations/annotations'; import {ElementRef} from 'angular2/src/core/compiler/element_injector';
import {ElementRef, ElementInjector, ProtoElementInjector, PreBuiltObjects} from 'angular2/src/core/compiler/element_injector'; import {If} from 'angular2/src/directives/if';
import {Compiler} from 'angular2/src/core/compiler/compiler';
import {AppProtoView, AppView} from 'angular2/src/core/compiler/view';
import {ViewFactory} from 'angular2/src/core/compiler/view_factory'
import {AppViewHydrator} from 'angular2/src/core/compiler/view_hydrator';
export function main() { export function main() {
describe("DynamicComponentLoader", () => { describe('DynamicComponentLoader', function () {
var compiler; describe("loading into existing location", () => {
var viewFactory; it('should work', inject([TestBed, AsyncTestCompleter], (tb, async) => {
var directiveMetadataReader; tb.overrideView(MyComp, new View({
var viewHydrator; template: '<dynamic-comp #dynamic></dynamic-comp>',
var loader; directives: [DynamicComp]
}));
beforeEach( () => { tb.createView(MyComp).then((view) => {
compiler = new SpyCompiler(); var dynamicComponent = view.rawView.locals.get("dynamic");
viewFactory = new SpyViewFactory(); expect(dynamicComponent).toBeAnInstanceOf(DynamicComp);
viewHydrator = new SpyAppViewHydrator();
directiveMetadataReader = new DirectiveMetadataReader();
loader = new DynamicComponentLoader(compiler, directiveMetadataReader, viewFactory, viewHydrator);
});
function createProtoView() { dynamicComponent.done.then((_) => {
return new AppProtoView(null, null); view.detectChanges();
} expect(view.rootNodes).toHaveText('hello');
function createEmptyView() {
var view = new AppView(null, null, null, createProtoView(), MapWrapper.create());
view.init(null, [], [], [], []);
return view;
}
function createElementRef(view, boundElementIndex) {
var peli = new ProtoElementInjector(null, boundElementIndex, []);
var eli = new ElementInjector(peli, null);
var preBuiltObjects = new PreBuiltObjects(view, null, null);
eli.instantiateDirectives(null, null, null, preBuiltObjects);
return new ElementRef(eli);
}
describe("loadIntoExistingLocation", () => {
describe('Load errors', () => {
it('should throw when trying to load a decorator', () => {
expect(() => loader.loadIntoExistingLocation(SomeDecorator, null))
.toThrowError("Could not load 'SomeDecorator' because it is not a component.");
});
it('should throw when trying to load a viewport', () => {
expect(() => loader.loadIntoExistingLocation(SomeViewport, null))
.toThrowError("Could not load 'SomeViewport' because it is not a component.");
});
});
it('should compile, create and hydrate the view', inject([AsyncTestCompleter], (async) => {
var log = [];
var protoView = createProtoView();
var hostView = createEmptyView();
var childView = createEmptyView();
viewHydrator.spy('hydrateDynamicComponentView').andCallFake( (hostView, boundElementIndex,
componentView, componentDirective, injector) => {
ListWrapper.push(log, ['hydrateDynamicComponentView', hostView, boundElementIndex, componentView]);
});
viewFactory.spy('getView').andCallFake( (protoView) => {
ListWrapper.push(log, ['getView', protoView]);
return childView;
});
compiler.spy('compile').andCallFake( (_) => PromiseWrapper.resolve(protoView));
var elementRef = createElementRef(hostView, 23);
loader.loadIntoExistingLocation(SomeComponent, elementRef).then( (componentRef) => {
expect(log[0]).toEqual(['getView', protoView]);
expect(log[1]).toEqual(['hydrateDynamicComponentView', hostView, 23, childView]);
async.done(); async.done();
}); });
});
})); }));
it('should inject dependencies of the dynamically-loaded component', inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({
template: '<dynamic-comp #dynamic></dynamic-comp>',
directives: [DynamicComp]
}));
tb.createView(MyComp).then((view) => {
var dynamicComponent = view.rawView.locals.get("dynamic");
dynamicComponent.done.then((ref) => {
expect(ref.instance.dynamicallyCreatedComponentService).toBeAnInstanceOf(DynamicallyCreatedComponentService);
async.done();
});
});
}));
it('should allow to destroy and create them via viewport directives',
inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({
template: '<div><dynamic-comp #dynamic template="if: ctxBoolProp"></dynamic-comp></div>',
directives: [DynamicComp, If]
}));
tb.createView(MyComp).then((view) => {
view.context.ctxBoolProp = true;
view.detectChanges();
var dynamicComponent = view.rawView.viewContainers[0].get(0).locals.get("dynamic");
dynamicComponent.done.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('hello');
view.context.ctxBoolProp = false;
view.detectChanges();
expect(view.rawView.viewContainers[0].length).toBe(0);
expect(view.rootNodes).toHaveText('');
view.context.ctxBoolProp = true;
view.detectChanges();
var dynamicComponent = view.rawView.viewContainers[0].get(0).locals.get("dynamic");
return dynamicComponent.done;
}).then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('hello');
async.done();
});
});
}));
}); });
describe("loading next to an existing location", () => {
it('should work', inject([DynamicComponentLoader, TestBed, AsyncTestCompleter],
(loader, tb, async) => {
tb.overrideView(MyComp, new View({
template: '<div><location #loc></location></div>',
directives: [Location]
}));
tb.createView(MyComp).then((view) => {
var location = view.rawView.locals.get("loc");
loader.loadNextToExistingLocation(DynamicallyLoaded, location.elementRef).then(ref => {
expect(view.rootNodes).toHaveText("Location;DynamicallyLoaded;")
async.done();
});
});
}));
it('should return a disposable component ref', inject([DynamicComponentLoader, TestBed, AsyncTestCompleter],
(loader, tb, async) => {
tb.overrideView(MyComp, new View({
template: '<div><location #loc></location></div>',
directives: [Location]
}));
tb.createView(MyComp).then((view) => {
var location = view.rawView.locals.get("loc");
loader.loadNextToExistingLocation(DynamicallyLoaded, location.elementRef).then(ref => {
loader.loadNextToExistingLocation(DynamicallyLoaded2, location.elementRef).then(ref2 => {
expect(view.rootNodes).toHaveText("Location;DynamicallyLoaded;DynamicallyLoaded2;")
ref2.dispose();
expect(view.rootNodes).toHaveText("Location;DynamicallyLoaded;")
async.done();
});
});
});
}));
});
}); });
} }
@Decorator({selector: 'someDecorator'})
class SomeDecorator {}
@Viewport({selector: 'someViewport'}) class DynamicallyCreatedComponentService {
class SomeViewport {} }
@Component({selector: 'someComponent'}) @DynamicComponent({
class SomeComponent {} selector: 'dynamic-comp'
})
class DynamicComp {
done;
constructor(loader:DynamicComponentLoader, location:ElementRef) {
this.done = loader.loadIntoExistingLocation(DynamicallyCreatedCmp, location);
}
}
@proxy @Component({
@IMPLEMENTS(Compiler) selector: 'hello-cmp',
class SpyCompiler extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} injectables: [DynamicallyCreatedComponentService]
})
@View({
template: "{{greeting}}"
})
class DynamicallyCreatedCmp {
greeting:string;
dynamicallyCreatedComponentService:DynamicallyCreatedComponentService;
@proxy constructor(a:DynamicallyCreatedComponentService) {
@IMPLEMENTS(ViewFactory) this.greeting = "hello";
class SpyViewFactory extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} this.dynamicallyCreatedComponentService = a;
}
}
@proxy @Component({selector: 'dummy'})
@IMPLEMENTS(AppViewHydrator) @View({template: "DynamicallyLoaded;"})
class SpyAppViewHydrator extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} class DynamicallyLoaded {
}
@proxy @Component({selector: 'dummy'})
@IMPLEMENTS(AppView) @View({template: "DynamicallyLoaded2;"})
class SpyAppView extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} class DynamicallyLoaded2 {
}
@Component({
selector: 'location'
})
@View({template: "Location;"})
class Location {
elementRef:ElementRef;
constructor(elementRef:ElementRef) {
this.elementRef = elementRef;
}
}
@Component()
@View({
directives: []
})
class MyComp {
ctxBoolProp:boolean;
constructor() {
this.ctxBoolProp = false;
}
}

View File

@ -28,8 +28,6 @@ import {Decorator, Component, Viewport, DynamicComponent} from 'angular2/src/cor
import {View} from 'angular2/src/core/annotations/view'; import {View} from 'angular2/src/core/annotations/view';
import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility'; import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility';
import {Attribute} from 'angular2/src/core/annotations/di'; import {Attribute} from 'angular2/src/core/annotations/di';
import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader';
import {ElementRef} from 'angular2/src/core/compiler/element_injector';
import {If} from 'angular2/src/directives/if'; import {If} from 'angular2/src/directives/if';
@ -643,75 +641,6 @@ export function main() {
})); }));
} }
describe("dynamic components", () => {
it('should support loading components dynamically', inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({
template: '<dynamic-comp #dynamic></dynamic-comp>',
directives: [DynamicComp]
}));
tb.createView(MyComp).then((view) => {
var dynamicComponent = view.rawView.locals.get("dynamic");
expect(dynamicComponent).toBeAnInstanceOf(DynamicComp);
dynamicComponent.done.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('hello');
async.done();
});
});
}));
it('should inject dependencies of the dynamically-loaded component', inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({
template: '<dynamic-comp #dynamic></dynamic-comp>',
directives: [DynamicComp]
}));
tb.createView(MyComp).then((view) => {
var dynamicComponent = view.rawView.locals.get("dynamic");
dynamicComponent.done.then((ref) => {
expect(ref.instance.dynamicallyCreatedComponentService).toBeAnInstanceOf(DynamicallyCreatedComponentService);
async.done();
});
});
}));
it('should allow to destroy and create them via viewport directives',
inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({
template: '<div><dynamic-comp #dynamic template="if: ctxBoolProp"></dynamic-comp></div>',
directives: [DynamicComp, If]
}));
tb.createView(MyComp).then((view) => {
view.context.ctxBoolProp = true;
view.detectChanges();
var dynamicComponent = view.rawView.viewContainers[0].get(0).locals.get("dynamic");
dynamicComponent.done.then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('hello');
view.context.ctxBoolProp = false;
view.detectChanges();
expect(view.rawView.viewContainers[0].length).toBe(0);
expect(view.rootNodes).toHaveText('');
view.context.ctxBoolProp = true;
view.detectChanges();
var dynamicComponent = view.rawView.viewContainers[0].get(0).locals.get("dynamic");
return dynamicComponent.done;
}).then((_) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('hello');
async.done();
});
});
}));
});
describe('dynamic ViewContainers', () => { describe('dynamic ViewContainers', () => {
it('should allow to create a ViewContainer at any bound location', it('should allow to create a ViewContainer at any bound location',
@ -868,34 +797,6 @@ class DynamicViewport {
} }
} }
class DynamicallyCreatedComponentService {}
@DynamicComponent({
selector: 'dynamic-comp'
})
class DynamicComp {
done;
constructor(loader:DynamicComponentLoader, location:ElementRef) {
this.done = loader.loadIntoExistingLocation(DynamicallyCreatedCmp, location);
}
}
@Component({
selector: 'hello-cmp',
injectables: [DynamicallyCreatedComponentService]
})
@View({
template: "{{greeting}}"
})
class DynamicallyCreatedCmp {
greeting:string;
dynamicallyCreatedComponentService:DynamicallyCreatedComponentService;
constructor(a:DynamicallyCreatedComponentService) {
this.greeting = "hello";
this.dynamicallyCreatedComponentService = a;
}
}
@Decorator({ @Decorator({
selector: '[my-dir]', selector: '[my-dir]',
properties: {'dirProp':'elprop'} properties: {'dirProp':'elprop'}