refactor(view_manager): split `inPlace` views into root and free host views.

BREAKING CHANGE:
`AppViewManager.createInPlaceHostView` is replaced by
`AppViewManager.createRootHostView` (for bootstrap) and
`AppViewManager.createFreeHostView` (for imperative components).

The later creates new host elements that are not attached anywhere.
To attach them, use `DomRenderer.getHostElement(hostviewRef)`
to get the host element.

Closes #1920
This commit is contained in:
Tobias Bosch 2015-05-15 09:55:43 -07:00
parent a38a0d6f87
commit 421d8916a6
14 changed files with 310 additions and 158 deletions

View File

@ -54,13 +54,10 @@ function _injectorBindings(appComponentType): List<Binding> {
return [ return [
bind(DOCUMENT_TOKEN).toValue(DOM.defaultDoc()), bind(DOCUMENT_TOKEN).toValue(DOM.defaultDoc()),
bind(appComponentRefToken).toAsyncFactory((dynamicComponentLoader, injector, 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. // 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); var domView = resolveInternalDomView(componentRef.hostView.render);
// 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.
@ -68,7 +65,7 @@ function _injectorBindings(appComponentType): List<Binding> {
return componentRef; return componentRef;
}); });
}, [DynamicComponentLoader, Injector, DirectiveResolver, }, [DynamicComponentLoader, Injector,
Testability, TestabilityRegistry]), Testability, TestabilityRegistry]),
bind(appComponentType).toFactory((ref) => ref.instance, bind(appComponentType).toFactory((ref) => ref.instance,

View File

@ -62,19 +62,38 @@ export class DynamicComponentLoader {
} }
/** /**
* Loads a component in the element specified by elementSelector. The loaded component receives * Loads a root component that is placed at the first element that matches the
* injection normally as a hosted view. * component's selector.
* The loaded component receives injection normally as a hosted view.
*/ */
loadIntoNewLocation(typeOrBinding, parentComponentLocation:ElementRef, elementSelector:string, loadAsRoot(typeOrBinding, overrideSelector = null, injector:Injector = null):Promise<ComponentRef> {
injector:Injector = null):Promise<ComponentRef> {
return this._compiler.compileInHost(this._getBinding(typeOrBinding)).then(hostProtoViewRef => { return this._compiler.compileInHost(this._getBinding(typeOrBinding)).then(hostProtoViewRef => {
var hostViewRef = this._viewManager.createInPlaceHostView( var hostViewRef = this._viewManager.createRootHostView(hostProtoViewRef, overrideSelector, injector);
parentComponentLocation, elementSelector, hostProtoViewRef, injector);
var newLocation = new ElementRef(hostViewRef, 0); var newLocation = new ElementRef(hostViewRef, 0);
var component = this._viewManager.getComponent(newLocation); var component = this._viewManager.getComponent(newLocation);
var dispose = () => { 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<ComponentRef> {
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); return new ComponentRef(newLocation, component, dispose);
}); });

View File

@ -32,7 +32,7 @@ export class AppView {
componentChildViews: List<AppView>; componentChildViews: List<AppView>;
/// Host views that were added by an imperative view. /// Host views that were added by an imperative view.
/// This is a dynamically growing / shrinking array. /// This is a dynamically growing / shrinking array.
inPlaceHostViews: List<AppView>; freeHostViews: List<AppView>;
viewContainers: List<AppViewContainer>; viewContainers: List<AppViewContainer>;
preBuiltObjects: List<PreBuiltObjects>; preBuiltObjects: List<PreBuiltObjects>;
proto: AppProtoView; proto: AppProtoView;
@ -64,7 +64,7 @@ export class AppView {
this.context = null; this.context = null;
this.locals = new Locals(null, MapWrapper.clone(protoLocals)); //TODO optimize this this.locals = new Locals(null, MapWrapper.clone(protoLocals)); //TODO optimize this
this.renderer = renderer; this.renderer = renderer;
this.inPlaceHostViews = []; this.freeHostViews = [];
} }
init(changeDetector:ChangeDetector, elementInjectors:List, rootElementInjectors:List, init(changeDetector:ChangeDetector, elementInjectors:List, rootElementInjectors:List,

View File

@ -62,33 +62,46 @@ export class AppViewManager {
return new ViewRef(componentView); return new ViewRef(componentView);
} }
createInPlaceHostView(parentComponentLocation:ElementRef, createRootHostView(hostProtoViewRef:ProtoViewRef, overrideSelector:string, injector:Injector):ViewRef {
hostElementSelector:string, hostProtoViewRef:ProtoViewRef, injector:Injector):ViewRef {
var hostProtoView = internalProtoView(hostProtoViewRef); var hostProtoView = internalProtoView(hostProtoViewRef);
var parentComponentHostView = null; var hostElementSelector = overrideSelector;
var parentComponentBoundElementIndex = null; if (isBlank(hostElementSelector)) {
var parentRenderViewRef = null; hostElementSelector = hostProtoView.elementBinders[0].componentDirective.metadata.selector;
if (isPresent(parentComponentLocation)) {
parentComponentHostView = internalView(parentComponentLocation.parentView);
parentComponentBoundElementIndex = parentComponentLocation.boundElementIndex;
parentRenderViewRef = parentComponentHostView.componentChildViews[parentComponentBoundElementIndex].render;
} }
var hostRenderView = this._renderer.createInPlaceHostView(parentRenderViewRef, hostElementSelector, hostProtoView.render); var renderView = this._renderer.createRootHostView(hostProtoView.render, hostElementSelector);
var hostView = this._utils.createView(hostProtoView, hostRenderView, this, this._renderer); var hostView = this._utils.createView(hostProtoView, renderView, this, this._renderer);
this._renderer.setEventDispatcher(hostView.render, hostView); this._renderer.setEventDispatcher(hostView.render, hostView);
this._createViewRecurse(hostView) this._createViewRecurse(hostView);
this._utils.attachAndHydrateInPlaceHostView(parentComponentHostView, parentComponentBoundElementIndex, hostView, injector);
this._utils.hydrateRootHostView(hostView, injector);
this._viewHydrateRecurse(hostView); this._viewHydrateRecurse(hostView);
return new ViewRef(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 hostView = internalView(hostViewRef);
var parentView = null; // We do want to destroy the component view though.
if (isPresent(parentComponentLocation)) { this._viewDehydrateRecurse(hostView, true);
parentView = internalView(parentComponentLocation.parentView).componentChildViews[parentComponentLocation.boundElementIndex]; this._renderer.destroyView(hostView.render);
} }
this._destroyInPlaceHostView(parentView, hostView);
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, createViewInContainer(viewContainerLocation:ElementRef,
@ -186,16 +199,11 @@ export class AppViewManager {
this._destroyPooledView(componentView); this._destroyPooledView(componentView);
} }
_destroyInPlaceHostView(parentView, hostView) { _destroyFreeHostView(parentView, hostView) {
var parentRenderViewRef = null;
if (isPresent(parentView)) {
parentRenderViewRef = parentView.render;
}
this._viewDehydrateRecurse(hostView, true); this._viewDehydrateRecurse(hostView, true);
this._utils.detachInPlaceHostView(parentView, hostView); this._renderer.detachFreeHostView(parentView.render, hostView.render);
this._renderer.destroyInPlaceHostView(parentRenderViewRef, hostView.render); this._utils.detachFreeHostView(parentView, hostView);
// Note: Don't put the inplace host view into the view pool this._destroyPooledView(hostView);
// as it is depending on the element for which it was created.
} }
_viewHydrateRecurse( _viewHydrateRecurse(
@ -234,10 +242,10 @@ export class AppViewManager {
} }
} }
// inPlaceHostViews // freeHostViews
for (var i = view.inPlaceHostViews.length-1; i>=0; i--) { for (var i = view.freeHostViews.length-1; i>=0; i--) {
var hostView = view.inPlaceHostViews[i]; var hostView = view.freeHostViews[i];
this._destroyInPlaceHostView(view, hostView); this._destroyFreeHostView(view, hostView);
} }
} }
} }

View File

@ -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) { hostView:viewModule.AppView, injector:Injector = null) {
var hostElementInjector = null; var hostElementInjector = parentComponentHostView.elementInjectors[parentComponentBoundElementIndex];
if (isPresent(parentComponentHostView)) { var parentView = parentComponentHostView.componentChildViews[parentComponentBoundElementIndex];
hostElementInjector = parentComponentHostView.elementInjectors[parentComponentBoundElementIndex]; parentView.changeDetector.addChild(hostView.changeDetector);
var parentView = parentComponentHostView.componentChildViews[parentComponentBoundElementIndex]; ListWrapper.push(parentView.freeHostViews, hostView);
parentView.changeDetector.addChild(hostView.changeDetector);
ListWrapper.push(parentView.inPlaceHostViews, hostView);
}
this._hydrateView(hostView, injector, hostElementInjector, new Object(), null); this._hydrateView(hostView, injector, hostElementInjector, new Object(), null);
} }
detachInPlaceHostView(parentView:viewModule.AppView, detachFreeHostView(parentView:viewModule.AppView,
hostView:viewModule.AppView) { hostView:viewModule.AppView) {
if (isPresent(parentView)) { parentView.changeDetector.removeChild(hostView.changeDetector);
parentView.changeDetector.removeChild(hostView.changeDetector); ListWrapper.remove(parentView.freeHostViews, hostView);
ListWrapper.remove(parentView.inPlaceHostViews, hostView);
}
} }
attachViewInContainer(parentView:viewModule.AppView, boundElementIndex:number, attachViewInContainer(parentView:viewModule.AppView, boundElementIndex:number,

View File

@ -187,20 +187,19 @@ export class RenderCompiler {
export class Renderer { export class Renderer {
/** /**
* Creates a host view that includes the given element. * Creates a root host view that includes the given element.
* @param {RenderViewRef} parentHostViewRef (might be null)
* @param {any} hostElementSelector css selector for the host element
* @param {RenderProtoViewRef} hostProtoViewRef a RenderProtoViewRef of type ProtoViewDto.HOST_VIEW_TYPE * @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 * @return {RenderViewRef} the created view
*/ */
createInPlaceHostView(parentHostViewRef:RenderViewRef, hostElementSelector:string, hostProtoViewRef:RenderProtoViewRef):RenderViewRef { createRootHostView(hostProtoViewRef:RenderProtoViewRef, hostElementSelector:string):RenderViewRef {
return null; 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) {
} }
/** /**

View File

@ -19,8 +19,6 @@ import {Renderer, RenderProtoViewRef, RenderViewRef} from '../api';
// const expressions! // const expressions!
export const DOCUMENT_TOKEN = 'DocumentToken'; export const DOCUMENT_TOKEN = 'DocumentToken';
var _DOCUMENT_SELECTOR_REGEX = RegExpWrapper.create('\\:document(.+)');
@Injectable() @Injectable()
export class DomRenderer extends Renderer { export class DomRenderer extends Renderer {
_eventManager:EventManager; _eventManager:EventManager;
@ -34,27 +32,16 @@ export class DomRenderer extends Renderer {
this._document = document; this._document = document;
} }
createInPlaceHostView(parentHostViewRef:RenderViewRef, hostElementSelector:string, hostProtoViewRef:RenderProtoViewRef):RenderViewRef { createRootHostView(hostProtoViewRef:RenderProtoViewRef, hostElementSelector:string):RenderViewRef {
var containerNode; var hostProtoView = resolveInternalDomProtoView(hostProtoViewRef);
var documentSelectorMatch = RegExpWrapper.firstMatch(_DOCUMENT_SELECTOR_REGEX, hostElementSelector); var element = DOM.querySelector(this._document, 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);
if (isBlank(element)) { if (isBlank(element)) {
throw new BaseException(`The selector "${hostElementSelector}" did not match any elements`); throw new BaseException(`The selector "${hostElementSelector}" did not match any elements`);
} }
var hostProtoView = resolveInternalDomProtoView(hostProtoViewRef);
return new DomViewRef(this._createView(hostProtoView, element)); return new DomViewRef(this._createView(hostProtoView, element));
} }
destroyInPlaceHostView(parentHostViewRef:RenderViewRef, hostViewRef:RenderViewRef) { detachFreeHostView(parentHostViewRef:RenderViewRef, hostViewRef:RenderViewRef) {
var hostView = resolveInternalDomView(hostViewRef); var hostView = resolveInternalDomView(hostViewRef);
this._removeViewNodes(hostView); this._removeViewNodes(hostView);
} }
@ -89,6 +76,11 @@ export class DomRenderer extends Renderer {
this._moveViewNodesIntoParent(componentView.shadowRoot, componentView); this._moveViewNodesIntoParent(componentView.shadowRoot, componentView);
} }
getHostElement(hostViewRef:RenderViewRef) {
var hostView = resolveInternalDomView(hostViewRef);
return hostView.boundElements[0];
}
detachComponentView(hostViewRef:RenderViewRef, boundElementIndex:number, componentViewRef:RenderViewRef) { detachComponentView(hostViewRef:RenderViewRef, boundElementIndex:number, componentViewRef:RenderViewRef) {
var hostView = resolveInternalDomView(hostViewRef); var hostView = resolveInternalDomView(hostViewRef);
var componentView = resolveInternalDomView(componentViewRef); var componentView = resolveInternalDomView(componentViewRef);

View File

@ -94,7 +94,7 @@ export class TestBed {
DOM.appendChild(doc.body, rootEl); DOM.appendChild(doc.body, rootEl);
var componentBinding = bind(component).toValue(context); 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); return new ViewProxy(hostComponentRef);
}); });
} }

View File

@ -11,17 +11,18 @@ import {
inject, inject,
beforeEachBindings, beforeEachBindings,
it, it,
xit xit,
viewRootNodes
} from 'angular2/test_lib'; } 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 {Component} from 'angular2/src/core/annotations_impl/annotations';
import {View} from 'angular2/src/core/annotations_impl/view'; import {View} from 'angular2/src/core/annotations_impl/view';
import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader'; import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader';
import {ElementRef} from 'angular2/src/core/compiler/element_ref'; import {ElementRef} from 'angular2/src/core/compiler/element_ref';
import {NgIf} from 'angular2/src/directives/ng_if'; 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 {DOM} from 'angular2/src/dom/dom_adapter';
import {AppViewManager} from 'angular2/src/core/compiler/view_manager'; 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('<child-cmp></child-cmp>');
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' selector: 'imp-ng-cmp'
}) })
@View({ @View({
renderer: 'imp-ng-cmp-renderer',
template: '' template: ''
}) })
class ImperativeViewComponentUsingNgComponent { class ImperativeViewComponentUsingNgComponent {
@ -210,7 +241,11 @@ class ImperativeViewComponentUsingNgComponent {
var div = el('<div id="impHost"></div>'); var div = el('<div id="impHost"></div>');
var shadowViewRef = viewManager.getComponentView(self); var shadowViewRef = viewManager.getComponentView(self);
renderer.setComponentViewRootNodes(shadowViewRef.render, [div]); 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;
});
} }
} }

View File

@ -141,7 +141,7 @@ export function main() {
} }
ListWrapper.insert(viewContainer.views, atIndex, childView); ListWrapper.insert(viewContainer.views, atIndex, childView);
}); });
renderer.spy('createInPlaceHostView').andCallFake( (_a, _b, _c) => { renderer.spy('createRootHostView').andCallFake( (_b, _c) => {
var rv = new RenderViewRef(); var rv = new RenderViewRef();
ListWrapper.push(createdRenderViews, rv); ListWrapper.push(createdRenderViews, rv);
return 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 // 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... // this is using the same mechanism as the other methods...
@ -314,27 +314,26 @@ export function main() {
it('should create the view', () => { it('should create the view', () => {
expect( 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]); ).toBe(createdViews[0]);
expect(createdViews[0].proto).toBe(hostProtoView); expect(createdViews[0].proto).toBe(hostProtoView);
}); });
it('should attachAndHydrate the view', () => { it('should attachAndHydrate the view', () => {
var injector = new Injector([], null, false); var injector = new Injector([], null, false);
manager.createInPlaceHostView(elementRef(wrapView(parentHostView), 0), null, wrapPv(hostProtoView), injector); manager.createFreeHostView(elementRef(wrapView(parentHostView), 0), wrapPv(hostProtoView), injector);
expect(utils.spy('attachAndHydrateInPlaceHostView')).toHaveBeenCalledWith(parentHostView, 0, createdViews[0], injector); expect(utils.spy('attachAndHydrateFreeHostView')).toHaveBeenCalledWith(parentHostView, 0, createdViews[0], injector);
expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(createdViews[0].render); expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(createdViews[0].render);
}); });
it('should create and set the render view', () => { it('should create and set the render view', () => {
var elementOrSelector = 'someSelector'; manager.createFreeHostView(elementRef(wrapView(parentHostView), 0), wrapPv(hostProtoView), null)
manager.createInPlaceHostView(elementRef(wrapView(parentHostView), 0), elementOrSelector, wrapPv(hostProtoView), null) expect(renderer.spy('createView')).toHaveBeenCalledWith(hostProtoView.render);
expect(renderer.spy('createInPlaceHostView')).toHaveBeenCalledWith(parentView.render, elementOrSelector, hostProtoView.render);
expect(createdViews[0].render).toBe(createdRenderViews[0]); expect(createdViews[0].render).toBe(createdRenderViews[0]);
}); });
it('should set the event dispatcher', () => { 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]; var cmpView = createdViews[0];
expect(renderer.spy('setEventDispatcher')).toHaveBeenCalledWith(cmpView.render, cmpView); expect(renderer.spy('setEventDispatcher')).toHaveBeenCalledWith(cmpView.render, cmpView);
}); });
@ -343,7 +342,7 @@ export function main() {
}); });
describe('destroyInPlaceHostView', () => { describe('destroyFreeHostView', () => {
describe('basic functionality', () => { describe('basic functionality', () => {
var parentHostView, parentView, hostProtoView, hostView, hostRenderViewRef; var parentHostView, parentView, hostProtoView, hostView, hostRenderViewRef;
beforeEach( () => { beforeEach( () => {
@ -355,29 +354,29 @@ export function main() {
hostProtoView = createProtoView( hostProtoView = createProtoView(
[createComponentElBinder(null)] [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; hostRenderViewRef = hostView.render;
}); });
it('should detach', () => { it('should detach', () => {
manager.destroyInPlaceHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView));
expect(utils.spy('detachInPlaceHostView')).toHaveBeenCalledWith(parentView, hostView); expect(utils.spy('detachFreeHostView')).toHaveBeenCalledWith(parentView, hostView);
}); });
it('should dehydrate', () => { 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(utils.spy('dehydrateView')).toHaveBeenCalledWith(hostView);
expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(hostView.render); expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(hostView.render);
}); });
it('should destroy and clear the render view', () => { it('should detach the render view', () => {
manager.destroyInPlaceHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView));
expect(renderer.spy('destroyInPlaceHostView')).toHaveBeenCalledWith(parentView.render, hostRenderViewRef); expect(renderer.spy('detachFreeHostView')).toHaveBeenCalledWith(parentView.render, hostRenderViewRef);
}); });
it('should not return the view to the pool', () => { it('should return the view to the pool', () => {
manager.destroyInPlaceHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView));
expect(viewPool.spy('returnView')).not.toHaveBeenCalled(); 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('createViewInContainer', () => {
describe('basic functionality', () => { describe('basic functionality', () => {
@ -483,19 +554,19 @@ export function main() {
}); });
it('should dehydrate', () => { it('should dehydrate', () => {
manager.destroyInPlaceHostView(null, wrapView(parentView)); manager.destroyRootHostView(wrapView(parentView));
expect(utils.spy('dehydrateView')).toHaveBeenCalledWith(parentView.viewContainers[0].views[0]); expect(utils.spy('dehydrateView')).toHaveBeenCalledWith(parentView.viewContainers[0].views[0]);
expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(childView.render); expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(childView.render);
}); });
it('should detach', () => { it('should detach', () => {
manager.destroyInPlaceHostView(null, wrapView(parentView)); manager.destroyRootHostView(wrapView(parentView));
expect(utils.spy('detachViewInContainer')).toHaveBeenCalledWith(parentView, 0, 0); expect(utils.spy('detachViewInContainer')).toHaveBeenCalledWith(parentView, 0, 0);
expect(renderer.spy('detachViewInContainer')).toHaveBeenCalledWith(parentView.render, 0, 0, childView.render); expect(renderer.spy('detachViewInContainer')).toHaveBeenCalledWith(parentView.render, 0, 0, childView.render);
}); });
it('should return the view to the pool', () => { it('should return the view to the pool', () => {
manager.destroyInPlaceHostView(null, wrapView(parentView)); manager.destroyRootHostView(wrapView(parentView));
expect(viewPool.spy('returnView')).toHaveBeenCalledWith(childView); expect(viewPool.spy('returnView')).toHaveBeenCalledWith(childView);
}); });

View File

@ -31,7 +31,6 @@ import {AppViewManagerUtils} from 'angular2/src/core/compiler/view_manager_utils
export function main() { export function main() {
// TODO(tbosch): add more tests here! // TODO(tbosch): add more tests here!
describe('AppViewManagerUtils', () => { describe('AppViewManagerUtils', () => {
var directiveResolver; var directiveResolver;
@ -170,7 +169,7 @@ export function main() {
var shadowView = createView(); var shadowView = createView();
utils.attachComponentView(hostView, 0, shadowView); utils.attachComponentView(hostView, 0, shadowView);
utils.attachAndHydrateInPlaceHostView(null, null, hostView, createInjector()); utils.hydrateRootHostView(hostView, createInjector());
expect(spyEventAccessor1.spy('subscribe')).toHaveBeenCalledWith(hostView, 0, dir); expect(spyEventAccessor1.spy('subscribe')).toHaveBeenCalledWith(hostView, 0, dir);
expect(spyEventAccessor2.spy('subscribe')).toHaveBeenCalledWith(hostView, 1, dir); expect(spyEventAccessor2.spy('subscribe')).toHaveBeenCalledWith(hostView, 1, dir);
@ -200,7 +199,7 @@ export function main() {
var shadowView = createView(); var shadowView = createView();
utils.attachComponentView(hostView, 0, shadowView); utils.attachComponentView(hostView, 0, shadowView);
utils.attachAndHydrateInPlaceHostView(null, null, hostView, createInjector()); utils.hydrateRootHostView(hostView, createInjector());
expect(spyActionAccessor1.spy('subscribe')).toHaveBeenCalledWith(hostView, 0, dir); expect(spyActionAccessor1.spy('subscribe')).toHaveBeenCalledWith(hostView, 0, dir);
expect(spyActionAccessor2.spy('subscribe')).toHaveBeenCalledWith(hostView, 1, 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]);
});
});
}); });
} }

View File

@ -17,7 +17,7 @@ import {
import {MapWrapper} from 'angular2/src/facade/collection'; import {MapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter'; 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'; import {ViewDefinition, DirectiveMetadata, RenderViewRef} from 'angular2/src/render/api';
@ -27,15 +27,29 @@ export function main() {
DomTestbed 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) => { inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
tb.compileAll([someComponent]).then( (protoViewDtos) => { tb.compiler.compileHost(someComponent).then( (hostProtoViewDto) => {
var view = tb.createRootView(protoViewDtos[0]); var view = new TestView(tb.renderer.createRootHostView(hostProtoViewDto.render, '#root'));
expect(tb.rootEl.parentNode).toBeTruthy();
expect(view.rawView.rootNodes[0]).toEqual(tb.rootEl); expect(view.rawView.rootNodes[0]).toEqual(tb.rootEl);
tb.renderer.destroyInPlaceHostView(null, view.viewRef); tb.renderer.destroyView(view.viewRef);
expect(tb.rootEl.parentNode).toBeFalsy(); // 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(); async.done();
}); });

View File

@ -77,7 +77,7 @@ export class DomTestbed {
} }
createRootView(rootProtoView:ProtoViewDto):TestView { 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); this.renderer.hydrateView(viewRef);
return this._createTestView(viewRef); return this._createTestView(viewRef);
} }

View File

@ -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 {bind, Injector} from 'angular2/di';
import {ObservableWrapper, Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {ObservableWrapper, Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {isPresent, Type} from 'angular2/src/facade/lang'; 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 {Parent} from 'angular2/src/core/annotations_impl/visibility';
import {View} from 'angular2/src/core/annotations_impl/view'; import {View} from 'angular2/src/core/annotations_impl/view';
// TODO(jelbourn): Opener of dialog can control where it is rendered. // TODO(jelbourn): Opener of dialog can control where it is rendered.
// TODO(jelbourn): body scrolling is disabled while dialog is open. // TODO(jelbourn): body scrolling is disabled while dialog is open.
// TODO(jelbourn): Don't manually construct and configure a DOM element. See #1402 // TODO(jelbourn): Don't manually construct and configure a DOM element. See #1402
@ -29,9 +28,11 @@ var _nextDialogId = 0;
*/ */
export class MdDialog { export class MdDialog {
componentLoader: DynamicComponentLoader; componentLoader: DynamicComponentLoader;
domRenderer: DomRenderer;
constructor(loader: DynamicComponentLoader) { constructor(loader: DynamicComponentLoader, domRenderer: DomRenderer) {
this.componentLoader = loader; this.componentLoader = loader;
this.domRenderer = domRenderer;
} }
/** /**
@ -47,25 +48,6 @@ export class MdDialog {
options: MdDialogConfig = null): Promise<MdDialogRef> { options: MdDialogConfig = null): Promise<MdDialogRef> {
var config = isPresent(options) ? options : new MdDialogConfig(); 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. // Create the dialogRef here so that it can be injected into the content component.
var dialogRef = new MdDialogRef(); var dialogRef = new MdDialogRef();
@ -76,7 +58,27 @@ export class MdDialog {
// First, load the MdDialogContainer, into which the given component will be loaded. // First, load the MdDialogContainer, into which the given component will be loaded.
return this.componentLoader.loadIntoNewLocation( 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; dialogRef.containerRef = containerRef;
// Now load the given component into the MdDialogContainer. // 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). */ /** Loads the dialog backdrop (transparent overlay over the rest of the page). */
_openBackdrop(elementRef:ElementRef, injector: Injector): Promise<ComponentRef> { _openBackdrop(elementRef:ElementRef, injector: Injector): Promise<ComponentRef> {
var backdropElement = this._createHostElement();
DOM.addClass(backdropElement, 'md-backdrop');
DOM.appendChild(DOM.query('body'), backdropElement);
return this.componentLoader.loadIntoNewLocation( return this.componentLoader.loadIntoNewLocation(
MdBackdrop, elementRef, `:document#${backdropElement.id}`, injector); 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);
_createHostElement() { DOM.addClass(backdropElement, 'md-backdrop');
var hostElement = DOM.createElement('div'); DOM.appendChild(DOM.query('body'), backdropElement);
hostElement.id = `mdDialog${_nextDialogId++}`; return componentRef;
return hostElement; });
} }
alert(message: string, okMessage: string): Promise { alert(message: string, okMessage: string): Promise {