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 [
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<Binding> {
return componentRef;
});
}, [DynamicComponentLoader, Injector, DirectiveResolver,
}, [DynamicComponentLoader, Injector,
Testability, TestabilityRegistry]),
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
* 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<ComponentRef> {
loadAsRoot(typeOrBinding, overrideSelector = null, injector:Injector = null):Promise<ComponentRef> {
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<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);
});

View File

@ -32,7 +32,7 @@ export class AppView {
componentChildViews: List<AppView>;
/// Host views that were added by an imperative view.
/// This is a dynamically growing / shrinking array.
inPlaceHostViews: List<AppView>;
freeHostViews: List<AppView>;
viewContainers: List<AppViewContainer>;
preBuiltObjects: List<PreBuiltObjects>;
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,

View File

@ -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);
}
}
}

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) {
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,

View File

@ -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) {
}
/**

View File

@ -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);

View File

@ -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);
});
}

View File

@ -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('<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'
})
@View({
renderer: 'imp-ng-cmp-renderer',
template: ''
})
class ImperativeViewComponentUsingNgComponent {
@ -210,7 +241,11 @@ class ImperativeViewComponentUsingNgComponent {
var div = el('<div id="impHost"></div>');
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;
});
}
}

View File

@ -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);
});

View File

@ -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]);
});
});
});
}

View File

@ -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();
});

View File

@ -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);
}

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 {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<MdDialogRef> {
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<ComponentRef> {
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 {