fix(ivy): correct content projection with nested templates (#27755)

Previously ivy code generation was emmiting the projectionDef instruction in
a template where the <ng-content> tag was found. This code generation logic was
incorrect since the ivy runtime expects the projectionDef instruction to be present
in the main template only.

This PR ammends the code generation logic so that the projectionDef instruction is
emmitedin the main template only.

PR Close #27755
This commit is contained in:
Pawel Kozlowski 2018-12-19 15:03:36 +01:00 committed by Matias Niemelä
parent a833b98fd0
commit a0585c9a9a
3 changed files with 301 additions and 158 deletions

View File

@ -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: '<div><ng-content></ng-content></div>'})
export class SimpleComponent {}
@Component({
selector: 'complex',
template: \`
<div id="first"><ng-content select="span[title=toFirst]"></ng-content></div>
<div id="second"><ng-content SELECT="span[title=toSecond]"></ng-content></div>\`
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: '<div><ng-content></ng-content></div>'})
export class SimpleComponent {}
@Component({
selector: 'complex',
template: \`
<div id="first"><ng-content select="span[title=toFirst]"></ng-content></div>
<div id="second"><ng-content SELECT="span[title=toSecond]"></ng-content></div>\`
})
export class ComplexComponent { }
@NgModule({declarations: [SimpleComponent, ComplexComponent]})
export class MyModule {}
@Component({
selector: 'my-app',
template: '<simple>content</simple> <complex></complex>'
})
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: '<simple>content</simple> <complex></complex>'
})
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: \`
<div id="second" *ngIf="visible">
<ng-content SELECT="span[title=toFirst]"></ng-content>
</div>
<div id="third" *ngIf="visible">
No ng-content, no instructions generated.
</div>
<ng-template>
'*' selector: <ng-content></ng-content>
</ng-template>
\`,
})
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: \`
<div id="second" *ngIf="visible">
<ng-content SELECT="span[title=toFirst]"></ng-content>
</div>
<div id="third" *ngIf="visible">
No ng-content, no instructions generated.
</div>
<ng-template>
'*' selector: <ng-content></ng-content>
</ng-template>
\`,
})
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: \`
<ng-content select="[id=toMainBefore]"></ng-content>
<ng-template>
<ng-content select="[id=toTemplate]"></ng-content>
<ng-template>
<ng-content select="[id=toNestedTemplate]"></ng-content>
</ng-template>
</ng-template>
<ng-template>
'*' selector in a template: <ng-content></ng-content>
</ng-template>
<ng-content select="[id=toMainAfter]"></ng-content>
\`,
})
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', () => {

View File

@ -114,6 +114,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Selectors found in the <ng-content> 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<void>, 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<void>, 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 `<ng-content>` 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 `<ng-content>` are present.
// The `projectionDef` instruction only emitted for the component template and it is skipped for
// nested templates (<ng-template> 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<void>, 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<void>, 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<void>, LocalResolver
// template definition. e.g. <div *ngIf="showing"> {{ foo }} </div> <div #foo></div>
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)

View File

@ -182,7 +182,7 @@ describe('projection', () => {
expect(main.nativeElement).toHaveText('OUTER(INNER(INNERINNER(A,BC)))');
});
fixmeIvy('FW-796: Content projection logic is incorrect for <ng-content> 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 <ng-content> 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 <ng-content> 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', () => {
'<cmp-a2>a2<cmp-b21>b21</cmp-b21><cmp-b22>b22</cmp-b22></cmp-a2>');
});
fixmeIvy('FW-796: Content projection logic is incorrect for <ng-content> in nested templates')
it('should project nodes into nested templates when the main template doesn\'t have <ng-content>',
() => {
@Component({
selector: 'content-in-template',
template:
`(<ng-template manual><ng-content select="[id=left]"></ng-content></ng-template>)`
})
class ContentInATemplateComponent {
}
TestBed.configureTestingModule(
{declarations: [ContentInATemplateComponent, ManualViewportDirective]});
TestBed.overrideComponent(
MainComp,
{set: {template: `<content-in-template><div id="left">A</div></content-in-template>`}});
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:
`<ng-content></ng-content>(<ng-template manual><ng-content select="[id=left]"></ng-content></ng-template>)`
})
class ContentInMainAndTemplateComponent {
}
TestBed.configureTestingModule(
{declarations: [ContentInMainAndTemplateComponent, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template:
`<content-in-main-and-template><div id="left">A</div>B</content-in-main-and-template>`
}
});
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]});