diff --git a/modules/angular2/src/core/compiler/view.ts b/modules/angular2/src/core/compiler/view.ts index d921224481..fa55fc336b 100644 --- a/modules/angular2/src/core/compiler/view.ts +++ b/modules/angular2/src/core/compiler/view.ts @@ -23,12 +23,9 @@ import * as renderApi from 'angular2/src/render/api'; import {EventDispatcher} from 'angular2/src/render/api'; export class AppViewContainer { - views: List; - - constructor() { - // The order in this list matches the DOM order. - this.views = []; - } + // The order in this list matches the DOM order. + views: List = []; + freeViews: List = []; } /** diff --git a/modules/angular2/src/core/compiler/view_manager.ts b/modules/angular2/src/core/compiler/view_manager.ts index 254613f942..a1e73d0d18 100644 --- a/modules/angular2/src/core/compiler/view_manager.ts +++ b/modules/angular2/src/core/compiler/view_manager.ts @@ -115,6 +115,24 @@ export class AppViewManager { this._destroyFreeHostView(parentView, hostView); } + createFreeEmbeddedView(location: ElementRef, protoViewRef: ProtoViewRef, + injector: Injector = null): ViewRef { + var protoView = internalProtoView(protoViewRef); + var parentView = internalView(location.parentView); + var boundElementIndex = location.boundElementIndex; + + var view = this._createPooledView(protoView); + this._utils.attachAndHydrateFreeEmbeddedView(parentView, boundElementIndex, view, injector); + this._viewHydrateRecurse(view); + return new ViewRef(view); + } + + destroyFreeEmbeddedView(location: ElementRef, viewRef: ViewRef) { + var parentView = internalView(location.parentView); + var boundElementIndex = location.boundElementIndex; + this._destroyFreeEmbeddedView(parentView, boundElementIndex, internalView(viewRef)); + } + createViewInContainer(viewContainerLocation: ElementRef, atIndex: number, protoViewRef: ProtoViewRef, context: ElementRef = null, injector: Injector = null): ViewRef { @@ -225,11 +243,18 @@ export class AppViewManager { _destroyFreeHostView(parentView, hostView) { this._viewDehydrateRecurse(hostView, true); - this._renderer.detachFreeHostView(parentView.render, hostView.render); + this._renderer.detachFreeView(hostView.render); this._utils.detachFreeHostView(parentView, hostView); this._destroyPooledView(hostView); } + _destroyFreeEmbeddedView(parentView, boundElementIndex, view) { + this._viewDehydrateRecurse(view, false); + this._renderer.detachFreeView(view.render); + this._utils.detachFreeEmbeddedView(parentView, boundElementIndex, view); + this._destroyPooledView(view); + } + _viewHydrateRecurse(view: viewModule.AppView) { this._renderer.hydrateView(view.render); @@ -260,6 +285,9 @@ export class AppViewManager { for (var j = vc.views.length - 1; j >= 0; j--) { this._destroyViewInContainer(view, i, j); } + for (var j = vc.freeViews.length - 1; j >= 0; j--) { + this._destroyFreeEmbeddedView(view, i, j); + } } } diff --git a/modules/angular2/src/core/compiler/view_manager_utils.ts b/modules/angular2/src/core/compiler/view_manager_utils.ts index 7b6a900459..a070263073 100644 --- a/modules/angular2/src/core/compiler/view_manager_utils.ts +++ b/modules/angular2/src/core/compiler/view_manager_utils.ts @@ -110,6 +110,28 @@ export class AppViewManagerUtils { ListWrapper.remove(parentView.freeHostViews, hostView); } + attachAndHydrateFreeEmbeddedView(parentView: viewModule.AppView, boundElementIndex: number, + view: viewModule.AppView, injector: Injector = null) { + parentView.changeDetector.addChild(view.changeDetector); + var viewContainer = this._getOrCreateViewContainer(parentView, boundElementIndex); + ListWrapper.push(viewContainer.freeViews, view); + var elementInjector = parentView.elementInjectors[boundElementIndex]; + for (var i = view.rootElementInjectors.length - 1; i >= 0; i--) { + view.rootElementInjectors[i].link(elementInjector); + } + this._hydrateView(view, injector, elementInjector, parentView.context, parentView.locals); + } + + detachFreeEmbeddedView(parentView: viewModule.AppView, boundElementIndex: number, + view: viewModule.AppView) { + var viewContainer = parentView.viewContainers[boundElementIndex]; + view.changeDetector.remove(); + ListWrapper.remove(viewContainer.freeViews, view); + for (var i = 0; i < view.rootElementInjectors.length; ++i) { + view.rootElementInjectors[i].unlink(); + } + } + attachViewInContainer(parentView: viewModule.AppView, boundElementIndex: number, contextView: viewModule.AppView, contextBoundElementIndex: number, atIndex: number, view: viewModule.AppView) { @@ -118,11 +140,7 @@ export class AppViewManagerUtils { contextBoundElementIndex = boundElementIndex; } parentView.changeDetector.addChild(view.changeDetector); - var viewContainer = parentView.viewContainers[boundElementIndex]; - if (isBlank(viewContainer)) { - viewContainer = new viewModule.AppViewContainer(); - parentView.viewContainers[boundElementIndex] = viewContainer; - } + var viewContainer = this._getOrCreateViewContainer(parentView, boundElementIndex); ListWrapper.insert(viewContainer.views, atIndex, view); var sibling; if (atIndex == 0) { @@ -208,6 +226,15 @@ export class AppViewManagerUtils { view.changeDetector.hydrate(view.context, view.locals, view); } + _getOrCreateViewContainer(parentView: viewModule.AppView, boundElementIndex: number) { + var viewContainer = parentView.viewContainers[boundElementIndex]; + if (isBlank(viewContainer)) { + viewContainer = new viewModule.AppViewContainer(); + parentView.viewContainers[boundElementIndex] = viewContainer; + } + return viewContainer; + } + _setUpEventEmitters(view: viewModule.AppView, elementInjector: eli.ElementInjector, boundElementIndex: number) { var emitters = elementInjector.getEventEmitterAccessors(); diff --git a/modules/angular2/src/render/api.ts b/modules/angular2/src/render/api.ts index 2132ab0396..3abf9ac063 100644 --- a/modules/angular2/src/render/api.ts +++ b/modules/angular2/src/render/api.ts @@ -237,9 +237,9 @@ export class Renderer { } /** - * Detaches a free host view's element from the DOM. + * Detaches a free view's element from the DOM. */ - detachFreeHostView(parentHostViewRef: RenderViewRef, hostViewRef: RenderViewRef) {} + detachFreeView(view: RenderViewRef) {} /** * Creates a regular view out of the given ProtoView diff --git a/modules/angular2/src/render/dom/dom_renderer.ts b/modules/angular2/src/render/dom/dom_renderer.ts index 00cab37d7e..9bea4069d0 100644 --- a/modules/angular2/src/render/dom/dom_renderer.ts +++ b/modules/angular2/src/render/dom/dom_renderer.ts @@ -47,9 +47,9 @@ export class DomRenderer extends Renderer { return new DomViewRef(this._createView(hostProtoView, element)); } - detachFreeHostView(parentHostViewRef: RenderViewRef, hostViewRef: RenderViewRef) { - var hostView = resolveInternalDomView(hostViewRef); - this._removeViewNodes(hostView); + detachFreeView(viewRef: RenderViewRef) { + var view = resolveInternalDomView(viewRef); + this._removeViewNodes(view); } createView(protoViewRef: RenderProtoViewRef): RenderViewRef { @@ -83,9 +83,8 @@ export class DomRenderer extends Renderer { this._moveViewNodesIntoParent(componentView.shadowRoot, componentView); } - getHostElement(hostViewRef: RenderViewRef) { - var hostView = resolveInternalDomView(hostViewRef); - return hostView.boundElements[0]; + getRootNodes(viewRef: RenderViewRef): List { + return resolveInternalDomView(viewRef).rootNodes; } detachComponentView(hostViewRef: RenderViewRef, boundElementIndex: number, diff --git a/modules/angular2/test/core/compiler/dynamic_component_loader_spec.ts b/modules/angular2/test/core/compiler/dynamic_component_loader_spec.ts index dfd6eb0cde..738969d6d8 100644 --- a/modules/angular2/test/core/compiler/dynamic_component_loader_spec.ts +++ b/modules/angular2/test/core/compiler/dynamic_component_loader_spec.ts @@ -257,7 +257,7 @@ class ImperativeViewComponentUsingNgComponent { renderer.setComponentViewRootNodes(shadowViewRef.render, [div]); this.done = dynamicComponentLoader.loadIntoNewLocation(ChildComp, self, null) .then((componentRef) => { - var element = renderer.getHostElement(componentRef.hostView.render); + var element = renderer.getRootNodes(componentRef.hostView.render)[0]; DOM.appendChild(div, element); return componentRef; }); diff --git a/modules/angular2/test/core/compiler/integration_spec.ts b/modules/angular2/test/core/compiler/integration_spec.ts index ae65374363..b82ab7a6cb 100644 --- a/modules/angular2/test/core/compiler/integration_spec.ts +++ b/modules/angular2/test/core/compiler/integration_spec.ts @@ -29,11 +29,12 @@ import { isJsObject, global, stringify, - CONST + CONST, + CONST_EXPR } from 'angular2/src/facade/lang'; import {PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; -import {Injector, bind, Injectable, Binding, FORWARD_REF} from 'angular2/di'; +import {Injector, bind, Injectable, Binding, FORWARD_REF, OpaqueToken, Inject} from 'angular2/di'; import { PipeRegistry, defaultPipeRegistry, @@ -63,17 +64,21 @@ import {NgIf} from 'angular2/src/directives/ng_if'; import {NgFor} from 'angular2/src/directives/ng_for'; import {ViewContainerRef} from 'angular2/src/core/compiler/view_container_ref'; -import {ProtoViewRef} from 'angular2/src/core/compiler/view_ref'; +import {ProtoViewRef, ViewRef} from 'angular2/src/core/compiler/view_ref'; import {Compiler} from 'angular2/src/core/compiler/compiler'; import {ElementRef} from 'angular2/src/core/compiler/element_ref'; import {DomRenderer} from 'angular2/src/render/dom/dom_renderer'; import {AppViewManager} from 'angular2/src/core/compiler/view_manager'; +const ANCHOR_ELEMENT = CONST_EXPR(new OpaqueToken('AnchorElement')); + export function main() { describe('integration tests', function() { var ctx; + beforeEachBindings(() => [bind(ANCHOR_ELEMENT).toValue(el('
'))]); + beforeEach(() => { ctx = new MyComp(); }); @@ -1124,6 +1129,28 @@ export function main() { }); })); + it('should support free embedded views', + inject([TestBed, AsyncTestCompleter, ANCHOR_ELEMENT], (tb, async, anchorElement) => { + tb.overrideView(MyComp, new viewAnn.View({ + template: '
hello
', + directives: [SomeImperativeViewport] + })); + tb.createView(MyComp).then((view) => { + view.detectChanges(); + expect(anchorElement).toHaveText(''); + + view.context.ctxBoolProp = true; + view.detectChanges(); + expect(anchorElement).toHaveText('hello'); + + view.context.ctxBoolProp = false; + view.detectChanges(); + expect(view.rootNodes).toHaveText(''); + + async.done(); + }); + })); + // Disabled until a solution is found, refs: // - https://github.com/angular/angular/issues/776 // - https://github.com/angular/angular/commit/81f3f32 @@ -1640,3 +1667,30 @@ class ChildConsumingEventBus { constructor(@Unbounded() bus: EventBus) { this.bus = bus; } } + +@Directive({selector: '[some-impvp]', properties: ['someImpvp']}) +@Injectable() +class SomeImperativeViewport { + view: ViewRef; + anchor; + constructor(public element: ElementRef, public protoView: ProtoViewRef, + public viewManager: AppViewManager, public renderer: DomRenderer, + @Inject(ANCHOR_ELEMENT) anchor) { + this.view = null; + this.anchor = anchor; + } + + set someImpvp(value: boolean) { + if (isPresent(this.view)) { + this.viewManager.destroyFreeEmbeddedView(this.element, this.view); + this.view = null; + } + if (value) { + this.view = this.viewManager.createFreeEmbeddedView(this.element, this.protoView); + var nodes = this.renderer.getRootNodes(this.view.render); + for (var i = 0; i < nodes.length; i++) { + DOM.appendChild(this.anchor, nodes[i]); + } + } + } +} diff --git a/modules/angular2/test/core/compiler/view_manager_spec.ts b/modules/angular2/test/core/compiler/view_manager_spec.ts index 957e6a48ab..b33a859eb7 100644 --- a/modules/angular2/test/core/compiler/view_manager_spec.ts +++ b/modules/angular2/test/core/compiler/view_manager_spec.ts @@ -377,8 +377,7 @@ export function main() { it('should detach the render view', () => { manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); - expect(renderer.spy('detachFreeHostView')) - .toHaveBeenCalledWith(parentView.render, hostRenderViewRef); + expect(renderer.spy('detachFreeView')).toHaveBeenCalledWith(hostRenderViewRef); }); it('should return the view to the pool', () => { @@ -402,6 +401,101 @@ export function main() { }); + describe('createFreeEmbeddedView', () => { + + // 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... + + describe('basic functionality', () => { + var parentView, childProtoView; + beforeEach(() => { + parentView = createView(createProtoView([createEmptyElBinder()])); + childProtoView = createProtoView(); + }); + + it('should create the view', () => { + expect(internalView(manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0), + wrapPv(childProtoView), null))) + .toBe(createdViews[0]); + expect(createdViews[0].proto).toBe(childProtoView); + expect(viewListener.spy('viewCreated')).toHaveBeenCalledWith(createdViews[0]); + }); + + it('should attachAndHydrate the view', () => { + var injector = new Injector([], null, false); + manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0), + wrapPv(childProtoView), injector); + expect(utils.spy('attachAndHydrateFreeEmbeddedView')) + .toHaveBeenCalledWith(parentView, 0, createdViews[0], injector); + expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(createdViews[0].render); + }); + + it('should create and set the render view', () => { + manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0), + wrapPv(childProtoView), null); + expect(renderer.spy('createView')).toHaveBeenCalledWith(childProtoView.render); + expect(createdViews[0].render).toBe(createdRenderViews[0]); + }); + + it('should set the event dispatcher', () => { + manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0), + wrapPv(childProtoView), null); + var cmpView = createdViews[0]; + expect(renderer.spy('setEventDispatcher')).toHaveBeenCalledWith(cmpView.render, cmpView); + }); + }); + + }); + + + describe('destroyFreeEmbeddedView', () => { + describe('basic functionality', () => { + var parentView, childProtoView, childView; + beforeEach(() => { + parentView = createView(createProtoView([createEmptyElBinder()])); + childProtoView = createProtoView(); + childView = internalView(manager.createFreeEmbeddedView( + elementRef(wrapView(parentView), 0), wrapPv(childProtoView), null)); + }); + + it('should detach', () => { + manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView)); + expect(utils.spy('detachFreeEmbeddedView')) + .toHaveBeenCalledWith(parentView, 0, childView); + }); + + it('should dehydrate', () => { + manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView)); + expect(utils.spy('dehydrateView')).toHaveBeenCalledWith(childView); + expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(childView.render); + }); + + it('should detach the render view', () => { + manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView)); + expect(renderer.spy('detachFreeView')).toHaveBeenCalledWith(childView.render); + }); + + it('should return the view to the pool', () => { + manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView)); + expect(viewPool.spy('returnView')).toHaveBeenCalledWith(childView); + expect(renderer.spy('destroyView')).not.toHaveBeenCalled(); + }); + + it('should destroy the view if the pool is full', () => { + viewPool.spy('returnView').andReturn(false); + manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView)); + expect(renderer.spy('destroyView')).toHaveBeenCalledWith(childView.render); + expect(viewListener.spy('viewDestroyed')).toHaveBeenCalledWith(childView); + }); + + }); + + describe('recursively destroyFreeEmbeddedView', () => { + // TODO + }); + + }); + describe('createRootHostView', () => { var hostProtoView; diff --git a/modules/angular2/test/render/dom/dom_renderer_integration_spec.ts b/modules/angular2/test/render/dom/dom_renderer_integration_spec.ts index 6631fe9944..6f5fb65662 100644 --- a/modules/angular2/test/render/dom/dom_renderer_integration_spec.ts +++ b/modules/angular2/test/render/dom/dom_renderer_integration_spec.ts @@ -40,17 +40,16 @@ export function main() { }); })); - it('should create and destroy free host views', + it('should create and destroy free 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); + var hostElement = tb.renderer.getRootNodes(view.viewRef)[0]; DOM.appendChild(tb.rootEl, hostElement); - tb.renderer.detachFreeHostView(null, view.viewRef); + tb.renderer.detachFreeView(view.viewRef); expect(DOM.parentElement(hostElement)).toBeFalsy(); - async.done(); }); })); diff --git a/modules/angular2_material/src/components/dialog/dialog.ts b/modules/angular2_material/src/components/dialog/dialog.ts index d56b88c918..b49188e38c 100644 --- a/modules/angular2_material/src/components/dialog/dialog.ts +++ b/modules/angular2_material/src/components/dialog/dialog.ts @@ -66,7 +66,7 @@ export class MdDialog { // 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); + var dialogElement = this.domRenderer.getRootNodes(containerRef.hostView.render)[0]; DOM.appendChild(DOM.query('body'), dialogElement); // TODO(jelbourn): Use hostProperties binding to set these once #1539 is fixed. @@ -111,7 +111,7 @@ export class MdDialog { .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); + var backdropElement = this.domRenderer.getRootNodes(componentRef.hostView.render)[0]; DOM.addClass(backdropElement, 'md-backdrop'); DOM.appendChild(DOM.query('body'), backdropElement); return componentRef;