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:
parent
a833b98fd0
commit
a0585c9a9a
|
@ -1092,6 +1092,8 @@ describe('compiler compliance', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('content projection', () => {
|
||||
|
||||
it('should support content projection in root template', () => {
|
||||
const files = {
|
||||
app: {
|
||||
|
@ -1169,7 +1171,8 @@ describe('compiler compliance', () => {
|
|||
const result = compile(files, angularFiles);
|
||||
const source = result.source;
|
||||
|
||||
expectEmit(result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition');
|
||||
expectEmit(
|
||||
result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition');
|
||||
expectEmit(
|
||||
result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition');
|
||||
});
|
||||
|
@ -1203,10 +1206,7 @@ describe('compiler compliance', () => {
|
|||
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();
|
||||
|
@ -1221,14 +1221,16 @@ describe('compiler compliance', () => {
|
|||
}
|
||||
function Cmp_ng_template_Template_2(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵprojectionDef();
|
||||
$r3$.ɵtext(0, " '*' selector: ");
|
||||
$r3$.ɵprojection(1);
|
||||
}
|
||||
}
|
||||
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");
|
||||
|
@ -1244,6 +1246,73 @@ describe('compiler compliance', () => {
|
|||
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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('queries', () => {
|
||||
const directive = {
|
||||
'some.directive.ts': `
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]});
|
||||
|
|
Loading…
Reference in New Issue