Skip to content

Commit f0b0d64

Browse files
AndrewKushnirIgorMinar
authored andcommitted
fix(ivy): adding projectDef instructions to all templates where <ng-content> is present (FW-745) (angular#27384)
Prior to this change `projectDef` instructions were placed to root templates only, thus the necessary information (selectors) in nested templates was missing. This update adds the logic to insert `projectDef` instructions to all templates where <ng-content> is present. PR Close angular#27384
1 parent 8e644d9 commit f0b0d64

File tree

7 files changed

+132
-105
lines changed

7 files changed

+132
-105
lines changed

packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,7 +1013,7 @@ describe('compiler compliance', () => {
10131013
});
10141014
});
10151015

1016-
it('should support content projection', () => {
1016+
it('should support content projection in root template', () => {
10171017
const files = {
10181018
app: {
10191019
'spec.ts': `
@@ -1061,10 +1061,10 @@ describe('compiler compliance', () => {
10611061
});`;
10621062

10631063
const ComplexComponentDefinition = `
1064-
const $c1$ = [[["span", "title", "tofirst"]], [["span", "title", "tosecond"]]];
1065-
const $c2$ = ["span[title=toFirst]", "span[title=toSecond]"];
10661064
const $c3$ = ["id","first"];
10671065
const $c4$ = ["id","second"];
1066+
const $c1$ = [[["span", "title", "tofirst"]], [["span", "title", "tosecond"]]];
1067+
const $c2$ = ["span[title=toFirst]", "span[title=toSecond]"];
10681068
10691069
ComplexComponent.ngComponentDef = $r3$.ɵdefineComponent({
10701070
type: ComplexComponent,
@@ -1095,6 +1095,76 @@ describe('compiler compliance', () => {
10951095
result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition');
10961096
});
10971097

1098+
it('should support content projection in nested templates', () => {
1099+
const files = {
1100+
app: {
1101+
'spec.ts': `
1102+
import {Component, NgModule} from '@angular/core';
1103+
1104+
@Component({
1105+
template: \`
1106+
<div id="second" *ngIf="visible">
1107+
<ng-content select="span[title=toFirst]"></ng-content>
1108+
</div>
1109+
<div id="third" *ngIf="visible">
1110+
No ng-content, no instructions generated.
1111+
</div>
1112+
<ng-template>
1113+
'*' selector: <ng-content></ng-content>
1114+
</ng-template>
1115+
\`,
1116+
})
1117+
class Cmp {}
1118+
1119+
@NgModule({ declarations: [Cmp] })
1120+
class Module {}
1121+
`
1122+
}
1123+
};
1124+
const output = `
1125+
const $_c0$ = [1, "ngIf"];
1126+
const $_c1$ = ["id", "second"];
1127+
const $_c2$ = [[["span", "title", "tofirst"]]];
1128+
const $_c3$ = ["span[title=toFirst]"];
1129+
function Cmp_div_Template_0(rf, ctx) { if (rf & 1) {
1130+
$r3$.ɵprojectionDef($_c2$, $_c3$);
1131+
$r3$.ɵelementStart(0, "div", $_c1$);
1132+
$r3$.ɵprojection(1, 1);
1133+
$r3$.ɵelementEnd();
1134+
} }
1135+
const $_c4$ = ["id", "third"];
1136+
function Cmp_div_Template_1(rf, ctx) {
1137+
if (rf & 1) {
1138+
$r3$.ɵelementStart(0, "div", $_c4$);
1139+
$r3$.ɵtext(1, " No ng-content, no instructions generated. ");
1140+
$r3$.ɵelementEnd();
1141+
}
1142+
}
1143+
function Template_2(rf, ctx) {
1144+
if (rf & 1) {
1145+
$r3$.ɵprojectionDef();
1146+
$r3$.ɵtext(0, " '*' selector: ");
1147+
$r3$.ɵprojection(1);
1148+
}
1149+
}
1150+
1151+
template: function Cmp_Template(rf, ctx) {
1152+
if (rf & 1) {
1153+
$r3$.ɵtemplate(0, Cmp_div_Template_0, 2, 0, null, $_c0$);
1154+
$r3$.ɵtemplate(1, Cmp_div_Template_1, 2, 0, null, $_c0$);
1155+
$r3$.ɵtemplate(2, Template_2, 2, 0);
1156+
}
1157+
if (rf & 2) {
1158+
$r3$.ɵelementProperty(0, "ngIf", $r3$.ɵbind(ctx.visible));
1159+
$r3$.ɵelementProperty(1, "ngIf", $r3$.ɵbind(ctx.visible));
1160+
}
1161+
}
1162+
`;
1163+
1164+
const {source} = compile(files, angularFiles);
1165+
expectEmit(source, output, 'Invalid content projection instructions generated');
1166+
});
1167+
10981168
describe('queries', () => {
10991169
const directive = {
11001170
'some.directive.ts': `

packages/compiler/src/render3/r3_ast.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class Template implements Node {
8383

8484
export class Content implements Node {
8585
constructor(
86-
public selectorIndex: number, public attributes: TextAttribute[],
86+
public selector: string, public attributes: TextAttribute[],
8787
public sourceSpan: ParseSourceSpan, public i18n?: I18nAST) {}
8888
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitContent(this); }
8989
}

packages/compiler/src/render3/r3_template_transform.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,10 @@ const IDENT_PROPERTY_IDX = 9;
4545
const IDENT_EVENT_IDX = 10;
4646

4747
const TEMPLATE_ATTR_PREFIX = '*';
48-
// Default selector used by `<ng-content>` if none specified
49-
const DEFAULT_CONTENT_SELECTOR = '*';
5048

5149
// Result of the html AST to Ivy AST transformation
5250
export type Render3ParseResult = {
5351
nodes: t.Node[]; errors: ParseError[];
54-
// Any non default (empty or '*') selector found in the template
55-
ngContentSelectors: string[];
56-
// Wether the template contains any `<ng-content>`
57-
hasNgContent: boolean;
5852
};
5953

6054
export function htmlAstToRender3Ast(
@@ -74,17 +68,11 @@ export function htmlAstToRender3Ast(
7468
return {
7569
nodes: ivyNodes,
7670
errors: allErrors,
77-
ngContentSelectors: transformer.ngContentSelectors,
78-
hasNgContent: transformer.hasNgContent,
7971
};
8072
}
8173

8274
class HtmlAstToIvyAst implements html.Visitor {
8375
errors: ParseError[] = [];
84-
// Selectors for the `ng-content` tags. Only non `*` selectors are recorded here
85-
ngContentSelectors: string[] = [];
86-
// Any `<ng-content>` in the template ?
87-
hasNgContent = false;
8876

8977
constructor(private bindingParser: BindingParser) {}
9078

@@ -168,20 +156,12 @@ class HtmlAstToIvyAst implements html.Visitor {
168156
let parsedElement: t.Node|undefined;
169157
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
170158
// `<ng-content>`
171-
this.hasNgContent = true;
172-
173159
if (element.children && !element.children.every(isEmptyTextNode)) {
174160
this.reportError(`<ng-content> element cannot have content.`, element.sourceSpan);
175161
}
176-
177162
const selector = preparsedElement.selectAttr;
178-
179-
let attributes: t.TextAttribute[] =
180-
element.attrs.map(attribute => this.visitAttribute(attribute));
181-
182-
const selectorIndex =
183-
selector === DEFAULT_CONTENT_SELECTOR ? 0 : this.ngContentSelectors.push(selector);
184-
parsedElement = new t.Content(selectorIndex, attributes, element.sourceSpan, element.i18n);
163+
const attrs: t.TextAttribute[] = element.attrs.map(attr => this.visitAttribute(attr));
164+
parsedElement = new t.Content(selector, attrs, element.sourceSpan, element.i18n);
185165
} else if (isTemplateElement) {
186166
// `<ng-template>`
187167
const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta);

packages/compiler/src/render3/view/api.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,6 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata {
123123
* Parsed nodes of the template.
124124
*/
125125
nodes: t.Node[];
126-
127-
/**
128-
* Whether the template includes <ng-content> tags.
129-
*/
130-
hasNgContent: boolean;
131-
132-
/**
133-
* Selectors found in the <ng-content> tags in the template.
134-
*/
135-
ngContentSelectors: string[];
136126
};
137127

138128
/**

packages/compiler/src/render3/view/compiler.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,7 @@ export function compileComponentFromMetadata(
258258
meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML,
259259
meta.relativeContextFilePath, meta.i18nUseExternalIds);
260260

261-
const templateFunctionExpression = templateBuilder.buildTemplateFunction(
262-
template.nodes, [], template.hasNgContent, template.ngContentSelectors);
261+
const templateFunctionExpression = templateBuilder.buildTemplateFunction(template.nodes, []);
263262

264263
// e.g. `consts: 2`
265264
definitionMap.set('consts', o.literal(templateBuilder.getConstCount()));
@@ -371,11 +370,7 @@ export function compileComponentFromRender2(
371370
const meta: R3ComponentMetadata = {
372371
...directiveMetadataFromGlobalMetadata(component, outputCtx, reflector),
373372
selector: component.selector,
374-
template: {
375-
nodes: render3Ast.nodes,
376-
hasNgContent: render3Ast.hasNgContent,
377-
ngContentSelectors: render3Ast.ngContentSelectors,
378-
},
373+
template: {nodes: render3Ast.nodes},
379374
directives: [],
380375
pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx),
381376
viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx),

packages/compiler/src/render3/view/template.ts

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export function renderFlagCheckIfStmt(
5858
return o.ifStmt(o.variable(RENDER_FLAGS).bitwiseAnd(o.literal(flags), null, false), statements);
5959
}
6060

61+
// Default selector used by `<ng-content>` if none specified
62+
const DEFAULT_CONTENT_SELECTOR = '*';
6163
export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver {
6264
private _dataIndex = 0;
6365
private _bindingContext = 0;
@@ -102,6 +104,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
102104

103105
private fileBasedI18nSuffix: string;
104106

107+
// Whether the template includes <ng-content> tags.
108+
private _hasNgContent: boolean = false;
109+
110+
// Selectors found in the <ng-content> tags in the template.
111+
private _ngContentSelectors: string[] = [];
112+
105113
constructor(
106114
private constantPool: ConstantPool, parentBindingScope: BindingScope, private level = 0,
107115
private contextName: string|null, private i18nContext: I18nContext|null,
@@ -154,32 +162,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
154162
});
155163
}
156164

157-
buildTemplateFunction(
158-
nodes: t.Node[], variables: t.Variable[], hasNgContent: boolean = false,
159-
ngContentSelectors: string[] = [], i18n?: i18n.AST): o.FunctionExpr {
165+
buildTemplateFunction(nodes: t.Node[], variables: t.Variable[], i18n?: i18n.AST): o.FunctionExpr {
160166
if (this._namespace !== R3.namespaceHTML) {
161167
this.creationInstruction(null, this._namespace);
162168
}
163169

164170
// Create variable bindings
165171
variables.forEach(v => this.registerContextVariables(v));
166172

167-
// Output a `ProjectionDef` instruction when some `<ng-content>` are present
168-
if (hasNgContent) {
169-
const parameters: o.Expression[] = [];
170-
171-
// Only selectors with a non-default value are generated
172-
if (ngContentSelectors.length > 1) {
173-
const r3Selectors = ngContentSelectors.map(s => core.parseSelectorToR3Selector(s));
174-
// `projectionDef` needs both the parsed and raw value of the selectors
175-
const parsed = this.constantPool.getConstLiteral(asLiteral(r3Selectors), true);
176-
const unParsed = this.constantPool.getConstLiteral(asLiteral(ngContentSelectors), true);
177-
parameters.push(parsed, unParsed);
178-
}
179-
180-
this.creationInstruction(null, R3.projectionDef, parameters);
181-
}
182-
183173
// Initiate i18n context in case:
184174
// - this template has parent i18n context
185175
// - or the template has i18n meta associated with it,
@@ -198,6 +188,26 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
198188
// resolving bindings. We also count bindings in this pass as we walk bound expressions.
199189
t.visitAll(this, nodes);
200190

191+
// Output a `ProjectionDef` instruction when some `<ng-content>` are present
192+
if (this._hasNgContent) {
193+
const parameters: o.Expression[] = [];
194+
195+
// Only selectors with a non-default value are generated
196+
if (this._ngContentSelectors.length) {
197+
const r3Selectors = this._ngContentSelectors.map(s => core.parseSelectorToR3Selector(s));
198+
// `projectionDef` needs both the parsed and raw value of the selectors
199+
const parsed = this.constantPool.getConstLiteral(asLiteral(r3Selectors), true);
200+
const unParsed =
201+
this.constantPool.getConstLiteral(asLiteral(this._ngContentSelectors), true);
202+
parameters.push(parsed, unParsed);
203+
}
204+
205+
// Since we accumulate ngContent selectors while processing template elements,
206+
// we *prepend* `projectionDef` to creation instructions block, to put it before
207+
// any `projection` instructions
208+
this.creationInstruction(null, R3.projectionDef, parameters, /* prepend */ true);
209+
}
210+
201211
// Add total binding count to pure function count so pure function instructions are
202212
// generated with the correct slot offset when update instructions are processed.
203213
this._pureFunctionSlots += this._bindingSlots;
@@ -399,8 +409,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
399409
}
400410

401411
visitContent(ngContent: t.Content) {
412+
this._hasNgContent = true;
402413
const slot = this.allocateDataSlot();
403-
const selectorIndex = ngContent.selectorIndex;
414+
let selectorIndex = ngContent.selector === DEFAULT_CONTENT_SELECTOR ?
415+
0 :
416+
this._ngContentSelectors.push(ngContent.selector);
404417
const parameters: o.Expression[] = [o.literal(slot)];
405418

406419
const attributeAsList: string[] = [];
@@ -724,7 +737,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
724737
// template definition. e.g. <div *ngIf="showing"> {{ foo }} </div> <div #foo></div>
725738
this._nestedTemplateFns.push(() => {
726739
const templateFunctionExpr = templateVisitor.buildTemplateFunction(
727-
template.children, template.variables, false, [], template.i18n);
740+
template.children, template.variables, template.i18n);
728741
this.constantPool.statements.push(templateFunctionExpr.toDeclStmt(templateName, null));
729742
});
730743

@@ -834,8 +847,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
834847
// bindings. e.g. {{ foo }} <div #foo></div>
835848
private instructionFn(
836849
fns: (() => o.Statement)[], span: ParseSourceSpan|null, reference: o.ExternalReference,
837-
paramsOrFn: o.Expression[]|(() => o.Expression[])): void {
838-
fns.push(() => {
850+
paramsOrFn: o.Expression[]|(() => o.Expression[]), prepend: boolean = false): void {
851+
fns[prepend ? 'unshift' : 'push'](() => {
839852
const params = Array.isArray(paramsOrFn) ? paramsOrFn : paramsOrFn();
840853
return instruction(span, reference, params).toStmt();
841854
});
@@ -856,8 +869,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
856869

857870
private creationInstruction(
858871
span: ParseSourceSpan|null, reference: o.ExternalReference,
859-
paramsOrFn?: o.Expression[]|(() => o.Expression[])) {
860-
this.instructionFn(this._creationCodeFns, span, reference, paramsOrFn || []);
872+
paramsOrFn?: o.Expression[]|(() => o.Expression[]), prepend?: boolean) {
873+
this.instructionFn(this._creationCodeFns, span, reference, paramsOrFn || [], prepend);
861874
}
862875

863876
private updateInstruction(
@@ -1398,14 +1411,14 @@ function interpolate(args: o.Expression[]): o.Expression {
13981411
export function parseTemplate(
13991412
template: string, templateUrl: string,
14001413
options: {preserveWhitespaces?: boolean, interpolationConfig?: InterpolationConfig} = {}):
1401-
{errors?: ParseError[], nodes: t.Node[], hasNgContent: boolean, ngContentSelectors: string[]} {
1414+
{errors?: ParseError[], nodes: t.Node[]} {
14021415
const {interpolationConfig, preserveWhitespaces} = options;
14031416
const bindingParser = makeBindingParser(interpolationConfig);
14041417
const htmlParser = new HtmlParser();
14051418
const parseResult = htmlParser.parse(template, templateUrl, true, interpolationConfig);
14061419

14071420
if (parseResult.errors && parseResult.errors.length > 0) {
1408-
return {errors: parseResult.errors, nodes: [], hasNgContent: false, ngContentSelectors: []};
1421+
return {errors: parseResult.errors, nodes: []};
14091422
}
14101423

14111424
let rootNodes: html.Node[] = parseResult.rootNodes;
@@ -1428,13 +1441,12 @@ export function parseTemplate(
14281441
new I18nMetaVisitor(interpolationConfig, /* keepI18nAttrs */ false), rootNodes);
14291442
}
14301443

1431-
const {nodes, hasNgContent, ngContentSelectors, errors} =
1432-
htmlAstToRender3Ast(rootNodes, bindingParser);
1444+
const {nodes, errors} = htmlAstToRender3Ast(rootNodes, bindingParser);
14331445
if (errors && errors.length > 0) {
1434-
return {errors, nodes: [], hasNgContent: false, ngContentSelectors: []};
1446+
return {errors, nodes: []};
14351447
}
14361448

1437-
return {nodes, hasNgContent, ngContentSelectors};
1449+
return {nodes};
14381450
}
14391451

14401452
/**

0 commit comments

Comments
 (0)