diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index d0307d63cc..13066d82c6 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -150,7 +150,7 @@ function walkTNodeTree( */ while (!nextTNode) { // If parent is null, we're crossing the view boundary, so we should get the host TNode. - tNode = tNode.parent || currentView[TVIEW].node; + tNode = tNode.parent || currentView[T_HOST]; if (tNode === null || tNode === rootTNode) return null; @@ -160,9 +160,26 @@ function walkTNodeTree( beforeNode = currentView[tNode.index][NATIVE]; } - if (tNode.type === TNodeType.View && currentView[NEXT]) { - currentView = currentView[NEXT] as LView; - nextTNode = currentView[TVIEW].node; + if (tNode.type === TNodeType.View) { + /** + * If current lView doesn't have next pointer, we try to find it by going up parents + * chain until: + * - we find an lView with a next pointer + * - or find a tNode with a parent that has a next pointer + * - or reach root TNode (in which case we exit, since we traversed all nodes) + */ + while (!currentView[NEXT] && currentView[PARENT] && + !(tNode.parent && tNode.parent.next)) { + if (tNode === rootTNode) return null; + currentView = currentView[PARENT] as LView; + tNode = currentView[T_HOST] !; + } + if (currentView[NEXT]) { + currentView = currentView[NEXT] as LView; + nextTNode = currentView[T_HOST]; + } else { + nextTNode = tNode.next; + } } else { nextTNode = tNode.next; } diff --git a/packages/core/test/acceptance/content_spec.ts b/packages/core/test/acceptance/content_spec.ts new file mode 100644 index 0000000000..da7853c9d1 --- /dev/null +++ b/packages/core/test/acceptance/content_spec.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; + +describe('projection', () => { + it('should handle projected containers inside other containers', () => { + @Component({ + selector: 'child-comp', // + template: '' + }) + class ChildComp { + } + + @Component({ + selector: 'root-comp', // + template: '' + }) + class RootComp { + } + + @Component({ + selector: 'my-app', + template: ` + + + {{ item }}| + + + ` + }) + class MyApp { + items: number[] = [1, 2, 3]; + } + + TestBed.configureTestingModule({declarations: [ChildComp, RootComp, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + // expecting # of elements to be (items.length - 1), since last element is filtered out by + // *ngIf, this applies to all other assertions below + expect(fixture.nativeElement).toHaveText('1|2|'); + + fixture.componentInstance.items = [4, 5]; + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('4|'); + + fixture.componentInstance.items = [6, 7, 8, 9]; + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('6|7|8|'); + }); +}); \ No newline at end of file diff --git a/packages/core/test/render3/content_spec.ts b/packages/core/test/render3/content_spec.ts index 00d9a0320b..a4d82b307c 100644 --- a/packages/core/test/render3/content_spec.ts +++ b/packages/core/test/render3/content_spec.ts @@ -8,7 +8,7 @@ import {SelectorFlags} from '@angular/core/src/render3/interfaces/projection'; -import {AttributeMarker, defineDirective, detectChanges, directiveInject, loadViewQuery, queryRefresh, reference, templateRefExtractor, viewQuery} from '../../src/render3/index'; +import {AttributeMarker, defineComponent, defineDirective, detectChanges, directiveInject, loadViewQuery, queryRefresh, reference, templateRefExtractor, viewQuery} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, projection, projectionDef, template, text, textBinding, interpolation1} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; @@ -1441,6 +1441,86 @@ describe('content projection', () => { expect(toHtml(parent)).toEqual('content'); }); + it('should handle projected containers inside other containers', () => { + //
Child content
+ const NestedComp = createComponent('nested-comp', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + text(1, 'Child content'); + elementEnd(); + } + }, 2, 0, []); + + // + const RootComp = createComponent('root-comp', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + projectionDef(); + projection(0); + } + }, 1, 0, []); + + // + // + // + // + // + function MyApp_ng_container_1_child_comp_1_Template(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'nested-comp'); + } + } + function MyApp_ng_container_1_Template(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementContainerStart(0); + template(1, MyApp_ng_container_1_child_comp_1_Template, 1, 0, 'nested-comp', [3, 'ngIf']); + elementContainerEnd(); + } + if (rf & RenderFlags.Update) { + const last_r2 = ctx.last; + elementProperty(1, 'ngIf', bind(!last_r2)); + } + } + let myAppInstance: MyApp; + class MyApp { + items = [1, 2]; + + static ngComponentDef = defineComponent({ + type: MyApp, + selectors: [['', 'my-app', '']], + factory: () => myAppInstance = new MyApp(), + consts: 2, + vars: 1, + template: function MyApp_Template(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'root-comp'); + template( + 1, MyApp_ng_container_1_Template, 2, 1, 'ng-container', + ['ngFor', '', 3, 'ngForOf']); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementProperty(1, 'ngForOf', bind(ctx.items)); + } + }, + directives: [NgForOf, NgIf, NestedComp, RootComp] + }); + } + const fixture = new ComponentFixture(MyApp); + fixture.update(); + + // expecting # of divs to be (items.length - 1), since last element is filtered out by *ngIf, + // this applies to all other assertions below + expect(fixture.hostElement.querySelectorAll('div').length).toBe(1); + + myAppInstance !.items = [3, 4, 5]; + fixture.update(); + expect(fixture.hostElement.querySelectorAll('div').length).toBe(2); + + myAppInstance !.items = [6, 7, 8, 9]; + fixture.update(); + expect(fixture.hostElement.querySelectorAll('div').length).toBe(3); + }); + describe('with selectors', () => { it('should project nodes using attribute selectors', () => {