Skip to content

Commit

Permalink
refactor(view_manager): split inPlace views into root and free host…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
tbosch committed May 15, 2015
1 parent a38a0d6 commit 421d891
Show file tree
Hide file tree
Showing 14 changed files with 309 additions and 157 deletions.
9 changes: 3 additions & 6 deletions modules/angular2/src/core/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,18 @@ 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.
registry.registerApplication(domView.boundElements[0], testability);

return componentRef;
});
}, [DynamicComponentLoader, Injector, DirectiveResolver,
}, [DynamicComponentLoader, Injector,
Testability, TestabilityRegistry]),

bind(appComponentType).toFactory((ref) => ref.instance,
Expand Down
31 changes: 25 additions & 6 deletions modules/angular2/src/core/compiler/dynamic_component_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
loadAsRoot(typeOrBinding, overrideSelector = null, injector:Injector = null):Promise<ComponentRef> {
return this._compiler.compileInHost(this._getBinding(typeOrBinding)).then(hostProtoViewRef => {
var hostViewRef = this._viewManager.createRootHostView(hostProtoViewRef, overrideSelector, injector);
var newLocation = new ElementRef(hostViewRef, 0);
var component = this._viewManager.getComponent(newLocation);

var dispose = () => {
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, elementSelector:string,
loadIntoNewLocation(typeOrBinding, parentComponentLocation:ElementRef,
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.createFreeHostView(
parentComponentLocation, hostProtoViewRef, injector);
var newLocation = new ElementRef(hostViewRef, 0);
var component = this._viewManager.getComponent(newLocation);

var dispose = () => {
this._viewManager.destroyInPlaceHostView(parentComponentLocation, hostViewRef);
this._viewManager.destroyFreeHostView(parentComponentLocation, hostViewRef);
};
return new ComponentRef(newLocation, component, dispose);
});
Expand Down
4 changes: 2 additions & 2 deletions modules/angular2/src/core/compiler/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
72 changes: 40 additions & 32 deletions modules/angular2/src/core/compiler/view_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
}
}
25 changes: 12 additions & 13 deletions modules/angular2/src/core/compiler/view_manager_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 5 additions & 6 deletions modules/angular2/src/render/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
}

/**
Expand Down
26 changes: 9 additions & 17 deletions modules/angular2/src/render/dom/dom_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion modules/angular2/src/test_lib/test_bed.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -193,14 +194,44 @@ 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();
});
}));

});

});
}

@Component({
selector: 'imp-ng-cmp'
})
@View({
renderer: 'imp-ng-cmp-renderer',
template: ''
})
class ImperativeViewComponentUsingNgComponent {
Expand All @@ -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;
});
}
}

Expand Down
Loading

0 comments on commit 421d891

Please sign in to comment.