diff --git a/modules/angular2/src/core/application.js b/modules/angular2/src/core/application.js index 40e1bb7a95..e284ea43e2 100644 --- a/modules/angular2/src/core/application.js +++ b/modules/angular2/src/core/application.js @@ -54,13 +54,10 @@ function _injectorBindings(appComponentType): List { return [ bind(DOCUMENT_TOKEN).toValue(DOM.defaultDoc()), bind(appComponentRefToken).toAsyncFactory((dynamicComponentLoader, injector, - metadataReader, testability, registry) => { + testability, registry) => { - var annotation = metadataReader.resolve(appComponentType); - - var selector = annotation.selector; // TODO(rado): investigate whether to support bindings on root component. - return dynamicComponentLoader.loadIntoNewLocation(appComponentType, null, selector, injector).then( (componentRef) => { + return dynamicComponentLoader.loadAsRoot(appComponentType, null, injector).then( (componentRef) => { var domView = resolveInternalDomView(componentRef.hostView.render); // We need to do this here to ensure that we create Testability and // it's ready on the window for users. @@ -68,7 +65,7 @@ function _injectorBindings(appComponentType): List { return componentRef; }); - }, [DynamicComponentLoader, Injector, DirectiveResolver, + }, [DynamicComponentLoader, Injector, Testability, TestabilityRegistry]), bind(appComponentType).toFactory((ref) => ref.instance, diff --git a/modules/angular2/src/core/compiler/dynamic_component_loader.js b/modules/angular2/src/core/compiler/dynamic_component_loader.js index ea1a2dc6c2..56c32360bd 100644 --- a/modules/angular2/src/core/compiler/dynamic_component_loader.js +++ b/modules/angular2/src/core/compiler/dynamic_component_loader.js @@ -62,19 +62,38 @@ export class DynamicComponentLoader { } /** - * Loads a component in the element specified by elementSelector. The loaded component receives - * injection normally as a hosted view. + * Loads a root component that is placed at the first element that matches the + * component's selector. + * The loaded component receives injection normally as a hosted view. */ - loadIntoNewLocation(typeOrBinding, parentComponentLocation:ElementRef, elementSelector:string, - injector:Injector = null):Promise { + loadAsRoot(typeOrBinding, overrideSelector = null, injector:Injector = null):Promise { return this._compiler.compileInHost(this._getBinding(typeOrBinding)).then(hostProtoViewRef => { - var hostViewRef = this._viewManager.createInPlaceHostView( - parentComponentLocation, elementSelector, hostProtoViewRef, injector); + var hostViewRef = this._viewManager.createRootHostView(hostProtoViewRef, overrideSelector, injector); var newLocation = new ElementRef(hostViewRef, 0); var component = this._viewManager.getComponent(newLocation); var dispose = () => { - this._viewManager.destroyInPlaceHostView(parentComponentLocation, hostViewRef); + this._viewManager.destroyRootHostView(hostViewRef); + }; + return new ComponentRef(newLocation, component, dispose); + }); + } + + /** + * Loads a component into a free host view that is not yet attached to + * a parent on the render side, although it is attached to a parent in the injector hierarchy. + * The loaded component receives injection normally as a hosted view. + */ + loadIntoNewLocation(typeOrBinding, parentComponentLocation:ElementRef, + injector:Injector = null):Promise { + return this._compiler.compileInHost(this._getBinding(typeOrBinding)).then(hostProtoViewRef => { + var hostViewRef = this._viewManager.createFreeHostView( + parentComponentLocation, hostProtoViewRef, injector); + var newLocation = new ElementRef(hostViewRef, 0); + var component = this._viewManager.getComponent(newLocation); + + var dispose = () => { + this._viewManager.destroyFreeHostView(parentComponentLocation, hostViewRef); }; return new ComponentRef(newLocation, component, dispose); }); diff --git a/modules/angular2/src/core/compiler/view.js b/modules/angular2/src/core/compiler/view.js index d239368ce4..8a26685adb 100644 --- a/modules/angular2/src/core/compiler/view.js +++ b/modules/angular2/src/core/compiler/view.js @@ -32,7 +32,7 @@ export class AppView { componentChildViews: List; /// Host views that were added by an imperative view. /// This is a dynamically growing / shrinking array. - inPlaceHostViews: List; + freeHostViews: List; viewContainers: List; preBuiltObjects: List; proto: AppProtoView; @@ -64,7 +64,7 @@ export class AppView { this.context = null; this.locals = new Locals(null, MapWrapper.clone(protoLocals)); //TODO optimize this this.renderer = renderer; - this.inPlaceHostViews = []; + this.freeHostViews = []; } init(changeDetector:ChangeDetector, elementInjectors:List, rootElementInjectors:List, diff --git a/modules/angular2/src/core/compiler/view_manager.js b/modules/angular2/src/core/compiler/view_manager.js index 43406abb7b..e04b08b499 100644 --- a/modules/angular2/src/core/compiler/view_manager.js +++ b/modules/angular2/src/core/compiler/view_manager.js @@ -62,33 +62,46 @@ export class AppViewManager { return new ViewRef(componentView); } - createInPlaceHostView(parentComponentLocation:ElementRef, - hostElementSelector:string, hostProtoViewRef:ProtoViewRef, injector:Injector):ViewRef { + createRootHostView(hostProtoViewRef:ProtoViewRef, overrideSelector:string, injector:Injector):ViewRef { var hostProtoView = internalProtoView(hostProtoViewRef); - var parentComponentHostView = null; - var parentComponentBoundElementIndex = null; - var parentRenderViewRef = null; - if (isPresent(parentComponentLocation)) { - parentComponentHostView = internalView(parentComponentLocation.parentView); - parentComponentBoundElementIndex = parentComponentLocation.boundElementIndex; - parentRenderViewRef = parentComponentHostView.componentChildViews[parentComponentBoundElementIndex].render; + var hostElementSelector = overrideSelector; + if (isBlank(hostElementSelector)) { + hostElementSelector = hostProtoView.elementBinders[0].componentDirective.metadata.selector; } - var hostRenderView = this._renderer.createInPlaceHostView(parentRenderViewRef, hostElementSelector, hostProtoView.render); - var hostView = this._utils.createView(hostProtoView, hostRenderView, this, this._renderer); + var renderView = this._renderer.createRootHostView(hostProtoView.render, hostElementSelector); + var hostView = this._utils.createView(hostProtoView, renderView, this, this._renderer); this._renderer.setEventDispatcher(hostView.render, hostView); - this._createViewRecurse(hostView) - this._utils.attachAndHydrateInPlaceHostView(parentComponentHostView, parentComponentBoundElementIndex, hostView, injector); + this._createViewRecurse(hostView); + + this._utils.hydrateRootHostView(hostView, injector); this._viewHydrateRecurse(hostView); return new ViewRef(hostView); } - destroyInPlaceHostView(parentComponentLocation:ElementRef, hostViewRef:ViewRef) { + destroyRootHostView(hostViewRef:ViewRef) { + // Note: Don't detach the hostView as we want to leave the + // root element in place. Also don't put the hostView into the view pool + // as it is depending on the element for which it was created. var hostView = internalView(hostViewRef); - var parentView = null; - if (isPresent(parentComponentLocation)) { - parentView = internalView(parentComponentLocation.parentView).componentChildViews[parentComponentLocation.boundElementIndex]; - } - this._destroyInPlaceHostView(parentView, hostView); + // We do want to destroy the component view though. + this._viewDehydrateRecurse(hostView, true); + this._renderer.destroyView(hostView.render); + } + + createFreeHostView(parentComponentLocation:ElementRef, hostProtoViewRef:ProtoViewRef, injector:Injector):ViewRef { + var hostProtoView = internalProtoView(hostProtoViewRef); + var hostView = this._createPooledView(hostProtoView); + var parentComponentHostView = internalView(parentComponentLocation.parentView); + var parentComponentBoundElementIndex = parentComponentLocation.boundElementIndex; + this._utils.attachAndHydrateFreeHostView(parentComponentHostView, parentComponentBoundElementIndex, hostView, injector); + this._viewHydrateRecurse(hostView); + return new ViewRef(hostView); + } + + destroyFreeHostView(parentComponentLocation:ElementRef, hostViewRef:ViewRef) { + var hostView = internalView(hostViewRef); + var parentView = internalView(parentComponentLocation.parentView).componentChildViews[parentComponentLocation.boundElementIndex]; + this._destroyFreeHostView(parentView, hostView); } createViewInContainer(viewContainerLocation:ElementRef, @@ -186,16 +199,11 @@ export class AppViewManager { this._destroyPooledView(componentView); } - _destroyInPlaceHostView(parentView, hostView) { - var parentRenderViewRef = null; - if (isPresent(parentView)) { - parentRenderViewRef = parentView.render; - } + _destroyFreeHostView(parentView, hostView) { this._viewDehydrateRecurse(hostView, true); - this._utils.detachInPlaceHostView(parentView, hostView); - this._renderer.destroyInPlaceHostView(parentRenderViewRef, hostView.render); - // Note: Don't put the inplace host view into the view pool - // as it is depending on the element for which it was created. + this._renderer.detachFreeHostView(parentView.render, hostView.render); + this._utils.detachFreeHostView(parentView, hostView); + this._destroyPooledView(hostView); } _viewHydrateRecurse( @@ -234,10 +242,10 @@ export class AppViewManager { } } - // inPlaceHostViews - for (var i = view.inPlaceHostViews.length-1; i>=0; i--) { - var hostView = view.inPlaceHostViews[i]; - this._destroyInPlaceHostView(view, hostView); + // freeHostViews + for (var i = view.freeHostViews.length-1; i>=0; i--) { + var hostView = view.freeHostViews[i]; + this._destroyFreeHostView(view, hostView); } } } diff --git a/modules/angular2/src/core/compiler/view_manager_utils.js b/modules/angular2/src/core/compiler/view_manager_utils.js index 4c7da85af6..263d11e9eb 100644 --- a/modules/angular2/src/core/compiler/view_manager_utils.js +++ b/modules/angular2/src/core/compiler/view_manager_utils.js @@ -93,24 +93,23 @@ export class AppViewManagerUtils { ); } - attachAndHydrateInPlaceHostView(parentComponentHostView:viewModule.AppView, parentComponentBoundElementIndex:number, + hydrateRootHostView(hostView:viewModule.AppView, injector:Injector = null) { + this._hydrateView(hostView, injector, null, new Object(), null); + } + + attachAndHydrateFreeHostView(parentComponentHostView:viewModule.AppView, parentComponentBoundElementIndex:number, hostView:viewModule.AppView, injector:Injector = null) { - var hostElementInjector = null; - if (isPresent(parentComponentHostView)) { - hostElementInjector = parentComponentHostView.elementInjectors[parentComponentBoundElementIndex]; - var parentView = parentComponentHostView.componentChildViews[parentComponentBoundElementIndex]; - parentView.changeDetector.addChild(hostView.changeDetector); - ListWrapper.push(parentView.inPlaceHostViews, hostView); - } + var hostElementInjector = parentComponentHostView.elementInjectors[parentComponentBoundElementIndex]; + var parentView = parentComponentHostView.componentChildViews[parentComponentBoundElementIndex]; + parentView.changeDetector.addChild(hostView.changeDetector); + ListWrapper.push(parentView.freeHostViews, hostView); this._hydrateView(hostView, injector, hostElementInjector, new Object(), null); } - detachInPlaceHostView(parentView:viewModule.AppView, + detachFreeHostView(parentView:viewModule.AppView, hostView:viewModule.AppView) { - if (isPresent(parentView)) { - parentView.changeDetector.removeChild(hostView.changeDetector); - ListWrapper.remove(parentView.inPlaceHostViews, hostView); - } + parentView.changeDetector.removeChild(hostView.changeDetector); + ListWrapper.remove(parentView.freeHostViews, hostView); } attachViewInContainer(parentView:viewModule.AppView, boundElementIndex:number, diff --git a/modules/angular2/src/render/api.js b/modules/angular2/src/render/api.js index fab06ba55b..b731ebdf30 100644 --- a/modules/angular2/src/render/api.js +++ b/modules/angular2/src/render/api.js @@ -187,20 +187,19 @@ export class RenderCompiler { export class Renderer { /** - * Creates a host view that includes the given element. - * @param {RenderViewRef} parentHostViewRef (might be null) - * @param {any} hostElementSelector css selector for the host element + * Creates a root host view that includes the given element. * @param {RenderProtoViewRef} hostProtoViewRef a RenderProtoViewRef of type ProtoViewDto.HOST_VIEW_TYPE + * @param {any} hostElementSelector css selector for the host element (will be queried against the main document) * @return {RenderViewRef} the created view */ - createInPlaceHostView(parentHostViewRef:RenderViewRef, hostElementSelector:string, hostProtoViewRef:RenderProtoViewRef):RenderViewRef { + createRootHostView(hostProtoViewRef:RenderProtoViewRef, hostElementSelector:string):RenderViewRef { return null; } /** - * Destroys the given host view in the given parent view. + * Detaches a free host view's element from the DOM. */ - destroyInPlaceHostView(parentHostViewRef:RenderViewRef, hostViewRef:RenderViewRef) { + detachFreeHostView(parentHostViewRef:RenderViewRef, hostViewRef:RenderViewRef) { } /** diff --git a/modules/angular2/src/render/dom/dom_renderer.js b/modules/angular2/src/render/dom/dom_renderer.js index 983d0b3e14..1c74274aeb 100644 --- a/modules/angular2/src/render/dom/dom_renderer.js +++ b/modules/angular2/src/render/dom/dom_renderer.js @@ -19,8 +19,6 @@ import {Renderer, RenderProtoViewRef, RenderViewRef} from '../api'; // const expressions! export const DOCUMENT_TOKEN = 'DocumentToken'; -var _DOCUMENT_SELECTOR_REGEX = RegExpWrapper.create('\\:document(.+)'); - @Injectable() export class DomRenderer extends Renderer { _eventManager:EventManager; @@ -34,27 +32,16 @@ export class DomRenderer extends Renderer { this._document = document; } - createInPlaceHostView(parentHostViewRef:RenderViewRef, hostElementSelector:string, hostProtoViewRef:RenderProtoViewRef):RenderViewRef { - var containerNode; - var documentSelectorMatch = RegExpWrapper.firstMatch(_DOCUMENT_SELECTOR_REGEX, hostElementSelector); - if (isPresent(documentSelectorMatch)) { - containerNode = this._document; - hostElementSelector = documentSelectorMatch[1]; - } else if (isPresent(parentHostViewRef)) { - var parentHostView = resolveInternalDomView(parentHostViewRef); - containerNode = parentHostView.shadowRoot; - } else { - containerNode = this._document; - } - var element = DOM.querySelector(containerNode, hostElementSelector); + createRootHostView(hostProtoViewRef:RenderProtoViewRef, hostElementSelector:string):RenderViewRef { + var hostProtoView = resolveInternalDomProtoView(hostProtoViewRef); + var element = DOM.querySelector(this._document, hostElementSelector); if (isBlank(element)) { throw new BaseException(`The selector "${hostElementSelector}" did not match any elements`); } - var hostProtoView = resolveInternalDomProtoView(hostProtoViewRef); return new DomViewRef(this._createView(hostProtoView, element)); } - destroyInPlaceHostView(parentHostViewRef:RenderViewRef, hostViewRef:RenderViewRef) { + detachFreeHostView(parentHostViewRef:RenderViewRef, hostViewRef:RenderViewRef) { var hostView = resolveInternalDomView(hostViewRef); this._removeViewNodes(hostView); } @@ -89,6 +76,11 @@ export class DomRenderer extends Renderer { this._moveViewNodesIntoParent(componentView.shadowRoot, componentView); } + getHostElement(hostViewRef:RenderViewRef) { + var hostView = resolveInternalDomView(hostViewRef); + return hostView.boundElements[0]; + } + detachComponentView(hostViewRef:RenderViewRef, boundElementIndex:number, componentViewRef:RenderViewRef) { var hostView = resolveInternalDomView(hostViewRef); var componentView = resolveInternalDomView(componentViewRef); diff --git a/modules/angular2/src/test_lib/test_bed.js b/modules/angular2/src/test_lib/test_bed.js index 08ccb03930..db40461a55 100644 --- a/modules/angular2/src/test_lib/test_bed.js +++ b/modules/angular2/src/test_lib/test_bed.js @@ -94,7 +94,7 @@ export class TestBed { DOM.appendChild(doc.body, rootEl); var componentBinding = bind(component).toValue(context); - return this._injector.get(DynamicComponentLoader).loadIntoNewLocation(componentBinding, null, '#root', this._injector).then((hostComponentRef) => { + return this._injector.get(DynamicComponentLoader).loadAsRoot(componentBinding,'#root', this._injector).then((hostComponentRef) => { return new ViewProxy(hostComponentRef); }); } 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 df73ec3b17..0e484437a8 100644 --- a/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js +++ b/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js @@ -11,17 +11,18 @@ import { inject, beforeEachBindings, it, - xit + xit, + viewRootNodes } from 'angular2/test_lib'; -import {TestBed} from 'angular2/src/test_lib/test_bed'; - +import {TestBed, ViewProxy} from 'angular2/src/test_lib/test_bed'; +import {Injector} from 'angular2/di'; import {Component} from 'angular2/src/core/annotations_impl/annotations'; import {View} from 'angular2/src/core/annotations_impl/view'; import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader'; import {ElementRef} from 'angular2/src/core/compiler/element_ref'; import {NgIf} from 'angular2/src/directives/ng_if'; -import {DomRenderer} from 'angular2/src/render/dom/dom_renderer'; +import {DomRenderer, DOCUMENT_TOKEN} from 'angular2/src/render/dom/dom_renderer'; import {DOM} from 'angular2/src/dom/dom_adapter'; import {AppViewManager} from 'angular2/src/core/compiler/view_manager'; @@ -193,6 +194,37 @@ export function main() { }); + describe('loadAsRoot', () => { + + it('should allow to create, update and destroy components', + inject([TestBed, AsyncTestCompleter, DynamicComponentLoader, DOCUMENT_TOKEN, Injector], (tb, async, dcl, doc, injector) => { + var rootEl = el(''); + DOM.appendChild(doc.body, rootEl); + dcl.loadAsRoot(ChildComp, null, injector).then( (componentRef) => { + var view = new ViewProxy(componentRef); + expect(rootEl.parentNode).toBe(doc.body); + + view.detectChanges(); + + expect(rootEl).toHaveText('hello'); + + componentRef.instance.ctxProp = 'new'; + + view.detectChanges(); + + expect(rootEl).toHaveText('new'); + + componentRef.dispose(); + + expect(rootEl).toHaveText(''); + expect(rootEl.parentNode).toBe(doc.body); + + async.done(); + }); + })); + + }); + }); } @@ -200,7 +232,6 @@ export function main() { selector: 'imp-ng-cmp' }) @View({ - renderer: 'imp-ng-cmp-renderer', template: '' }) class ImperativeViewComponentUsingNgComponent { @@ -210,7 +241,11 @@ class ImperativeViewComponentUsingNgComponent { var div = el('
'); var shadowViewRef = viewManager.getComponentView(self); renderer.setComponentViewRootNodes(shadowViewRef.render, [div]); - this.done = dynamicComponentLoader.loadIntoNewLocation(ChildComp, self, '#impHost', null); + this.done = dynamicComponentLoader.loadIntoNewLocation(ChildComp, self, null).then( (componentRef) => { + var element = renderer.getHostElement(componentRef.hostView.render); + DOM.appendChild(div, element); + return componentRef; + }); } } diff --git a/modules/angular2/test/core/compiler/view_manager_spec.js b/modules/angular2/test/core/compiler/view_manager_spec.js index cfa832def0..e5dbb63bb4 100644 --- a/modules/angular2/test/core/compiler/view_manager_spec.js +++ b/modules/angular2/test/core/compiler/view_manager_spec.js @@ -141,7 +141,7 @@ export function main() { } ListWrapper.insert(viewContainer.views, atIndex, childView); }); - renderer.spy('createInPlaceHostView').andCallFake( (_a, _b, _c) => { + renderer.spy('createRootHostView').andCallFake( (_b, _c) => { var rv = new RenderViewRef(); ListWrapper.push(createdRenderViews, rv); return rv; @@ -294,7 +294,7 @@ export function main() { }); - describe('createInPlaceHostView', () => { + describe('createFreeHostView', () => { // Note: We don't add tests for recursion or viewpool here as we assume that // this is using the same mechanism as the other methods... @@ -314,27 +314,26 @@ export function main() { it('should create the view', () => { expect( - internalView(manager.createInPlaceHostView(elementRef(wrapView(parentHostView), 0), null, wrapPv(hostProtoView), null)) + internalView(manager.createFreeHostView(elementRef(wrapView(parentHostView), 0), wrapPv(hostProtoView), null)) ).toBe(createdViews[0]); expect(createdViews[0].proto).toBe(hostProtoView); }); it('should attachAndHydrate the view', () => { var injector = new Injector([], null, false); - manager.createInPlaceHostView(elementRef(wrapView(parentHostView), 0), null, wrapPv(hostProtoView), injector); - expect(utils.spy('attachAndHydrateInPlaceHostView')).toHaveBeenCalledWith(parentHostView, 0, createdViews[0], injector); + manager.createFreeHostView(elementRef(wrapView(parentHostView), 0), wrapPv(hostProtoView), injector); + expect(utils.spy('attachAndHydrateFreeHostView')).toHaveBeenCalledWith(parentHostView, 0, createdViews[0], injector); expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(createdViews[0].render); }); it('should create and set the render view', () => { - var elementOrSelector = 'someSelector'; - manager.createInPlaceHostView(elementRef(wrapView(parentHostView), 0), elementOrSelector, wrapPv(hostProtoView), null) - expect(renderer.spy('createInPlaceHostView')).toHaveBeenCalledWith(parentView.render, elementOrSelector, hostProtoView.render); + manager.createFreeHostView(elementRef(wrapView(parentHostView), 0), wrapPv(hostProtoView), null) + expect(renderer.spy('createView')).toHaveBeenCalledWith(hostProtoView.render); expect(createdViews[0].render).toBe(createdRenderViews[0]); }); it('should set the event dispatcher', () => { - manager.createInPlaceHostView(elementRef(wrapView(parentHostView), 0), null, wrapPv(hostProtoView), null); + manager.createFreeHostView(elementRef(wrapView(parentHostView), 0), wrapPv(hostProtoView), null); var cmpView = createdViews[0]; expect(renderer.spy('setEventDispatcher')).toHaveBeenCalledWith(cmpView.render, cmpView); }); @@ -343,7 +342,7 @@ export function main() { }); - describe('destroyInPlaceHostView', () => { + describe('destroyFreeHostView', () => { describe('basic functionality', () => { var parentHostView, parentView, hostProtoView, hostView, hostRenderViewRef; beforeEach( () => { @@ -355,29 +354,29 @@ export function main() { hostProtoView = createProtoView( [createComponentElBinder(null)] ); - hostView = internalView(manager.createInPlaceHostView(elementRef(wrapView(parentHostView), 0), null, wrapPv(hostProtoView), null)); + hostView = internalView(manager.createFreeHostView(elementRef(wrapView(parentHostView), 0), wrapPv(hostProtoView), null)); hostRenderViewRef = hostView.render; }); it('should detach', () => { - manager.destroyInPlaceHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); - expect(utils.spy('detachInPlaceHostView')).toHaveBeenCalledWith(parentView, hostView); + manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); + expect(utils.spy('detachFreeHostView')).toHaveBeenCalledWith(parentView, hostView); }); it('should dehydrate', () => { - manager.destroyInPlaceHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); + manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); expect(utils.spy('dehydrateView')).toHaveBeenCalledWith(hostView); expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(hostView.render); }); - it('should destroy and clear the render view', () => { - manager.destroyInPlaceHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); - expect(renderer.spy('destroyInPlaceHostView')).toHaveBeenCalledWith(parentView.render, hostRenderViewRef); + it('should detach the render view', () => { + manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); + expect(renderer.spy('detachFreeHostView')).toHaveBeenCalledWith(parentView.render, hostRenderViewRef); }); - it('should not return the view to the pool', () => { - manager.destroyInPlaceHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); - expect(viewPool.spy('returnView')).not.toHaveBeenCalled(); + it('should return the view to the pool', () => { + manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); + expect(viewPool.spy('returnView')).toHaveBeenCalledWith(hostView); }); }); @@ -387,6 +386,78 @@ export function main() { }); + describe('createRootHostView', () => { + + var hostProtoView; + beforeEach( () => { + hostProtoView = createProtoView( + [createComponentElBinder(null)] + ); + }); + + it('should create the view', () => { + expect( + internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null)) + ).toBe(createdViews[0]); + expect(createdViews[0].proto).toBe(hostProtoView); + }); + + it('should hydrate the view', () => { + var injector = new Injector([], null, false); + manager.createRootHostView(wrapPv(hostProtoView), null, injector); + expect(utils.spy('hydrateRootHostView')).toHaveBeenCalledWith(createdViews[0], injector); + expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(createdViews[0].render); + }); + + it('should create and set the render view using the component selector', () => { + manager.createRootHostView(wrapPv(hostProtoView), null, null) + expect(renderer.spy('createRootHostView')).toHaveBeenCalledWith(hostProtoView.render, 'someComponent'); + expect(createdViews[0].render).toBe(createdRenderViews[0]); + }); + + it('should allow to override the selector', () => { + var selector = 'someOtherSelector'; + manager.createRootHostView(wrapPv(hostProtoView), selector, null) + expect(renderer.spy('createRootHostView')).toHaveBeenCalledWith(hostProtoView.render, selector); + }); + + it('should set the event dispatcher', () => { + manager.createRootHostView(wrapPv(hostProtoView), null, null); + var cmpView = createdViews[0]; + expect(renderer.spy('setEventDispatcher')).toHaveBeenCalledWith(cmpView.render, cmpView); + }); + + }); + + + describe('destroyRootHostView', () => { + var hostProtoView, hostView, hostRenderViewRef; + beforeEach( () => { + hostProtoView = createProtoView( + [createComponentElBinder(null)] + ); + hostView = internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null)); + hostRenderViewRef = hostView.render; + }); + + it('should dehydrate', () => { + manager.destroyRootHostView(wrapView(hostView)); + expect(utils.spy('dehydrateView')).toHaveBeenCalledWith(hostView); + expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(hostView.render); + }); + + it('should destroy the render view', () => { + manager.destroyRootHostView(wrapView(hostView)); + expect(renderer.spy('destroyView')).toHaveBeenCalledWith(hostRenderViewRef); + }); + + it('should not return the view to the pool', () => { + manager.destroyRootHostView(wrapView(hostView)); + expect(viewPool.spy('returnView')).not.toHaveBeenCalled(); + }); + + }); + describe('createViewInContainer', () => { describe('basic functionality', () => { @@ -483,19 +554,19 @@ export function main() { }); it('should dehydrate', () => { - manager.destroyInPlaceHostView(null, wrapView(parentView)); + manager.destroyRootHostView(wrapView(parentView)); expect(utils.spy('dehydrateView')).toHaveBeenCalledWith(parentView.viewContainers[0].views[0]); expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(childView.render); }); it('should detach', () => { - manager.destroyInPlaceHostView(null, wrapView(parentView)); + manager.destroyRootHostView(wrapView(parentView)); expect(utils.spy('detachViewInContainer')).toHaveBeenCalledWith(parentView, 0, 0); expect(renderer.spy('detachViewInContainer')).toHaveBeenCalledWith(parentView.render, 0, 0, childView.render); }); it('should return the view to the pool', () => { - manager.destroyInPlaceHostView(null, wrapView(parentView)); + manager.destroyRootHostView(wrapView(parentView)); expect(viewPool.spy('returnView')).toHaveBeenCalledWith(childView); }); diff --git a/modules/angular2/test/core/compiler/view_manager_utils_spec.js b/modules/angular2/test/core/compiler/view_manager_utils_spec.js index 6953e70ad4..80f7924cc9 100644 --- a/modules/angular2/test/core/compiler/view_manager_utils_spec.js +++ b/modules/angular2/test/core/compiler/view_manager_utils_spec.js @@ -31,7 +31,6 @@ import {AppViewManagerUtils} from 'angular2/src/core/compiler/view_manager_utils export function main() { // TODO(tbosch): add more tests here! - describe('AppViewManagerUtils', () => { var directiveResolver; @@ -170,7 +169,7 @@ export function main() { var shadowView = createView(); utils.attachComponentView(hostView, 0, shadowView); - utils.attachAndHydrateInPlaceHostView(null, null, hostView, createInjector()); + utils.hydrateRootHostView(hostView, createInjector()); expect(spyEventAccessor1.spy('subscribe')).toHaveBeenCalledWith(hostView, 0, dir); expect(spyEventAccessor2.spy('subscribe')).toHaveBeenCalledWith(hostView, 1, dir); @@ -200,7 +199,7 @@ export function main() { var shadowView = createView(); utils.attachComponentView(hostView, 0, shadowView); - utils.attachAndHydrateInPlaceHostView(null, null, hostView, createInjector()); + utils.hydrateRootHostView(hostView, createInjector()); expect(spyActionAccessor1.spy('subscribe')).toHaveBeenCalledWith(hostView, 0, dir); expect(spyActionAccessor2.spy('subscribe')).toHaveBeenCalledWith(hostView, 1, dir); @@ -268,6 +267,27 @@ export function main() { }); + describe('hydrateRootHostView', () => { + var hostView; + + function createViews() { + var hostPv = createProtoView([ + createComponentElBinder() + ]); + hostView = createView(hostPv); + } + + it("should instantiate the elementInjectors with the given injector and an empty host element injector", () => { + var injector = createInjector(); + createViews(); + + utils.hydrateRootHostView(hostView, injector); + expect(hostView.rootElementInjectors[0].spy('instantiateDirectives')) + .toHaveBeenCalledWith(injector, null, hostView.preBuiltObjects[0]); + }); + + }); + }); } diff --git a/modules/angular2/test/render/dom/dom_renderer_integration_spec.js b/modules/angular2/test/render/dom/dom_renderer_integration_spec.js index 4db5f06116..90378d8bab 100644 --- a/modules/angular2/test/render/dom/dom_renderer_integration_spec.js +++ b/modules/angular2/test/render/dom/dom_renderer_integration_spec.js @@ -17,7 +17,7 @@ import { import {MapWrapper} from 'angular2/src/facade/collection'; import {DOM} from 'angular2/src/dom/dom_adapter'; -import {DomTestbed} from './dom_testbed'; +import {DomTestbed, TestView} from './dom_testbed'; import {ViewDefinition, DirectiveMetadata, RenderViewRef} from 'angular2/src/render/api'; @@ -27,15 +27,29 @@ export function main() { DomTestbed ]); - it('should create and destroy host views while using the given elements in place', + it('should create and destroy root host views while using the given elements in place', inject([AsyncTestCompleter, DomTestbed], (async, tb) => { - tb.compileAll([someComponent]).then( (protoViewDtos) => { - var view = tb.createRootView(protoViewDtos[0]); - expect(tb.rootEl.parentNode).toBeTruthy(); + tb.compiler.compileHost(someComponent).then( (hostProtoViewDto) => { + var view = new TestView(tb.renderer.createRootHostView(hostProtoViewDto.render, '#root')); expect(view.rawView.rootNodes[0]).toEqual(tb.rootEl); - tb.renderer.destroyInPlaceHostView(null, view.viewRef); - expect(tb.rootEl.parentNode).toBeFalsy(); + tb.renderer.destroyView(view.viewRef); + // destroying a root view should not disconnect it! + expect(tb.rootEl.parentNode).toBeTruthy(); + + async.done(); + }); + })); + + it('should create and destroy free host views', + inject([AsyncTestCompleter, DomTestbed], (async, tb) => { + tb.compiler.compileHost(someComponent).then( (hostProtoViewDto) => { + var view = new TestView(tb.renderer.createView(hostProtoViewDto.render)); + var hostElement = tb.renderer.getHostElement(view.viewRef); + DOM.appendChild(tb.rootEl, hostElement); + + tb.renderer.detachFreeHostView(null, view.viewRef); + expect(DOM.parentElement(hostElement)).toBeFalsy(); async.done(); }); diff --git a/modules/angular2/test/render/dom/dom_testbed.js b/modules/angular2/test/render/dom/dom_testbed.js index e687931908..3c8b727ca7 100644 --- a/modules/angular2/test/render/dom/dom_testbed.js +++ b/modules/angular2/test/render/dom/dom_testbed.js @@ -77,7 +77,7 @@ export class DomTestbed { } createRootView(rootProtoView:ProtoViewDto):TestView { - var viewRef = this.renderer.createInPlaceHostView(null, '#root', rootProtoView.render); + var viewRef = this.renderer.createRootHostView(rootProtoView.render, '#root'); this.renderer.hydrateView(viewRef); return this._createTestView(viewRef); } diff --git a/modules/angular2_material/src/components/dialog/dialog.js b/modules/angular2_material/src/components/dialog/dialog.js index 560ab9f991..3e23f16227 100644 --- a/modules/angular2_material/src/components/dialog/dialog.js +++ b/modules/angular2_material/src/components/dialog/dialog.js @@ -1,4 +1,4 @@ -import {DynamicComponentLoader, ElementRef, ComponentRef, onDestroy} from 'angular2/angular2'; +import {DynamicComponentLoader, ElementRef, ComponentRef, onDestroy, DomRenderer} from 'angular2/angular2'; import {bind, Injector} from 'angular2/di'; import {ObservableWrapper, Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {isPresent, Type} from 'angular2/src/facade/lang'; @@ -12,7 +12,6 @@ import {Component, Directive} from 'angular2/src/core/annotations_impl/annotatio import {Parent} from 'angular2/src/core/annotations_impl/visibility'; import {View} from 'angular2/src/core/annotations_impl/view'; - // TODO(jelbourn): Opener of dialog can control where it is rendered. // TODO(jelbourn): body scrolling is disabled while dialog is open. // TODO(jelbourn): Don't manually construct and configure a DOM element. See #1402 @@ -29,9 +28,11 @@ var _nextDialogId = 0; */ export class MdDialog { componentLoader: DynamicComponentLoader; + domRenderer: DomRenderer; - constructor(loader: DynamicComponentLoader) { + constructor(loader: DynamicComponentLoader, domRenderer: DomRenderer) { this.componentLoader = loader; + this.domRenderer = domRenderer; } /** @@ -47,25 +48,6 @@ export class MdDialog { options: MdDialogConfig = null): Promise { var config = isPresent(options) ? options : new MdDialogConfig(); - // TODO(jelbourn): Don't use direct DOM access. Need abstraction to create an element - // directly on the document body (also needed for web workers stuff). - // Create a DOM node to serve as a physical host element for the dialog. - var dialogElement = this._createHostElement(); - DOM.appendChild(DOM.query('body'), dialogElement); - - // TODO(jelbourn): Use hostProperties binding to set these once #1539 is fixed. - // Configure properties on the host element. - DOM.addClass(dialogElement, 'md-dialog'); - DOM.setAttribute(dialogElement, 'tabindex', '0'); - - // TODO(jelbourn): Do this with hostProperties (or another rendering abstraction) once ready. - if (isPresent(config.width)) { - DOM.setStyle(dialogElement, 'width', config.width); - } - if (isPresent(config.height)) { - DOM.setStyle(dialogElement, 'height', config.height); - } - // Create the dialogRef here so that it can be injected into the content component. var dialogRef = new MdDialogRef(); @@ -76,7 +58,27 @@ export class MdDialog { // First, load the MdDialogContainer, into which the given component will be loaded. return this.componentLoader.loadIntoNewLocation( - MdDialogContainer, elementRef, `:document#${dialogElement.id}`).then(containerRef => { + MdDialogContainer, elementRef).then(containerRef => { + // TODO(tbosch): clean this up when we have custom renderers (https://github.com/angular/angular/issues/1807) + // TODO(jelbourn): Don't use direct DOM access. Need abstraction to create an element + // directly on the document body (also needed for web workers stuff). + // Create a DOM node to serve as a physical host element for the dialog. + var dialogElement = this.domRenderer.getHostElement(containerRef.hostView.render); + DOM.appendChild(DOM.query('body'), dialogElement); + + // TODO(jelbourn): Use hostProperties binding to set these once #1539 is fixed. + // Configure properties on the host element. + DOM.addClass(dialogElement, 'md-dialog'); + DOM.setAttribute(dialogElement, 'tabindex', '0'); + + // TODO(jelbourn): Do this with hostProperties (or another rendering abstraction) once ready. + if (isPresent(config.width)) { + DOM.setStyle(dialogElement, 'width', config.width); + } + if (isPresent(config.height)) { + DOM.setStyle(dialogElement, 'height', config.height); + } + dialogRef.containerRef = containerRef; // Now load the given component into the MdDialogContainer. @@ -102,18 +104,14 @@ export class MdDialog { /** Loads the dialog backdrop (transparent overlay over the rest of the page). */ _openBackdrop(elementRef:ElementRef, injector: Injector): Promise { - var backdropElement = this._createHostElement(); - DOM.addClass(backdropElement, 'md-backdrop'); - DOM.appendChild(DOM.query('body'), backdropElement); - return this.componentLoader.loadIntoNewLocation( - MdBackdrop, elementRef, `:document#${backdropElement.id}`, injector); - } - - _createHostElement() { - var hostElement = DOM.createElement('div'); - hostElement.id = `mdDialog${_nextDialogId++}`; - return hostElement; + MdBackdrop, elementRef, injector).then( (componentRef) => { + // TODO(tbosch): clean this up when we have custom renderers (https://github.com/angular/angular/issues/1807) + var backdropElement = this.domRenderer.getHostElement(componentRef.hostView.render); + DOM.addClass(backdropElement, 'md-backdrop'); + DOM.appendChild(DOM.query('body'), backdropElement); + return componentRef; + }); } alert(message: string, okMessage: string): Promise {