From dc1f1295ee8dc1cab7e823183dd967f941ef6a7a Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Tue, 3 Jul 2018 10:03:48 -0700 Subject: [PATCH] fix(ivy): support projecting into dynamic views (#24752) PR Close #24752 --- packages/core/src/render3/instructions.ts | 13 +- packages/core/test/render3/content_spec.ts | 165 +++++++++++++++++++++ 2 files changed, 171 insertions(+), 7 deletions(-) diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 185e651f32..3725b08f7b 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -1963,12 +1963,9 @@ export function projectionDef( // execute selector matching logic if and only if: // - there are selectors defined // - a node has a tag name / attributes that can be matched - if (selectors) { - const matchedIdx = matchingSelectorIndex(componentChild.tNode, selectors, textSelectors !); - distributedNodes[matchedIdx].push(componentChild); - } else { - distributedNodes[0].push(componentChild); - } + const bucketIndex = + selectors ? matchingSelectorIndex(componentChild.tNode, selectors, textSelectors !) : 0; + distributedNodes[bucketIndex].push(componentChild); componentChild = getNextLNode(componentChild); } @@ -2031,8 +2028,10 @@ export function projection( const currentParent = getParentLNode(node); const canInsert = canInsertNativeNode(currentParent, viewData); + let grandparent: LContainerNode; const renderParent = currentParent.tNode.type === TNodeType.View ? - (getParentLNode(currentParent) as LContainerNode).data[RENDER_PARENT] ! : + (grandparent = getParentLNode(currentParent) as LContainerNode) && + grandparent.data[RENDER_PARENT] ! : currentParent as LElementNode; for (let i = 0; i < nodesForSelector.length; i++) { diff --git a/packages/core/test/render3/content_spec.ts b/packages/core/test/render3/content_spec.ts index 8f16a1fa00..2944e11d85 100644 --- a/packages/core/test/render3/content_spec.ts +++ b/packages/core/test/render3/content_spec.ts @@ -706,6 +706,171 @@ describe('content projection', () => { expect(toHtml(parent)).toEqual('
'); }); + it('should project into dynamic views (with createEmbeddedView)', () => { + class NgIf { + constructor(public vcr: ViewContainerRef, public template: TemplateRef) {} + + @Input() + set ngIf(value: boolean) { + value ? this.vcr.createEmbeddedView(this.template) : this.vcr.clear(); + } + + static ngDirectiveDef = defineDirective({ + type: NgIf, + selectors: [['', 'ngIf', '']], + inputs: {'ngIf': 'ngIf'}, + factory: () => new NgIf(injectViewContainerRef(), injectTemplateRef()) + }); + } + + /** + * Before- + * + * + * + * -After + */ + const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + projectionDef(0); + text(1, 'Before-'); + container(2, IfTemplate, '', [AttributeMarker.SelectOnly, 'ngIf']); + text(3, '-After'); + } + if (rf & RenderFlags.Update) { + elementProperty(2, 'ngIf', bind(ctx.showing)); + } + + function IfTemplate(rf1: RenderFlags, ctx1: any) { + if (rf1 & RenderFlags.Create) { + projectionDef(0); + projection(1, 0); + } + } + }, [NgIf]); + + let child: {showing: boolean}; + /** + * + *
A
+ * Some text + *
+ */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'child'); + { + elementStart(1, 'div'); + { text(2, 'A'); } + elementEnd(); + text(3, 'Some text'); + } + elementEnd(); + + // testing + child = loadDirective(0); + } + }, [Child]); + + const fixture = new ComponentFixture(App); + child !.showing = true; + fixture.update(); + expect(fixture.html).toEqual('Before-
A
Some text-After
'); + + child !.showing = false; + fixture.update(); + expect(fixture.html).toEqual('Before--After'); + + child !.showing = true; + fixture.update(); + expect(fixture.html).toEqual('Before-
A
Some text-After
'); + }); + + it('should project into dynamic views (with insertion)', () => { + class NgIf { + constructor(public vcr: ViewContainerRef, public template: TemplateRef) {} + + @Input() + set ngIf(value: boolean) { + if (value) { + const viewRef = this.template.createEmbeddedView({}); + this.vcr.insert(viewRef); + } else { + this.vcr.clear(); + } + } + + static ngDirectiveDef = defineDirective({ + type: NgIf, + selectors: [['', 'ngIf', '']], + inputs: {'ngIf': 'ngIf'}, + factory: () => new NgIf(injectViewContainerRef(), injectTemplateRef()) + }); + } + + /** + * Before- + * + * + * + * -After + */ + const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + projectionDef(0); + text(1, 'Before-'); + container(2, IfTemplate, '', [AttributeMarker.SelectOnly, 'ngIf']); + text(3, '-After'); + } + if (rf & RenderFlags.Update) { + elementProperty(2, 'ngIf', bind(ctx.showing)); + } + + function IfTemplate(rf1: RenderFlags, ctx1: any) { + if (rf1 & RenderFlags.Create) { + projectionDef(0); + projection(1, 0); + } + } + }, [NgIf]); + + let child: {showing: boolean}; + /** + * + *
A
+ * Some text + *
+ */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'child'); + { + elementStart(1, 'div'); + { text(2, 'A'); } + elementEnd(); + text(3, 'Some text'); + } + elementEnd(); + + // testing + child = loadDirective(0); + } + }, [Child]); + + const fixture = new ComponentFixture(App); + child !.showing = true; + fixture.update(); + expect(fixture.html).toEqual('Before-
A
Some text-After
'); + + child !.showing = false; + fixture.update(); + expect(fixture.html).toEqual('Before--After'); + + child !.showing = true; + fixture.update(); + expect(fixture.html).toEqual('Before-
A
Some text-After
'); + }); + it('should project nodes into the last ng-content', () => { /** *