diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index f0edc25f51..4ca1f7181e 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -1092,156 +1092,225 @@ describe('compiler compliance', () => { }); }); - it('should support content projection in root template', () => { - const files = { - app: { - 'spec.ts': ` - import {Component, Directive, NgModule, TemplateRef} from '@angular/core'; + describe('content projection', () => { - @Component({selector: 'simple', template: '
'}) - export class SimpleComponent {} - - @Component({ - selector: 'complex', - template: \` -
-
\` + it('should support content projection in root template', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Directive, NgModule, TemplateRef} from '@angular/core'; + + @Component({selector: 'simple', template: '
'}) + export class SimpleComponent {} + + @Component({ + selector: 'complex', + template: \` +
+
\` + }) + export class ComplexComponent { } + + @NgModule({declarations: [SimpleComponent, ComplexComponent]}) + export class MyModule {} + + @Component({ + selector: 'my-app', + template: 'content ' }) - export class ComplexComponent { } + export class MyApp {} + ` + } + }; - @NgModule({declarations: [SimpleComponent, ComplexComponent]}) - export class MyModule {} + const SimpleComponentDefinition = ` + SimpleComponent.ngComponentDef = $r3$.ɵdefineComponent({ + type: SimpleComponent, + selectors: [["simple"]], + factory: function SimpleComponent_Factory(t) { return new (t || SimpleComponent)(); }, + consts: 2, + vars: 0, + template: function SimpleComponent_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵprojectionDef(); + $r3$.ɵelementStart(0, "div"); + $r3$.ɵprojection(1); + $r3$.ɵelementEnd(); + } + }, + encapsulation: 2 + });`; - @Component({ - selector: 'my-app', - template: 'content ' - }) - export class MyApp {} - ` - } - }; + const ComplexComponentDefinition = ` + const $c3$ = ["id","first"]; + const $c4$ = ["id","second"]; + const $c1$ = [[["span", "title", "tofirst"]], [["span", "title", "tosecond"]]]; + const $c2$ = ["span[title=toFirst]", "span[title=toSecond]"]; + … + ComplexComponent.ngComponentDef = $r3$.ɵdefineComponent({ + type: ComplexComponent, + selectors: [["complex"]], + factory: function ComplexComponent_Factory(t) { return new (t || ComplexComponent)(); }, + consts: 4, + vars: 0, + template: function ComplexComponent_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵprojectionDef($c1$, $c2$); + $r3$.ɵelementStart(0, "div", $c3$); + $r3$.ɵprojection(1, 1); + $r3$.ɵelementEnd(); + $r3$.ɵelementStart(2, "div", $c4$); + $r3$.ɵprojection(3, 2); + $r3$.ɵelementEnd(); + } + }, + encapsulation: 2 + }); + `; - const SimpleComponentDefinition = ` - SimpleComponent.ngComponentDef = $r3$.ɵdefineComponent({ - type: SimpleComponent, - selectors: [["simple"]], - factory: function SimpleComponent_Factory(t) { return new (t || SimpleComponent)(); }, - consts: 2, - vars: 0, - template: function SimpleComponent_Template(rf, ctx) { - if (rf & 1) { - $r3$.ɵprojectionDef(); - $r3$.ɵelementStart(0, "div"); - $r3$.ɵprojection(1); - $r3$.ɵelementEnd(); - } - }, - encapsulation: 2 - });`; + const result = compile(files, angularFiles); + const source = result.source; - const ComplexComponentDefinition = ` - const $c3$ = ["id","first"]; - const $c4$ = ["id","second"]; - const $c1$ = [[["span", "title", "tofirst"]], [["span", "title", "tosecond"]]]; - const $c2$ = ["span[title=toFirst]", "span[title=toSecond]"]; - … - ComplexComponent.ngComponentDef = $r3$.ɵdefineComponent({ - type: ComplexComponent, - selectors: [["complex"]], - factory: function ComplexComponent_Factory(t) { return new (t || ComplexComponent)(); }, - consts: 4, - vars: 0, - template: function ComplexComponent_Template(rf, ctx) { - if (rf & 1) { - $r3$.ɵprojectionDef($c1$, $c2$); - $r3$.ɵelementStart(0, "div", $c3$); + expectEmit( + result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition'); + expectEmit( + result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition'); + }); + + it('should support content projection in nested templates', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + template: \` +
+ +
+
+ No ng-content, no instructions generated. +
+ + '*' selector: + + \`, + }) + class Cmp {} + + @NgModule({ declarations: [Cmp] }) + class Module {} + ` + } + }; + const output = ` + const $_c0$ = [1, "ngIf"]; + const $_c1$ = ["id", "second"]; + function Cmp_div_Template_0(rf, ctx) { if (rf & 1) { + $r3$.ɵelementStart(0, "div", $_c1$); $r3$.ɵprojection(1, 1); $r3$.ɵelementEnd(); - $r3$.ɵelementStart(2, "div", $c4$); - $r3$.ɵprojection(3, 2); + } } + const $_c4$ = ["id", "third"]; + function Cmp_div_Template_1(rf, ctx) { + if (rf & 1) { + $r3$.ɵelementStart(0, "div", $_c4$); + $r3$.ɵtext(1, " No ng-content, no instructions generated. "); $r3$.ɵelementEnd(); } - }, - encapsulation: 2 - }); - `; - - const result = compile(files, angularFiles); - const source = result.source; - - expectEmit(result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition'); - expectEmit( - result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition'); - }); - - it('should support content projection in nested templates', () => { - const files = { - app: { - 'spec.ts': ` - import {Component, NgModule} from '@angular/core'; - - @Component({ - template: \` -
- -
-
- No ng-content, no instructions generated. -
- - '*' selector: - - \`, - }) - class Cmp {} - - @NgModule({ declarations: [Cmp] }) - class Module {} - ` - } - }; - const output = ` - const $_c0$ = [1, "ngIf"]; - const $_c1$ = ["id", "second"]; - const $_c2$ = [[["span", "title", "tofirst"]]]; - const $_c3$ = ["span[title=toFirst]"]; - function Cmp_div_Template_0(rf, ctx) { if (rf & 1) { - $r3$.ɵprojectionDef($_c2$, $_c3$); - $r3$.ɵelementStart(0, "div", $_c1$); - $r3$.ɵprojection(1, 1); - $r3$.ɵelementEnd(); - } } - const $_c4$ = ["id", "third"]; - function Cmp_div_Template_1(rf, ctx) { - if (rf & 1) { - $r3$.ɵelementStart(0, "div", $_c4$); - $r3$.ɵtext(1, " No ng-content, no instructions generated. "); - $r3$.ɵelementEnd(); } - } - function Cmp_ng_template_Template_2(rf, ctx) { - if (rf & 1) { - $r3$.ɵprojectionDef(); - $r3$.ɵtext(0, " '*' selector: "); - $r3$.ɵprojection(1); + function Cmp_ng_template_Template_2(rf, ctx) { + if (rf & 1) { + $r3$.ɵtext(0, " '*' selector: "); + $r3$.ɵprojection(1); + } } - } - … - template: function Cmp_Template(rf, ctx) { - if (rf & 1) { - $r3$.ɵtemplate(0, Cmp_div_Template_0, 2, 0, "div", $_c0$); - $r3$.ɵtemplate(1, Cmp_div_Template_1, 2, 0, "div", $_c0$); - $r3$.ɵtemplate(2, Cmp_ng_template_Template_2, 2, 0, "ng-template"); + const $_c2$ = [[["span", "title", "tofirst"]]]; + const $_c3$ = ["span[title=toFirst]"]; + … + template: function Cmp_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵprojectionDef($_c2$, $_c3$); + $r3$.ɵtemplate(0, Cmp_div_Template_0, 2, 0, "div", $_c0$); + $r3$.ɵtemplate(1, Cmp_div_Template_1, 2, 0, "div", $_c0$); + $r3$.ɵtemplate(2, Cmp_ng_template_Template_2, 2, 0, "ng-template"); + } + if (rf & 2) { + $r3$.ɵelementProperty(0, "ngIf", $r3$.ɵbind(ctx.visible)); + $r3$.ɵelementProperty(1, "ngIf", $r3$.ɵbind(ctx.visible)); + } } - if (rf & 2) { - $r3$.ɵelementProperty(0, "ngIf", $r3$.ɵbind(ctx.visible)); - $r3$.ɵelementProperty(1, "ngIf", $r3$.ɵbind(ctx.visible)); - } - } - `; + `; + + const {source} = compile(files, angularFiles); + expectEmit(source, output, 'Invalid content projection instructions generated'); + }); + + it('should support content projection in both the root and nested templates', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + template: \` + + + + + + + + + '*' selector in a template: + + + \`, + }) + class Cmp {} + + @NgModule({ declarations: [Cmp] }) + class Module {} + ` + } + }; + + const output = ` + function Cmp_ng_template_ng_template_Template_1(rf, ctx) { + if (rf & 1) { + $r3$.ɵprojection(0, 4); + } + } + function Cmp_ng_template_Template_1(rf, ctx) { + if (rf & 1) { + $r3$.ɵprojection(0, 3); + $r3$.ɵtemplate(1, Cmp_ng_template_ng_template_Template_1, 1, 0, "ng-template"); + } + } + function Cmp_ng_template_Template_2(rf, ctx) { + if (rf & 1) { + $r3$.ɵtext(0, " '*' selector in a template: "); + $r3$.ɵprojection(1); + } + } + const $_c0$ = [[["", "id", "tomainbefore"]], [["", "id", "tomainafter"]], [["", "id", "totemplate"]], [["", "id", "tonestedtemplate"]]]; + const $_c1$ = ["[id=toMainBefore]", "[id=toMainAfter]", "[id=toTemplate]", "[id=toNestedTemplate]"]; + … + template: function Cmp_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵprojectionDef($_c2$, $_c3$); + $r3$.ɵprojection(0, 1); + $r3$.ɵtemplate(1, Cmp_ng_template_Template_1, 2, 0, "ng-template"); + $r3$.ɵtemplate(2, Cmp_ng_template_Template_2, 2, 0, "ng-template"); + $r3$.ɵprojection(3, 2); + } + } + `; + + const {source} = compile(files, angularFiles); + expectEmit(source, output, 'Invalid content projection instructions generated'); + }); - const {source} = compile(files, angularFiles); - expectEmit(source, output, 'Invalid content projection instructions generated'); }); describe('queries', () => { diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 21ee059439..9ec9f62ab8 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -114,6 +114,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Selectors found in the tags in the template. private _ngContentSelectors: string[] = []; + // Number of non-default selectors found in all parent templates of this template. We need to + // track it to properly adjust projection bucket index in the `projection` instruction. + private _ngContentSelectorsOffset = 0; + constructor( private constantPool: ConstantPool, parentBindingScope: BindingScope, private level = 0, private contextName: string|null, private i18nContext: I18nContext|null, @@ -166,7 +170,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); } - buildTemplateFunction(nodes: t.Node[], variables: t.Variable[], i18n?: i18n.AST): o.FunctionExpr { + buildTemplateFunction( + nodes: t.Node[], variables: t.Variable[], ngContentSelectorsOffset: number = 0, + i18n?: i18n.AST): o.FunctionExpr { + this._ngContentSelectorsOffset = ngContentSelectorsOffset; + if (this._namespace !== R3.namespaceHTML) { this.creationInstruction(null, this._namespace); } @@ -192,8 +200,23 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // resolving bindings. We also count bindings in this pass as we walk bound expressions. t.visitAll(this, nodes); - // Output a `ProjectionDef` instruction when some `` are present - if (this._hasNgContent) { + // Add total binding count to pure function count so pure function instructions are + // generated with the correct slot offset when update instructions are processed. + this._pureFunctionSlots += this._bindingSlots; + + // Pipes are walked in the first pass (to enqueue `pipe()` creation instructions and + // `pipeBind` update instructions), so we have to update the slot offsets manually + // to account for bindings. + this._valueConverter.updatePipeSlotOffsets(this._bindingSlots); + + // Nested templates must be processed before creation instructions so template() + // instructions can be generated with the correct internal const count. + this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn()); + + // Output the `projectionDef` instruction when some `` are present. + // The `projectionDef` instruction only emitted for the component template and it is skipped for + // nested templates ( tags). + if (this.level === 0 && this._hasNgContent) { const parameters: o.Expression[] = []; // Only selectors with a non-default value are generated @@ -212,19 +235,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this.creationInstruction(null, R3.projectionDef, parameters, /* prepend */ true); } - // Add total binding count to pure function count so pure function instructions are - // generated with the correct slot offset when update instructions are processed. - this._pureFunctionSlots += this._bindingSlots; - - // Pipes are walked in the first pass (to enqueue `pipe()` creation instructions and - // `pipeBind` update instructions), so we have to update the slot offsets manually - // to account for bindings. - this._valueConverter.updatePipeSlotOffsets(this._bindingSlots); - - // Nested templates must be processed before creation instructions so template() - // instructions can be generated with the correct internal const count. - this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn()); - if (initI18nContext) { this.i18nEnd(null, selfClosingI18nInstruction); } @@ -419,7 +429,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const slot = this.allocateDataSlot(); let selectorIndex = ngContent.selector === DEFAULT_NG_CONTENT_SELECTOR ? 0 : - this._ngContentSelectors.push(ngContent.selector); + this._ngContentSelectors.push(ngContent.selector) + this._ngContentSelectorsOffset; const parameters: o.Expression[] = [o.literal(slot)]; const attributeAsList: string[] = []; @@ -738,8 +748,13 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // template definition. e.g.
{{ foo }}
this._nestedTemplateFns.push(() => { const templateFunctionExpr = templateVisitor.buildTemplateFunction( - template.children, template.variables, template.i18n); + template.children, template.variables, + this._ngContentSelectors.length + this._ngContentSelectorsOffset, template.i18n); this.constantPool.statements.push(templateFunctionExpr.toDeclStmt(templateName, null)); + if (templateVisitor._hasNgContent) { + this._hasNgContent = true; + this._ngContentSelectors.push(...templateVisitor._ngContentSelectors); + } }); // e.g. template(1, MyComp_Template_1) diff --git a/packages/core/test/linker/projection_integration_spec.ts b/packages/core/test/linker/projection_integration_spec.ts index 105705b263..d41a092e41 100644 --- a/packages/core/test/linker/projection_integration_spec.ts +++ b/packages/core/test/linker/projection_integration_spec.ts @@ -182,7 +182,7 @@ describe('projection', () => { expect(main.nativeElement).toHaveText('OUTER(INNER(INNERINNER(A,BC)))'); }); - fixmeIvy('FW-796: Content projection logic is incorrect for in nested templates') + fixmeIvy('FW-833: Directive / projected node matching against class name') .it('should redistribute when the shadow dom changes', () => { TestBed.configureTestingModule( {declarations: [ConditionalContentComponent, ManualViewportDirective]}); @@ -290,7 +290,7 @@ describe('projection', () => { expect(main.nativeElement).toHaveText('SIMPLE()START(A)END'); }); - fixmeIvy('FW-796: Content projection logic is incorrect for in nested templates') + fixmeIvy('FW-833: Directive / projected node matching against class name') .it('should support moving ng-content around', () => { TestBed.configureTestingModule({ declarations: [ConditionalContentComponent, ProjectDirective, ManualViewportDirective] @@ -430,7 +430,7 @@ describe('projection', () => { }); } - fixmeIvy('FW-796: Content projection logic is incorrect for in nested templates') + fixmeIvy('FW-869: debugElement.queryAllNodes returns nodes in the wrong order') .it('should support nested conditionals that contain ng-contents', () => { TestBed.configureTestingModule( {declarations: [ConditionalTextComponent, ManualViewportDirective]}); @@ -474,7 +474,66 @@ describe('projection', () => { 'a2b21b22'); }); - fixmeIvy('FW-796: Content projection logic is incorrect for in nested templates') + it('should project nodes into nested templates when the main template doesn\'t have ', + () => { + + @Component({ + selector: 'content-in-template', + template: + `()` + }) + class ContentInATemplateComponent { + } + + + TestBed.configureTestingModule( + {declarations: [ContentInATemplateComponent, ManualViewportDirective]}); + TestBed.overrideComponent( + MainComp, + {set: {template: `
A
`}}); + + const main = TestBed.createComponent(MainComp); + + main.detectChanges(); + expect(main.nativeElement).toHaveText('()'); + + let viewportElement = + main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0]; + viewportElement.injector.get(ManualViewportDirective).show(); + expect(main.nativeElement).toHaveText('(A)'); + }); + + it('should project nodes into nested templates and the main template', () => { + + @Component({ + selector: 'content-in-main-and-template', + template: + `()` + }) + class ContentInMainAndTemplateComponent { + } + + + TestBed.configureTestingModule( + {declarations: [ContentInMainAndTemplateComponent, ManualViewportDirective]}); + TestBed.overrideComponent(MainComp, { + set: { + template: + `
A
B
` + } + }); + + const main = TestBed.createComponent(MainComp); + + main.detectChanges(); + expect(main.nativeElement).toHaveText('B()'); + + let viewportElement = main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0]; + viewportElement.injector.get(ManualViewportDirective).show(); + expect(main.nativeElement).toHaveText('B(A)'); + }); + + fixmeIvy('FW-833: Directive / projected node matching against class name') .it('should project filled view containers into a view container', () => { TestBed.configureTestingModule( {declarations: [ConditionalContentComponent, ManualViewportDirective]});