feat(view): allow to transplant a view into a ViewContainer at another place.

Closes #1492.
This commit is contained in:
Tobias Bosch 2015-05-08 17:58:08 -07:00
parent 2185e7cee9
commit 4f3433b5bd
9 changed files with 199 additions and 34 deletions

View File

@ -88,7 +88,7 @@ export class DynamicComponentLoader {
var binding = this._getBinding(typeOrBinding);
return this._compiler.compileInHost(binding).then(hostProtoViewRef => {
var viewContainer = this._viewManager.getViewContainer(location);
var hostViewRef = viewContainer.create(hostProtoViewRef, viewContainer.length, injector);
var hostViewRef = viewContainer.create(hostProtoViewRef, viewContainer.length, null, injector);
var newLocation = new ElementRef(hostViewRef, 0);
var component = this._viewManager.getComponent(newLocation);

View File

@ -40,9 +40,9 @@ export class ViewContainerRef {
// TODO(rado): profile and decide whether bounds checks should be added
// to the methods below.
create(protoViewRef:ProtoViewRef = null, atIndex:number=-1, injector:Injector = null): ViewRef {
create(protoViewRef:ProtoViewRef = null, atIndex:number=-1, context:ElementRef, injector:Injector = null): ViewRef {
if (atIndex == -1) atIndex = this.length;
return this._viewManager.createViewInContainer(this._element, atIndex, protoViewRef, injector);
return this._viewManager.createViewInContainer(this._element, atIndex, protoViewRef, context, injector);
}
insert(viewRef:ViewRef, atIndex:number=-1): ViewRef {
@ -68,4 +68,4 @@ export class ViewContainerRef {
if (atIndex == -1) atIndex = this.length - 1;
return this._viewManager.detachViewInContainer(this._element, atIndex);
}
}
}

View File

@ -92,16 +92,22 @@ export class AppViewManager {
}
createViewInContainer(viewContainerLocation:ElementRef,
atIndex:number, protoViewRef:ProtoViewRef, injector:Injector = null):ViewRef {
atIndex:number, protoViewRef:ProtoViewRef, context:ElementRef = null, injector:Injector = null):ViewRef {
var protoView = internalProtoView(protoViewRef);
var parentView = internalView(viewContainerLocation.parentView);
var boundElementIndex = viewContainerLocation.boundElementIndex;
var contextView = null;
var contextBoundElementIndex = null;
if (isPresent(context)) {
contextView = internalView(context.parentView);
contextBoundElementIndex = context.boundElementIndex;
}
var view = this._createPooledView(protoView);
this._renderer.attachViewInContainer(parentView.render, boundElementIndex, atIndex, view.render);
this._utils.attachViewInContainer(parentView, boundElementIndex, atIndex, view);
this._utils.hydrateViewInContainer(parentView, boundElementIndex, atIndex, injector);
this._utils.attachViewInContainer(parentView, boundElementIndex, contextView, contextBoundElementIndex, atIndex, view);
this._utils.hydrateViewInContainer(parentView, boundElementIndex, contextView, contextBoundElementIndex, atIndex, injector);
this._viewHydrateRecurse(view);
return new ViewRef(view);
}
@ -116,7 +122,13 @@ export class AppViewManager {
var view = internalView(viewRef);
var parentView = internalView(viewContainerLocation.parentView);
var boundElementIndex = viewContainerLocation.boundElementIndex;
this._utils.attachViewInContainer(parentView, boundElementIndex, atIndex, view);
// TODO(tbosch): the public methods attachViewInContainer/detachViewInContainer
// are used for moving elements without the same container.
// We will change this into an atomic `move` operation, which should preserve the
// previous parent injector (see https://github.com/angular/angular/issues/1377).
// Right now we are destroying any special
// context view that might have been used.
this._utils.attachViewInContainer(parentView, boundElementIndex, null, null, atIndex, view);
this._renderer.attachViewInContainer(parentView.render, boundElementIndex, atIndex, view.render);
return viewRef;
}

View File

@ -114,7 +114,12 @@ export class AppViewManagerUtils {
}
attachViewInContainer(parentView:viewModule.AppView, boundElementIndex:number,
contextView:viewModule.AppView, contextBoundElementIndex:number,
atIndex:number, view:viewModule.AppView) {
if (isBlank(contextView)) {
contextView = parentView;
contextBoundElementIndex = boundElementIndex;
}
parentView.changeDetector.addChild(view.changeDetector);
var viewContainer = parentView.viewContainers[boundElementIndex];
if (isBlank(viewContainer)) {
@ -128,7 +133,7 @@ export class AppViewManagerUtils {
} else {
sibling = ListWrapper.last(viewContainer.views[atIndex - 1].rootElementInjectors)
}
var elementInjector = parentView.elementInjectors[boundElementIndex];
var elementInjector = contextView.elementInjectors[contextBoundElementIndex];
for (var i = view.rootElementInjectors.length - 1; i >= 0; i--) {
view.rootElementInjectors[i].linkAfter(elementInjector, sibling);
}
@ -145,11 +150,16 @@ export class AppViewManagerUtils {
}
hydrateViewInContainer(parentView:viewModule.AppView, boundElementIndex:number,
contextView:viewModule.AppView, contextBoundElementIndex:number,
atIndex:number, injector:Injector) {
if (isBlank(contextView)) {
contextView = parentView;
contextBoundElementIndex = boundElementIndex;
}
var viewContainer = parentView.viewContainers[boundElementIndex];
var view = viewContainer.views[atIndex];
var elementInjector = parentView.elementInjectors[boundElementIndex];
this._hydrateView(view, injector, elementInjector, parentView.context, parentView.locals);
var elementInjector = contextView.elementInjectors[contextBoundElementIndex].getHost();
this._hydrateView(view, injector, elementInjector, contextView.context, contextView.locals);
}
hydrateDynamicComponentInElementInjector(hostView:viewModule.AppView, boundElementIndex:number,

View File

@ -37,7 +37,7 @@ export class RouterOutlet {
]);
this._viewContainer.clear();
this._viewContainer.create(pv, 0, outletInjector);
this._viewContainer.create(pv, 0, null, outletInjector);
});
}

View File

@ -27,11 +27,13 @@ import {PipeRegistry, defaultPipeRegistry,
import {Directive, Component} from 'angular2/src/core/annotations_impl/annotations';
import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader';
import {QueryList} from 'angular2/src/core/compiler/query_list';
import {View} from 'angular2/src/core/annotations_impl/view';
import {Parent, Ancestor} from 'angular2/src/core/annotations_impl/visibility';
import {Attribute} from 'angular2/src/core/annotations_impl/di';
import {Attribute, Query} from 'angular2/src/core/annotations_impl/di';
import {If} from 'angular2/src/directives/if';
import {For} from 'angular2/src/directives/for';
import {ViewContainerRef} from 'angular2/src/core/compiler/view_container_ref';
import {ProtoViewRef} from 'angular2/src/core/compiler/view_ref';
@ -341,6 +343,21 @@ export function main() {
});
}));
it('should allow to transplant embedded ProtoViews into other ViewContainers', inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({
template: '<some-directive><toolbar><template toolbarpart var-toolbar-prop="toolbarProp">{{ctxProp}},{{toolbarProp}},<cmp-with-parent></cmp-with-parent></template></toolbar></some-directive>',
directives: [SomeDirective, CompWithParent, ToolbarComponent, ToolbarPart]
}));
ctx.ctxProp = 'From myComp';
tb.createView(MyComp, {context: ctx}).then((view) => {
view.detectChanges();
expect(view.rootNodes).toHaveText('TOOLBAR(From myComp,From toolbar,Component with an injected parent)');
async.done();
});
}));
it('should assign the component instance to a var-', inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({
template: '<p><child-cmp var-alice></child-cmp></p>',
@ -951,7 +968,7 @@ class DynamicViewport {
var myService = new MyService();
myService.greeting = 'dynamic greet';
this.done = compiler.compileInHost(ChildCompUsingService).then( (hostPv) => {
vc.create(hostPv, 0, inj.createChildFromResolved(Injector.resolve([bind(MyService).toValue(myService)])))
vc.create(hostPv, 0, null, inj.createChildFromResolved(Injector.resolve([bind(MyService).toValue(myService)])))
});
}
}
@ -1411,3 +1428,50 @@ class ChildComponent {
this.appDependency = a;
}
}
@Directive({
selector: '[toolbar-vc]',
properties: {
'toolbarVc': 'toolbarVc'
}
})
class ToolbarViewContainer {
vc:ViewContainerRef;
constructor(vc:ViewContainerRef) {
this.vc = vc;
}
set toolbarVc(part:ToolbarPart) {
var view = this.vc.create(part.protoViewRef, 0, part.elementRef);
view.setLocal('toolbarProp', 'From toolbar');
}
}
@Directive({
selector: '[toolbarpart]'
})
class ToolbarPart {
protoViewRef:ProtoViewRef;
elementRef:ElementRef;
constructor(protoViewRef:ProtoViewRef, elementRef:ElementRef) {
this.elementRef = elementRef;
this.protoViewRef = protoViewRef;
}
}
@Component({
selector: 'toolbar'
})
@View({
template: 'TOOLBAR(<div *for="var part of query" [toolbar-vc]="part"></div>)',
directives: [ToolbarViewContainer, For]
})
class ToolbarComponent {
query:QueryList;
ctxProp:string;
constructor(@Query(ToolbarPart) query: QueryList) {
this.ctxProp = 'hello world';
this.query = query;
}
}

View File

@ -55,16 +55,20 @@ export function main() {
location = new ElementRef(wrapView(view), 0);
});
it('should return a 0 length if there is no underlying ViewContainerRef', () => {
var vc = createViewContainer();
expect(vc.length).toBe(0);
});
describe('length', () => {
it('should return a 0 length if there is no underlying ViewContainerRef', () => {
var vc = createViewContainer();
expect(vc.length).toBe(0);
});
it('should return the size of the underlying ViewContainerRef', () => {
var vc = createViewContainer();
view.viewContainers = [new AppViewContainer()];
view.viewContainers[0].views = [createView()];
expect(vc.length).toBe(1);
});
it('should return the size of the underlying ViewContainerRef', () => {
var vc = createViewContainer();
view.viewContainers = [new AppViewContainer()];
view.viewContainers[0].views = [createView()];
expect(vc.length).toBe(1);
});
// TODO: add missing tests here!

View File

@ -133,7 +133,7 @@ export function main() {
utils.spy('attachComponentView').andCallFake( (hostView, elementIndex, childView) => {
hostView.componentChildViews[elementIndex] = childView;
});
utils.spy('attachViewInContainer').andCallFake( (parentView, elementIndex, atIndex, childView) => {
utils.spy('attachViewInContainer').andCallFake( (parentView, elementIndex, _a, _b, atIndex, childView) => {
var viewContainer = parentView.viewContainers[elementIndex];
if (isBlank(viewContainer)) {
viewContainer = new AppViewContainer();
@ -411,26 +411,30 @@ export function main() {
});
it('should attach the view', () => {
manager.createViewInContainer(elementRef(wrapView(parentView), 0), 0, wrapPv(childProtoView), null)
expect(utils.spy('attachViewInContainer')).toHaveBeenCalledWith(parentView, 0, 0, createdViews[0]);
var contextView = createView();
manager.createViewInContainer(elementRef(wrapView(parentView), 0), 0, wrapPv(childProtoView),
elementRef(wrapView(contextView), 1), null);
expect(utils.spy('attachViewInContainer')).toHaveBeenCalledWith(parentView, 0, contextView, 1, 0, createdViews[0]);
expect(renderer.spy('attachViewInContainer')).toHaveBeenCalledWith(parentView.render, 0, 0, createdViews[0].render);
});
it('should hydrate the view', () => {
var injector = new Injector([], null, false);
manager.createViewInContainer(elementRef(wrapView(parentView), 0), 0, wrapPv(childProtoView), injector);
expect(utils.spy('hydrateViewInContainer')).toHaveBeenCalledWith(parentView, 0, 0, injector);
var contextView = createView();
manager.createViewInContainer(elementRef(wrapView(parentView), 0), 0, wrapPv(childProtoView),
elementRef(wrapView(contextView), 1), injector);
expect(utils.spy('hydrateViewInContainer')).toHaveBeenCalledWith(parentView, 0, contextView, 1, 0, injector);
expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(createdViews[0].render);
});
it('should create and set the render view', () => {
manager.createViewInContainer(elementRef(wrapView(parentView), 0), 0, wrapPv(childProtoView), null);
manager.createViewInContainer(elementRef(wrapView(parentView), 0), 0, wrapPv(childProtoView), null, null);
expect(renderer.spy('createView')).toHaveBeenCalledWith(childProtoView.render);
expect(createdViews[0].render).toBe(createdRenderViews[0]);
});
it('should set the event dispatcher', () => {
manager.createViewInContainer(elementRef(wrapView(parentView), 0), 0, wrapPv(childProtoView), null);
manager.createViewInContainer(elementRef(wrapView(parentView), 0), 0, wrapPv(childProtoView), null, null);
var childView = createdViews[0];
expect(renderer.spy('setEventDispatcher')).toHaveBeenCalledWith(childView.render, childView);
});

View File

@ -23,7 +23,7 @@ import {MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/col
import {AppProtoView, AppView} from 'angular2/src/core/compiler/view';
import {ChangeDetector} from 'angular2/change_detection';
import {ElementBinder} from 'angular2/src/core/compiler/element_binder';
import {DirectiveBinding, ElementInjector, ElementRef} from 'angular2/src/core/compiler/element_injector';
import {DirectiveBinding, ElementInjector, PreBuiltObjects} from 'angular2/src/core/compiler/element_injector';
import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader';
import {Component} from 'angular2/src/core/annotations_impl/annotations';
import {AppViewManagerUtils} from 'angular2/src/core/compiler/view_manager_utils';
@ -66,12 +66,14 @@ export function main() {
}
function createElementInjector() {
var host = new SpyElementInjector();
return SpyObject.stub(new SpyElementInjector(), {
'isExportingComponent' : false,
'isExportingElement' : false,
'getEventEmitterAccessors' : [],
'getComponent' : null,
'getDynamicallyLoadedComponent': null
'getDynamicallyLoadedComponent': null,
'getHost': host
}, {});
}
@ -81,13 +83,15 @@ export function main() {
}
var view = new AppView(null, pv, MapWrapper.create());
var elementInjectors = ListWrapper.createFixedSize(pv.elementBinders.length);
var preBuiltObjects = ListWrapper.createFixedSize(pv.elementBinders.length);
for (var i=0; i<pv.elementBinders.length; i++) {
elementInjectors[i] = createElementInjector();
preBuiltObjects[i] = new SpyPreBuiltObjects();
}
view.init(new SpyChangeDetector(),
elementInjectors,
[],
ListWrapper.createFixedSize(pv.elementBinders.length),
elementInjectors,
preBuiltObjects,
ListWrapper.createFixedSize(pv.elementBinders.length)
);
return view;
@ -170,6 +174,66 @@ export function main() {
});
describe('attachViewInContainer', () => {
var parentView, contextView, childView;
function createViews() {
var parentPv = createProtoView([
createEmptyElBinder()
]);
parentView = createView(parentPv);
var contextPv = createProtoView([
createEmptyElBinder()
]);
contextView = createView(contextPv);
var childPv = createProtoView([
createEmptyElBinder()
]);
childView = createView(childPv);
}
it('should link the views rootElementInjectors after the elementInjector at the given context', () => {
createViews();
utils.attachViewInContainer(parentView, 0, contextView, 0, 0, childView);
expect(childView.rootElementInjectors[0].spy('linkAfter'))
.toHaveBeenCalledWith(contextView.elementInjectors[0], null);
});
});
describe('hydrateViewInContainer', () => {
var parentView, contextView, childView;
function createViews() {
var parentPv = createProtoView([
createEmptyElBinder()
]);
parentView = createView(parentPv);
var contextPv = createProtoView([
createEmptyElBinder()
]);
contextView = createView(contextPv);
var childPv = createProtoView([
createEmptyElBinder()
]);
childView = createView(childPv);
utils.attachViewInContainer(parentView, 0, contextView, 0, 0, childView);
}
it("should instantiate the elementInjectors with the host of the context's elementInjector", () => {
createViews();
utils.hydrateViewInContainer(parentView, 0, contextView, 0, 0, null);
expect(childView.rootElementInjectors[0].spy('instantiateDirectives'))
.toHaveBeenCalledWith(null, contextView.elementInjectors[0].getHost(), childView.preBuiltObjects[0]);
});
});
});
}
@ -190,3 +254,10 @@ class SpyChangeDetector extends SpyObject {
constructor(){super(ChangeDetector);}
noSuchMethod(m){return super.noSuchMethod(m)}
}
@proxy
@IMPLEMENTS(PreBuiltObjects)
class SpyPreBuiltObjects extends SpyObject {
constructor(){super(PreBuiltObjects);}
noSuchMethod(m){return super.noSuchMethod(m)}
}