From f2bbef3e33b2fc45abee71b6be855a654f5b5ec7 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Thu, 3 Nov 2016 15:32:44 -0700 Subject: [PATCH] fix(core): allow to query content of templates that are stamped out at a different place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, if a `TemplateRef` was created in a `ViewContainerRef` at a different place, the content was not query able at all. With this change, the content of the template can be queried as if it was stamped out at the declaration place of the template. E.g. in the following example, the `QueryList` will be filled once the button is clicked. ``` @Component({ selector: ‘my-comp’, template: ‘’ }) class MyComp { @ContentChildren(ChildCmp) children: QueryList; @ContentChildren(TemplateRef) template: TemplateRef; @ViewChild(‘vc’, {read: ViewContainerRef}) vc: ViewContainerRef; createView() { this.vc.createEmbeddedView(this.template); } } @Component({ template: ` ` }) class App {} ``` Closes #12283 Closes #12094 --- .../src/view_compiler/compile_element.ts | 2 +- .../src/view_compiler/view_builder.ts | 14 ++++- modules/@angular/core/src/linker/view.ts | 62 +++++++++++++------ .../core/src/linker/view_container.ts | 48 ++++++-------- .../test/linker/query_integration_spec.ts | 37 ++++++++++- 5 files changed, 110 insertions(+), 53 deletions(-) diff --git a/modules/@angular/compiler/src/view_compiler/compile_element.ts b/modules/@angular/compiler/src/view_compiler/compile_element.ts index 8ef696b84a..5fcb028dc3 100644 --- a/modules/@angular/compiler/src/view_compiler/compile_element.ts +++ b/modules/@angular/compiler/src/view_compiler/compile_element.ts @@ -73,7 +73,7 @@ export class CompileElement extends CompileNode { o.THIS_EXPR.callMethod('injector', [o.literal(this.nodeIndex)])); this.instances.set( resolveIdentifierToken(Identifiers.Renderer).reference, o.THIS_EXPR.prop('renderer')); - if (this.hasViewContainer) { + if (this.hasViewContainer || this.hasEmbeddedView) { this._createViewContainer(); } if (this.component) { diff --git a/modules/@angular/compiler/src/view_compiler/view_builder.ts b/modules/@angular/compiler/src/view_compiler/view_builder.ts index 204b125639..104bf1f2c3 100644 --- a/modules/@angular/compiler/src/view_compiler/view_builder.ts +++ b/modules/@angular/compiler/src/view_compiler/view_builder.ts @@ -459,6 +459,11 @@ function createViewClass( if (view.genConfig.genDebugInfo) { superConstructorArgs.push(nodeDebugInfosVar); } + if (view.viewType === ViewType.EMBEDDED) { + viewConstructorArgs.push(new o.FnParam( + 'declaredViewContainer', o.importType(resolveIdentifier(Identifiers.ViewContainer)))); + superConstructorArgs.push(o.variable('declaredViewContainer')); + } var viewMethods = [ new o.ClassMethod( 'createInternal', [new o.FnParam(rootSelectorVar.name, o.STRING_TYPE)], @@ -676,7 +681,7 @@ function generateVisitNodesStmts( return stmts; } -function generateCreateEmbeddedViewsMethod(view: CompileView) { +function generateCreateEmbeddedViewsMethod(view: CompileView): o.ClassMethod { const nodeIndexVar = o.variable('nodeIndex'); const stmts: o.Statement[] = []; view.nodes.forEach((node) => { @@ -686,12 +691,15 @@ function generateCreateEmbeddedViewsMethod(view: CompileView) { stmts.push(new o.IfStmt( nodeIndexVar.equals(o.literal(node.nodeIndex)), [new o.ReturnStatement(node.embeddedView.classExpr.instantiate([ - ViewProperties.viewUtils, o.THIS_EXPR, o.literal(node.nodeIndex), node.renderNode + ViewProperties.viewUtils, o.THIS_EXPR, o.literal(node.nodeIndex), node.renderNode, + node.viewContainer ]))])); } } }); - stmts.push(new o.ReturnStatement(o.NULL_EXPR)); + if (stmts.length > 0) { + stmts.push(new o.ReturnStatement(o.NULL_EXPR)); + } return new o.ClassMethod( 'createEmbeddedViewInternal', [new o.FnParam(nodeIndexVar.name, o.NUMBER_TYPE)], stmts, o.importType(resolveIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])); diff --git a/modules/@angular/core/src/linker/view.ts b/modules/@angular/core/src/linker/view.ts index 995af6a704..b5306aaaad 100644 --- a/modules/@angular/core/src/linker/view.ts +++ b/modules/@angular/core/src/linker/view.ts @@ -41,7 +41,7 @@ export abstract class AppView { lastRootNode: any; allNodes: any[]; disposables: Function[]; - viewContainerElement: ViewContainer = null; + viewContainer: ViewContainer = null; numberOfChecks: number = 0; @@ -58,7 +58,8 @@ export abstract class AppView { constructor( public clazz: any, public componentType: RenderComponentType, public type: ViewType, public viewUtils: ViewUtils, public parentView: AppView, public parentIndex: number, - public parentElement: any, public cdMode: ChangeDetectorStatus) { + public parentElement: any, public cdMode: ChangeDetectorStatus, + public declaredViewContainer: ViewContainer = null) { this.ref = new ViewRef_(this); if (type === ViewType.COMPONENT || type === ViewType.HOST) { this.renderer = viewUtils.renderComponent(componentType); @@ -139,8 +140,8 @@ export abstract class AppView { detachAndDestroy() { if (this._hasExternalHostElement) { this.detach(); - } else if (isPresent(this.viewContainerElement)) { - this.viewContainerElement.detachView(this.viewContainerElement.nestedViews.indexOf(this)); + } else if (isPresent(this.viewContainer)) { + this.viewContainer.detachView(this.viewContainer.nestedViews.indexOf(this)); } this.destroy(); } @@ -185,6 +186,18 @@ export abstract class AppView { } else { this._renderDetach(); } + if (this.declaredViewContainer && this.declaredViewContainer !== this.viewContainer) { + const projectedViews = this.declaredViewContainer.projectedViews; + const index = projectedViews.indexOf(this); + // perf: pop is faster than splice! + if (index >= projectedViews.length - 1) { + projectedViews.pop(); + } else { + projectedViews.splice(index, 1); + } + } + this.viewContainer = null; + this.dirtyParentQueriesInternal(); } private _renderDetach() { @@ -195,7 +208,25 @@ export abstract class AppView { } } - attachAfter(prevNode: any) { + attachAfter(viewContainer: ViewContainer, prevView: AppView) { + this._renderAttach(viewContainer, prevView); + this.viewContainer = viewContainer; + if (this.declaredViewContainer && this.declaredViewContainer !== viewContainer) { + if (!this.declaredViewContainer.projectedViews) { + this.declaredViewContainer.projectedViews = []; + } + this.declaredViewContainer.projectedViews.push(this); + } + this.dirtyParentQueriesInternal(); + } + + moveAfter(viewContainer: ViewContainer, prevView: AppView) { + this._renderAttach(viewContainer, prevView); + this.dirtyParentQueriesInternal(); + } + + private _renderAttach(viewContainer: ViewContainer, prevView: AppView) { + const prevNode = prevView ? prevView.lastRootNode : viewContainer.nativeElement; if (this._directRenderer) { const nextSibling = this._directRenderer.nextSibling(prevNode); if (nextSibling) { @@ -282,18 +313,6 @@ export abstract class AppView { */ detectChangesInternal(throwOnChange: boolean): void {} - markContentChildAsMoved(viewContainer: ViewContainer): void { this.dirtyParentQueriesInternal(); } - - addToContentChildren(viewContainer: ViewContainer): void { - this.viewContainerElement = viewContainer; - this.dirtyParentQueriesInternal(); - } - - removeFromContentChildren(viewContainer: ViewContainer): void { - this.dirtyParentQueriesInternal(); - this.viewContainerElement = null; - } - markAsCheckOnce(): void { this.cdMode = ChangeDetectorStatus.CheckOnce; } markPathToRootAsCheckOnce(): void { @@ -305,7 +324,7 @@ export abstract class AppView { if (c.type === ViewType.COMPONENT) { c = c.parentView; } else { - c = c.viewContainerElement ? c.viewContainerElement.parentView : null; + c = c.viewContainer ? c.viewContainer.parentView : null; } } } @@ -323,8 +342,11 @@ export class DebugAppView extends AppView { constructor( clazz: any, componentType: RenderComponentType, type: ViewType, viewUtils: ViewUtils, parentView: AppView, parentIndex: number, parentNode: any, cdMode: ChangeDetectorStatus, - public staticNodeDebugInfos: StaticNodeDebugInfo[]) { - super(clazz, componentType, type, viewUtils, parentView, parentIndex, parentNode, cdMode); + public staticNodeDebugInfos: StaticNodeDebugInfo[], + declaredViewContainer: ViewContainer = null) { + super( + clazz, componentType, type, viewUtils, parentView, parentIndex, parentNode, cdMode, + declaredViewContainer); } create(context: T) { diff --git a/modules/@angular/core/src/linker/view_container.ts b/modules/@angular/core/src/linker/view_container.ts index 485dbb28ff..c1812ae8fc 100644 --- a/modules/@angular/core/src/linker/view_container.ts +++ b/modules/@angular/core/src/linker/view_container.ts @@ -22,6 +22,9 @@ import {ViewType} from './view_type'; */ export class ViewContainer { public nestedViews: AppView[]; + // views that have been declared at the place of this view container, + // but inserted into another view container + public projectedViews: AppView[]; constructor( public index: number, public parentIndex: number, public parentView: AppView, @@ -59,13 +62,22 @@ export class ViewContainer { } mapNestedViews(nestedViewClass: any, callback: Function): any[] { - var result: any[] /** TODO #9100 */ = []; - if (isPresent(this.nestedViews)) { - this.nestedViews.forEach((nestedView) => { + var result: any[] = []; + if (this.nestedViews) { + for (var i = 0; i < this.nestedViews.length; i++) { + const nestedView = this.nestedViews[i]; if (nestedView.clazz === nestedViewClass) { result.push(callback(nestedView)); } - }); + } + } + if (this.projectedViews) { + for (var i = 0; i < this.projectedViews.length; i++) { + const projectedView = this.projectedViews[i]; + if (projectedView.clazz === nestedViewClass) { + result.push(callback(projectedView)); + } + } } return result; } @@ -82,17 +94,8 @@ export class ViewContainer { } nestedViews.splice(previousIndex, 1); nestedViews.splice(currentIndex, 0, view); - var refRenderNode: any /** TODO #9100 */; - if (currentIndex > 0) { - var prevView = nestedViews[currentIndex - 1]; - refRenderNode = prevView.lastRootNode; - } else { - refRenderNode = this.nativeElement; - } - if (isPresent(refRenderNode)) { - view.attachAfter(refRenderNode); - } - view.markContentChildAsMoved(this); + const prevView = currentIndex > 0 ? nestedViews[currentIndex - 1] : null; + view.moveAfter(this, prevView); } attachView(view: AppView, viewIndex: number) { @@ -110,17 +113,8 @@ export class ViewContainer { } else { nestedViews.splice(viewIndex, 0, view); } - var refRenderNode: any /** TODO #9100 */; - if (viewIndex > 0) { - var prevView = nestedViews[viewIndex - 1]; - refRenderNode = prevView.lastRootNode; - } else { - refRenderNode = this.nativeElement; - } - if (isPresent(refRenderNode)) { - view.attachAfter(refRenderNode); - } - view.addToContentChildren(this); + const prevView = viewIndex > 0 ? nestedViews[viewIndex - 1] : null; + view.attachAfter(this, prevView); } detachView(viewIndex: number): AppView { @@ -135,8 +129,6 @@ export class ViewContainer { throw new Error(`Component views can't be moved!`); } view.detach(); - - view.removeFromContentChildren(this); return view; } } diff --git a/modules/@angular/core/test/linker/query_integration_spec.ts b/modules/@angular/core/test/linker/query_integration_spec.ts index ef0a1ed505..83f60ede1f 100644 --- a/modules/@angular/core/test/linker/query_integration_spec.ts +++ b/modules/@angular/core/test/linker/query_integration_spec.ts @@ -43,7 +43,8 @@ export function main() { NeedsContentChildWithRead, NeedsViewChildrenWithRead, NeedsViewChildWithRead, - NeedsViewContainerWithRead + NeedsViewContainerWithRead, + ManualProjecting ] })); @@ -505,6 +506,25 @@ export function main() { expect(q.query4).toBeDefined(); }); }); + + describe('query over moved templates', () => { + it('should include manually projected templates in queries', () => { + const template = + ''; + const view = createTestCmpAndDetectChanges(MyComp0, template); + const q = view.debugElement.children[0].references['q']; + expect(q.query.length).toBe(0); + + q.create(); + view.detectChanges(); + expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1']); + + q.destroy(); + view.detectChanges(); + expect(q.query.length).toBe(0); + }); + + }); }); } @@ -751,3 +771,18 @@ class MyComp0 { @Component({selector: 'my-comp', template: ''}) class MyCompBroken0 { } + +@Component({selector: 'manual-projecting', template: '
'}) +class ManualProjecting { + @ContentChild(TemplateRef) template: TemplateRef; + + @ViewChild('vc', {read: ViewContainerRef}) + vc: ViewContainerRef; + + @ContentChildren(TextDirective) + query: QueryList; + + create() { this.vc.createEmbeddedView(this.template); } + + destroy() { this.vc.clear(); } +}