From bb3f0e5ed217a7a3a427777dbdd3afaf1268691f Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Mon, 9 Apr 2018 17:35:50 +0200 Subject: [PATCH] feat(ivy): support projection of ViewContainerRef (#23272) PR Close #23272 --- .../core/src/render3/node_manipulation.ts | 3 + .../test/render3/view_container_ref_spec.ts | 222 ++++++++++++++++-- 2 files changed, 206 insertions(+), 19 deletions(-) diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index f42816ce94..5850a4fb5b 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -490,4 +490,7 @@ export function appendProjectedNode( addRemoveViewFromContainer(node as LContainerNode, views[i], true, null); } } + if (node.dynamicLContainerNode) { + node.dynamicLContainerNode.data.renderParent = currentParent as LElementNode; + } } diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index 2e2693eed3..8dfb671fa7 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -6,19 +6,32 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, TemplateRef, ViewContainerRef} from '../../src/core'; +import {Component, Directive, TemplateRef, ViewContainerRef} from '../../src/core'; import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di'; import {defineComponent, defineDirective, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; -import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, text, textBinding} from '../../src/render3/instructions'; +import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, projection, projectionDef, text, textBinding} from '../../src/render3/instructions'; import {ComponentFixture, TemplateFixture} from './render_util'; describe('ViewContainerRef', () => { + let directiveInstance: DirectiveWithVCRef|null; + + beforeEach(() => { directiveInstance = null; }); + + class DirectiveWithVCRef { + static ngDirectiveDef = defineDirective({ + type: DirectiveWithVCRef, + selectors: [['', 'vcref', '']], + factory: () => directiveInstance = new DirectiveWithVCRef(injectViewContainerRef()), + inputs: {tplRef: 'tplRef'} + }); + + tplRef: TemplateRef<{}>; + + constructor(public vcref: ViewContainerRef) {} + } + describe('API', () => { - let directiveInstance: DirectiveWithVCRef|null; - - beforeEach(() => { directiveInstance = null; }); - function embeddedTemplate(ctx: any, cm: boolean) { if (cm) { text(0); @@ -26,19 +39,6 @@ describe('ViewContainerRef', () => { textBinding(0, ctx.name); } - class DirectiveWithVCRef { - static ngDirectiveDef = defineDirective({ - type: DirectiveWithVCRef, - selectors: [['', 'vcref', '']], - factory: () => directiveInstance = new DirectiveWithVCRef(injectViewContainerRef()), - inputs: {tplRef: 'tplRef'} - }); - - tplRef: TemplateRef<{}>; - - constructor(public vcref: ViewContainerRef) {} - } - function createView(s: string, index?: number) { directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, {name: s}, index); } @@ -452,4 +452,188 @@ describe('ViewContainerRef', () => { }); }); }); + + describe('projection', () => { + function embeddedTemplate(ctx: any, cm: boolean) { + if (cm) { + elementStart(0, 'span'); + text(1); + elementEnd(); + } + textBinding(1, ctx.name); + } + + it('should project the ViewContainerRef content along its host', () => { + @Component({selector: 'child', template: '
'}) + class Child { + static ngComponentDef = defineComponent({ + type: Child, + selectors: [['child']], + factory: () => new Child(), + template: (cmp: Child, cm: boolean) => { + if (cm) { + projectionDef(0); + elementStart(1, 'div'); + { projection(2, 0); } + elementEnd(); + } + } + }); + } + + @Component({ + selector: 'parent', + template: ` + + {{name}} + +
blah
` + }) + class Parent { + name: string = 'bar'; + static ngComponentDef = defineComponent({ + type: Parent, + selectors: [['parent']], + factory: () => new Parent(), + template: (cmp: Parent, cm: boolean) => { + if (cm) { + container(0, embeddedTemplate); + elementStart(1, 'child'); + elementStart(2, 'header', ['vcref', '']); + text(3, 'blah'); + elementEnd(); + elementEnd(); + } + const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0))); + elementProperty(2, 'tplRef', bind(tplRef)); + elementProperty(2, 'name', bind(cmp.name)); + }, + directives: [Child, DirectiveWithVCRef] + }); + } + + const fixture = new ComponentFixture(Parent); + expect(fixture.html).toEqual('
blah
'); + + directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); + fixture.update(); + expect(fixture.html) + .toEqual('
blah
bar
'); + }); + + @Component({ + selector: 'child-with-selector', + template: ` + + ` + }) + class ChildWithSelector { + static ngComponentDef = defineComponent({ + type: ChildWithSelector, + selectors: [['child-with-selector']], + factory: () => new ChildWithSelector(), + template: (cmp: ChildWithSelector, cm: boolean) => { + if (cm) { + projectionDef(0, [[['header']]], ['header']); + elementStart(1, 'first'); + { projection(2, 0, 1); } + elementEnd(); + elementStart(3, 'second'); + { projection(4, 0); } + elementEnd(); + } + } + }); + } + + it('should project the ViewContainerRef content along its host, when the host matches a selector', + () => { + @Component({ + selector: 'parent', + template: ` + + {{name}} + +
blah
` + }) + class Parent { + name: string = 'bar'; + static ngComponentDef = defineComponent({ + type: Parent, + selectors: [['parent']], + factory: () => new Parent(), + template: (cmp: Parent, cm: boolean) => { + if (cm) { + container(0, embeddedTemplate); + elementStart(1, 'child-with-selector'); + elementStart(2, 'header', ['vcref', '']); + text(3, 'blah'); + elementEnd(); + elementEnd(); + } + const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0))); + elementProperty(2, 'tplRef', bind(tplRef)); + elementProperty(2, 'name', bind(cmp.name)); + }, + directives: [ChildWithSelector, DirectiveWithVCRef] + }); + } + + const fixture = new ComponentFixture(Parent); + expect(fixture.html) + .toEqual( + '
blah
'); + + directiveInstance !.vcref.createEmbeddedView( + directiveInstance !.tplRef, fixture.component); + fixture.update(); + expect(fixture.html) + .toEqual( + '
blah
bar
'); + }); + + it('should not project the ViewContainerRef content, when the host does not match a selector', () => { + @Component({ + selector: 'parent', + template: ` + + {{name}} + +
blah
` + }) + class Parent { + name: string = 'bar'; + static ngComponentDef = defineComponent({ + type: Parent, + selectors: [['parent']], + factory: () => new Parent(), + template: (cmp: Parent, cm: boolean) => { + if (cm) { + container(0, embeddedTemplate); + elementStart(1, 'child-with-selector'); + elementStart(2, 'footer', ['vcref', '']); + text(3, 'blah'); + elementEnd(); + elementEnd(); + } + const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0))); + elementProperty(2, 'tplRef', bind(tplRef)); + elementProperty(2, 'name', bind(cmp.name)); + }, + directives: [ChildWithSelector, DirectiveWithVCRef] + }); + } + + const fixture = new ComponentFixture(Parent); + expect(fixture.html) + .toEqual( + '
blah
'); + + directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); + fixture.update(); + expect(fixture.html) + .toEqual( + '
blah
bar
'); + }); + }); });