diff --git a/modules/angular2/src/core/application.js b/modules/angular2/src/core/application.js index 993d758e32..ab25fee1e6 100644 --- a/modules/angular2/src/core/application.js +++ b/modules/angular2/src/core/application.js @@ -75,7 +75,7 @@ function _injectorBindings(appComponentType): List { // We need to do this here to ensure that we create Testability and // it's ready on the window for users. registry.registerApplication(appElement, testability); - return dynamicComponentLoader.loadIntoNewLocation(appElement, appComponentAnnotatedType.type, null, injector); + return dynamicComponentLoader.loadIntoNewLocation(appElement, appComponentAnnotatedType.type, injector); }, [DynamicComponentLoader, Injector, appElementToken, appComponentAnnotatedTypeToken, Testability, TestabilityRegistry]), diff --git a/modules/angular2/src/core/compiler/dynamic_component_loader.js b/modules/angular2/src/core/compiler/dynamic_component_loader.js index ec9036f190..67b1827a29 100644 --- a/modules/angular2/src/core/compiler/dynamic_component_loader.js +++ b/modules/angular2/src/core/compiler/dynamic_component_loader.js @@ -16,11 +16,13 @@ export class ComponentRef { location:ElementRef; instance:any; componentView:AppView; + _dispose:Function; - constructor(location:ElementRef, instance:any, componentView:AppView){ + constructor(location:ElementRef, instance:any, componentView:AppView, dispose:Function){ this.location = location; this.instance = instance; this.componentView = componentView; + this._dispose = dispose; } get injector() { @@ -30,6 +32,10 @@ export class ComponentRef { get hostView() { return this.location.hostView; } + + dispose() { + this._dispose(); + } } /** @@ -72,16 +78,16 @@ export class DynamicComponentLoader { // from the component child views // See ViewFactory.returnView // 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 - * component receives injection normally as a hosted view. + * Loads a component in the element specified by elementOrSelector. The loaded component receives + * injection normally as a hosted view. */ - loadIntoNewLocation(elementOrSelector:any, type:Type, location:ElementRef, - injector:Injector = null):Promise { + loadIntoNewLocation(elementOrSelector:any, type:Type, injector:Injector = null):Promise { this._assertTypeIsComponent(type); 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 // See ViewFactory.returnView // See AppViewHydrator.dehydrateInPlaceHostView - var newLocation = new ElementRef(hostView.elementInjectors[0]); + var newLocation = hostView.elementInjectors[0].getElementRef(); 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 { + 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); }); } diff --git a/modules/angular2/src/core/compiler/element_injector.js b/modules/angular2/src/core/compiler/element_injector.js index 060398bd47..94054a914d 100644 --- a/modules/angular2/src/core/compiler/element_injector.js +++ b/modules/angular2/src/core/compiler/element_injector.js @@ -672,6 +672,10 @@ export class ElementInjector extends TreeNode { } } + getElementRef() { + return new ElementRef(this); + } + getDynamicallyLoadedComponent() { return this._dynamicallyCreatedComponent; } @@ -741,7 +745,7 @@ export class ElementInjector extends TreeNode { if (isPresent(dep.attributeName)) return this._buildAttribute(dep); if (isPresent(dep.queryDirective)) return this._findQuery(dep.queryDirective).list; if (dep.key.id === StaticKeys.instance().elementRefId) { - return new ElementRef(this); + return this.getElementRef(); } return this._getByKey(dep.key, dep.depth, dep.optional, requestor); } diff --git a/modules/angular2/src/core/compiler/view_container.js b/modules/angular2/src/core/compiler/view_container.js index a7af234bb0..27de3230b0 100644 --- a/modules/angular2/src/core/compiler/view_container.js +++ b/modules/angular2/src/core/compiler/view_container.js @@ -92,6 +92,10 @@ export class ViewContainer { return view; } + indexOf(view:viewModule.AppView) { + return ListWrapper.indexOf(this._views, view); + } + remove(atIndex=-1) { if (atIndex == -1) atIndex = this._views.length - 1; var view = this._views[atIndex]; diff --git a/modules/angular2/src/facade/collection.es6 b/modules/angular2/src/facade/collection.es6 index eed0ba58d3..d3df77b452 100644 --- a/modules/angular2/src/facade/collection.es6 +++ b/modules/angular2/src/facade/collection.es6 @@ -129,6 +129,9 @@ export class ListWrapper { static filter(array, pred:Function) { return array.filter(pred); } + static indexOf(array, value, startIndex = -1) { + return array.indexOf(value, startIndex); + } static any(list:List, pred:Function) { for (var i = 0 ; i < list.length; ++i) { if (pred(list[i])) return true; diff --git a/modules/angular2/src/facade/collection.ts b/modules/angular2/src/facade/collection.ts index 6b0f8581cc..523ce06947 100644 --- a/modules/angular2/src/facade/collection.ts +++ b/modules/angular2/src/facade/collection.ts @@ -114,6 +114,9 @@ export class ListWrapper { } return null; } + static indexOf(array: List, value, startIndex = -1) { + return array.indexOf(value, startIndex); + } static reduce(list: List, fn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, init: T) { diff --git a/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js b/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js index 887b3d76df..d6a6056473 100644 --- a/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js +++ b/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js @@ -11,118 +11,194 @@ import { inject, beforeEachBindings, it, - xit, - SpyObject, proxy -} from 'angular2/test_lib'; -import {IMPLEMENTS} from 'angular2/src/facade/lang'; -import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection'; -import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; -import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader'; + xit + } from 'angular2/test_lib'; + +import {TestBed} from 'angular2/src/test_lib/test_bed'; + +import {Decorator, Component, Viewport, DynamicComponent} from 'angular2/src/core/annotations/annotations'; +import {View} from 'angular2/src/core/annotations/view'; import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader'; -import {Decorator, Viewport, Component} from 'angular2/src/core/annotations/annotations'; -import {ElementRef, ElementInjector, ProtoElementInjector, PreBuiltObjects} from 'angular2/src/core/compiler/element_injector'; -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'; +import {ElementRef} from 'angular2/src/core/compiler/element_injector'; +import {If} from 'angular2/src/directives/if'; export function main() { - describe("DynamicComponentLoader", () => { - var compiler; - var viewFactory; - var directiveMetadataReader; - var viewHydrator; - var loader; + describe('DynamicComponentLoader', function () { + describe("loading into existing location", () => { + it('should work', inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView(MyComp, new View({ + template: '', + directives: [DynamicComp] + })); - beforeEach( () => { - compiler = new SpyCompiler(); - viewFactory = new SpyViewFactory(); - viewHydrator = new SpyAppViewHydrator(); - directiveMetadataReader = new DirectiveMetadataReader(); - loader = new DynamicComponentLoader(compiler, directiveMetadataReader, viewFactory, viewHydrator); - }); + tb.createView(MyComp).then((view) => { + var dynamicComponent = view.rawView.locals.get("dynamic"); + expect(dynamicComponent).toBeAnInstanceOf(DynamicComp); - function createProtoView() { - return new AppProtoView(null, null); - } - - 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(); + 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: '', + 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: '
', + 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: '
', + 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: '
', + 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 SomeViewport {} +class DynamicallyCreatedComponentService { +} -@Component({selector: 'someComponent'}) -class SomeComponent {} +@DynamicComponent({ + selector: 'dynamic-comp' +}) +class DynamicComp { + done; + constructor(loader:DynamicComponentLoader, location:ElementRef) { + this.done = loader.loadIntoExistingLocation(DynamicallyCreatedCmp, location); + } +} -@proxy -@IMPLEMENTS(Compiler) -class SpyCompiler extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} +@Component({ + selector: 'hello-cmp', + injectables: [DynamicallyCreatedComponentService] +}) +@View({ + template: "{{greeting}}" +}) +class DynamicallyCreatedCmp { + greeting:string; + dynamicallyCreatedComponentService:DynamicallyCreatedComponentService; -@proxy -@IMPLEMENTS(ViewFactory) -class SpyViewFactory extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} + constructor(a:DynamicallyCreatedComponentService) { + this.greeting = "hello"; + this.dynamicallyCreatedComponentService = a; + } +} -@proxy -@IMPLEMENTS(AppViewHydrator) -class SpyAppViewHydrator extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} +@Component({selector: 'dummy'}) +@View({template: "DynamicallyLoaded;"}) +class DynamicallyLoaded { +} -@proxy -@IMPLEMENTS(AppView) -class SpyAppView extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} +@Component({selector: 'dummy'}) +@View({template: "DynamicallyLoaded2;"}) +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; + } +} \ No newline at end of file diff --git a/modules/angular2/test/core/compiler/integration_spec.js b/modules/angular2/test/core/compiler/integration_spec.js index ed57d5fd16..974438c282 100644 --- a/modules/angular2/test/core/compiler/integration_spec.js +++ b/modules/angular2/test/core/compiler/integration_spec.js @@ -28,8 +28,6 @@ import {Decorator, Component, Viewport, DynamicComponent} from 'angular2/src/cor import {View} from 'angular2/src/core/annotations/view'; import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility'; 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'; @@ -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: '', - 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: '', - 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: '
', - 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', () => { 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({ selector: '[my-dir]', properties: {'dirProp':'elprop'}