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