Skip to content

Commit c7b730e

Browse files
crisbetoalxhub
authored andcommitted
refactor(compiler): implement control flow content projection fix in template pipeline (#52414)
Recreates the fix for content projection in control flow in the new template pipeline. I also had to make the following adjustments to the pipeline: 1. The `TemplateOp.tag` property was being used to generate the name of the template function, rather than the actual tag name being passed into `ɵɵtemplate`. Since the content projection fix requires the tag name to be passed in, I've introduced a new `functionNameSuffix` property instead. 2. `TemplateOp.block` was being used to determine whether to pass `TemplateOp.tag` into the `ɵɵtemplate` instruction. Now that we're always passing in the tag name after the refactor in point 1, we no longer need this flag. In addition to the refactors above, I also made some minor cleanups where I saw the opportunity to do so. PR Close #52414
1 parent eb15358 commit c7b730e

File tree

5 files changed

+113
-41
lines changed

5 files changed

+113
-41
lines changed

packages/compiler/src/template/pipeline/ir/src/ops/create.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,9 @@ export interface TemplateOp extends ElementOpBase {
183183
vars: number|null;
184184

185185
/**
186-
* Whether or not this template was automatically created for use with block syntax (control flow
187-
* or defer). This will eventually cause the emitted template instruction to use fewer arguments,
188-
* since several of the default arguments are unnecessary for blocks.
186+
* Suffix to add to the name of the generated template function.
189187
*/
190-
block: boolean;
188+
functionNameSuffix: string;
191189

192190
/**
193191
* The i18n placeholder data associated with this template.
@@ -199,15 +197,15 @@ export interface TemplateOp extends ElementOpBase {
199197
* Create a `TemplateOp`.
200198
*/
201199
export function createTemplateOp(
202-
xref: XrefId, tag: string|null, namespace: Namespace, generatedInBlock: boolean,
200+
xref: XrefId, tag: string|null, functionNameSuffix: string, namespace: Namespace,
203201
i18nPlaceholder: i18n.TagPlaceholder|undefined, sourceSpan: ParseSourceSpan): TemplateOp {
204202
return {
205203
kind: OpKind.Template,
206204
xref,
207205
attributes: null,
208206
tag,
209207
slot: new SlotHandle(),
210-
block: generatedInBlock,
208+
functionNameSuffix,
211209
decls: null,
212210
vars: null,
213211
localRefs: [],
@@ -264,6 +262,11 @@ export interface RepeaterCreateOp extends ElementOpBase {
264262
*/
265263
usesComponentInstance: boolean;
266264

265+
/**
266+
* Suffix to add to the name of the generated template function.
267+
*/
268+
functionNameSuffix: string;
269+
267270
sourceSpan: ParseSourceSpan;
268271
}
269272

@@ -279,8 +282,8 @@ export interface RepeaterVarNames {
279282
}
280283

281284
export function createRepeaterCreateOp(
282-
primaryView: XrefId, emptyView: XrefId|null, track: o.Expression, varNames: RepeaterVarNames,
283-
sourceSpan: ParseSourceSpan): RepeaterCreateOp {
285+
primaryView: XrefId, emptyView: XrefId|null, tag: string|null, track: o.Expression,
286+
varNames: RepeaterVarNames, sourceSpan: ParseSourceSpan): RepeaterCreateOp {
284287
return {
285288
kind: OpKind.RepeaterCreate,
286289
attributes: null,
@@ -289,7 +292,8 @@ export function createRepeaterCreateOp(
289292
emptyView,
290293
track,
291294
trackByFn: null,
292-
tag: 'For',
295+
tag,
296+
functionNameSuffix: 'For',
293297
namespace: Namespace.HTML,
294298
nonBindable: false,
295299
localRefs: [],

packages/compiler/src/template/pipeline/src/ingest.ts

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {BindingParser} from '../../../template_parser/binding_parser';
1818
import * as ir from '../ir';
1919

2020
import {ComponentCompilationJob, HostBindingCompilationJob, type CompilationJob, type ViewCompilationUnit} from './compilation';
21-
import {BINARY_OPERATORS, namespaceForKey} from './conversion';
21+
import {BINARY_OPERATORS, namespaceForKey, prefixWithNamespace} from './conversion';
2222

2323
const compatibilityMode = ir.CompatibilityMode.TemplateDefinitionBuilder;
2424

@@ -149,10 +149,6 @@ function ingestElement(unit: ViewCompilationUnit, element: t.Element): void {
149149
throw Error(`Unhandled i18n metadata type for element: ${element.i18n.constructor.name}`);
150150
}
151151

152-
const staticAttributes: Record<string, string> = {};
153-
for (const attr of element.attributes) {
154-
staticAttributes[attr.name] = attr.value;
155-
}
156152
const id = unit.job.allocateXrefId();
157153

158154
const [namespaceKey, elementName] = splitNsName(element.name);
@@ -196,9 +192,13 @@ function ingestTemplate(unit: ViewCompilationUnit, tmpl: t.Template): void {
196192
}
197193

198194
const i18nPlaceholder = tmpl.i18n instanceof i18n.TagPlaceholder ? tmpl.i18n : undefined;
195+
const namespace = namespaceForKey(namespacePrefix);
196+
const functionNameSuffix = tagNameWithoutNamespace === null ?
197+
'' :
198+
prefixWithNamespace(tagNameWithoutNamespace, namespace);
199199
const tplOp = ir.createTemplateOp(
200-
childView.xref, tagNameWithoutNamespace, namespaceForKey(namespacePrefix), false,
201-
i18nPlaceholder, tmpl.startSourceSpan);
200+
childView.xref, tagNameWithoutNamespace, functionNameSuffix, namespace, i18nPlaceholder,
201+
tmpl.startSourceSpan);
202202
unit.create.push(tplOp);
203203

204204
ingestBindings(unit, tplOp, tmpl);
@@ -281,20 +281,29 @@ function ingestIfBlock(unit: ViewCompilationUnit, ifBlock: t.IfBlock): void {
281281
let firstXref: ir.XrefId|null = null;
282282
let firstSlotHandle: ir.SlotHandle|null = null;
283283
let conditions: Array<ir.ConditionalCaseExpr> = [];
284-
for (const ifCase of ifBlock.branches) {
284+
for (let i = 0; i < ifBlock.branches.length; i++) {
285+
const ifCase = ifBlock.branches[i];
285286
const cView = unit.job.allocateView(unit.xref);
287+
let tagName: string|null = null;
288+
289+
// Only the first branch can be used for projection, because the conditional
290+
// uses the container of the first branch as the insertion point for all branches.
291+
if (i === 0) {
292+
tagName = ingestControlFlowInsertionPoint(unit, cView.xref, ifCase);
293+
}
286294
if (ifCase.expressionAlias !== null) {
287295
cView.contextVariables.set(ifCase.expressionAlias.name, ir.CTX_REF);
288296
}
289297
const tmplOp = ir.createTemplateOp(
290-
cView.xref, 'Conditional', ir.Namespace.HTML, true,
298+
cView.xref, tagName, 'Conditional', ir.Namespace.HTML,
291299
undefined /* TODO: figure out how i18n works with new control flow */, ifCase.sourceSpan);
292300
unit.create.push(tmplOp);
293301

294302
if (firstXref === null) {
295303
firstXref = cView.xref;
296304
firstSlotHandle = tmplOp.slot;
297305
}
306+
298307
const caseExpr = ifCase.expression ? convertAst(ifCase.expression, unit.job, null) : null;
299308
const conditionalCaseExpr =
300309
new ir.ConditionalCaseExpr(caseExpr, tmplOp.xref, tmplOp.slot, ifCase.expressionAlias);
@@ -316,7 +325,7 @@ function ingestSwitchBlock(unit: ViewCompilationUnit, switchBlock: t.SwitchBlock
316325
for (const switchCase of switchBlock.cases) {
317326
const cView = unit.job.allocateView(unit.xref);
318327
const tmplOp = ir.createTemplateOp(
319-
cView.xref, 'Case', ir.Namespace.HTML, true,
328+
cView.xref, null, 'Case', ir.Namespace.HTML,
320329
undefined /* TODO: figure out how i18n works with new control flow */,
321330
switchCase.sourceSpan);
322331
unit.create.push(tmplOp);
@@ -346,7 +355,7 @@ function ingestDeferView(
346355
const secondaryView = unit.job.allocateView(unit.xref);
347356
ingestNodes(secondaryView, children);
348357
const templateOp = ir.createTemplateOp(
349-
secondaryView.xref, `Defer${suffix}`, ir.Namespace.HTML, true, undefined, sourceSpan!);
358+
secondaryView.xref, null, `Defer${suffix}`, ir.Namespace.HTML, undefined, sourceSpan!);
350359
unit.create.push(templateOp);
351360
return templateOp;
352361
}
@@ -503,8 +512,9 @@ function ingestForBlock(unit: ViewCompilationUnit, forBlock: t.ForLoopBlock): vo
503512
$implicit: forBlock.item.name,
504513
};
505514

515+
const tagName = ingestControlFlowInsertionPoint(unit, repeaterView.xref, forBlock);
506516
const repeaterCreate = ir.createRepeaterCreateOp(
507-
repeaterView.xref, emptyView?.xref ?? null, track, varNames, forBlock.sourceSpan);
517+
repeaterView.xref, emptyView?.xref ?? null, tagName, track, varNames, forBlock.sourceSpan);
508518
unit.create.push(repeaterCreate);
509519

510520
const expression = convertAst(
@@ -841,3 +851,64 @@ function convertSourceSpan(
841851
const fullStart = baseSourceSpan.fullStart.moveBy(span.start);
842852
return new ParseSourceSpan(start, end, fullStart);
843853
}
854+
855+
/**
856+
* With the directive-based control flow users were able to conditionally project content using
857+
* the `*` syntax. E.g. `<div *ngIf="expr" projectMe></div>` will be projected into
858+
* `<ng-content select="[projectMe]"/>`, because the attributes and tag name from the `div` are
859+
* copied to the template via the template creation instruction. With `@if` and `@for` that is
860+
* not the case, because the conditional is placed *around* elements, rather than *on* them.
861+
* The result is that content projection won't work in the same way if a user converts from
862+
* `*ngIf` to `@if`.
863+
*
864+
* This function aims to cover the most common case by doing the same copying when a control flow
865+
* node has *one and only one* root element or template node.
866+
*
867+
* This approach comes with some caveats:
868+
* 1. As soon as any other node is added to the root, the copying behavior won't work anymore.
869+
* A diagnostic will be added to flag cases like this and to explain how to work around it.
870+
* 2. If `preserveWhitespaces` is enabled, it's very likely that indentation will break this
871+
* workaround, because it'll include an additional text node as the first child. We can work
872+
* around it here, but in a discussion it was decided not to, because the user explicitly opted
873+
* into preserving the whitespace and we would have to drop it from the generated code.
874+
* The diagnostic mentioned point #1 will flag such cases to users.
875+
*
876+
* @returns Tag name to be used for the control flow template.
877+
*/
878+
function ingestControlFlowInsertionPoint(
879+
unit: ViewCompilationUnit, xref: ir.XrefId, node: t.IfBlockBranch|t.ForLoopBlock): string|null {
880+
let root: t.Element|t.Template|null = null;
881+
882+
for (const child of node.children) {
883+
// Skip over comment nodes.
884+
if (child instanceof t.Comment) {
885+
continue;
886+
}
887+
888+
// We can only infer the tag name/attributes if there's a single root node.
889+
if (root !== null) {
890+
return null;
891+
}
892+
893+
// Root nodes can only elements or templates with a tag name (e.g. `<div *foo></div>`).
894+
if (child instanceof t.Element || (child instanceof t.Template && child.tagName !== null)) {
895+
root = child;
896+
}
897+
}
898+
899+
// If we've found a single root node, its tag name and *static* attributes can be copied
900+
// to the surrounding template to be used for content projection. Note that it's important
901+
// that we don't copy any bound attributes since they don't participate in content projection
902+
// and they can be used in directive matching (in the case of `Template.templateAttrs`).
903+
if (root !== null) {
904+
for (const attr of root.attributes) {
905+
ingestBinding(
906+
unit, xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null,
907+
SecurityContext.NONE, attr.sourceSpan, BindingFlags.TextValue);
908+
}
909+
910+
return root instanceof t.Element ? root.name : root.tagName;
911+
}
912+
913+
return null;
914+
}

packages/compiler/src/template/pipeline/src/instruction.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -263,22 +263,23 @@ export function i18nStart(slot: number, constIndex: number, subTemplateIndex: nu
263263
}
264264

265265
export function repeaterCreate(
266-
slot: number, viewFnName: string, decls: number, vars: number, trackByFn: o.Expression,
267-
trackByUsesComponentInstance: boolean, emptyViewFnName: string|null, emptyDecls: number|null,
268-
emptyVars: number|null, sourceSpan: ParseSourceSpan|null): ir.CreateOp {
269-
let args = [
266+
slot: number, viewFnName: string, decls: number, vars: number, tag: string|null,
267+
constIndex: number|null, trackByFn: o.Expression, trackByUsesComponentInstance: boolean,
268+
emptyViewFnName: string|null, emptyDecls: number|null, emptyVars: number|null,
269+
sourceSpan: ParseSourceSpan|null): ir.CreateOp {
270+
const args = [
270271
o.literal(slot),
271272
o.variable(viewFnName),
272273
o.literal(decls),
273274
o.literal(vars),
275+
o.literal(tag),
276+
o.literal(constIndex),
274277
trackByFn,
275278
];
276279
if (trackByUsesComponentInstance || emptyViewFnName !== null) {
277280
args.push(o.literal(trackByUsesComponentInstance));
278281
if (emptyViewFnName !== null) {
279-
args.push(o.variable(emptyViewFnName));
280-
args.push(o.literal(emptyDecls));
281-
args.push(o.literal(emptyVars));
282+
args.push(o.variable(emptyViewFnName), o.literal(emptyDecls), o.literal(emptyVars));
282283
}
283284
}
284285
return call(Identifiers.repeaterCreate, args, sourceSpan);

packages/compiler/src/template/pipeline/src/phases/naming.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {sanitizeIdentifier} from '../../../../parse_util';
1010
import {hyphenate} from '../../../../render3/view/style_parser';
1111
import * as ir from '../../ir';
1212
import {ViewCompilationUnit, type CompilationJob, type CompilationUnit} from '../compilation';
13-
import {prefixWithNamespace} from '../conversion';
1413

1514
/**
1615
* Generate names for functions and variables across all views.
@@ -76,17 +75,13 @@ function addNamesToView(
7675
const emptyView = unit.job.views.get(op.emptyView)!;
7776
// Repeater empty view function is at slot +2 (metadata is in the first slot).
7877
addNamesToView(
79-
emptyView,
80-
`${baseName}_${prefixWithNamespace(`${op.tag}Empty`, op.namespace)}_${
81-
op.slot.slot + 2}`,
78+
emptyView, `${baseName}_${`${op.functionNameSuffix}Empty`}_${op.slot.slot + 2}`,
8279
state, compatibility);
8380
}
84-
const repeaterToken =
85-
op.tag === null ? '' : '_' + prefixWithNamespace(op.tag, op.namespace);
8681
// Repeater primary view function is at slot +1 (metadata is in the first slot).
8782
addNamesToView(
88-
unit.job.views.get(op.xref)!, `${baseName}${repeaterToken}_${op.slot.slot + 1}`, state,
89-
compatibility);
83+
unit.job.views.get(op.xref)!,
84+
`${baseName}_${op.functionNameSuffix}_${op.slot.slot + 1}`, state, compatibility);
9085
break;
9186
case ir.OpKind.Template:
9287
if (!(unit instanceof ViewCompilationUnit)) {
@@ -96,8 +91,8 @@ function addNamesToView(
9691
if (op.slot.slot === null) {
9792
throw new Error(`Expected slot to be assigned`);
9893
}
99-
const tagToken = op.tag === null ? '' : '_' + prefixWithNamespace(op.tag, op.namespace);
100-
addNamesToView(childView, `${baseName}${tagToken}_${op.slot.slot}`, state, compatibility);
94+
const suffix = op.functionNameSuffix.length === 0 ? '' : `_${op.functionNameSuffix}`;
95+
addNamesToView(childView, `${baseName}${suffix}_${op.slot.slot}`, state, compatibility);
10196
break;
10297
case ir.OpKind.StyleProp:
10398
op.name = normalizeStylePropName(op.name);

packages/compiler/src/template/pipeline/src/phases/reify.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function reifyCreateOperations(unit: CompilationUnit, ops: ir.OpList<ir.CreateOp
9898
op,
9999
ng.template(
100100
op.slot.slot!, o.variable(childView.fnName!), childView.decls!, childView.vars!,
101-
op.block ? null : op.tag, op.attributes, op.sourceSpan),
101+
op.tag, op.attributes, op.sourceSpan),
102102
);
103103
break;
104104
case ir.OpKind.DisableBindings:
@@ -220,8 +220,9 @@ function reifyCreateOperations(unit: CompilationUnit, ops: ir.OpList<ir.CreateOp
220220
ir.OpList.replace(
221221
op,
222222
ng.repeaterCreate(
223-
op.slot.slot!, repeaterView.fnName, op.decls!, op.vars!, op.trackByFn!,
224-
op.usesComponentInstance, emptyViewFnName, emptyDecls, emptyVars, op.sourceSpan));
223+
op.slot.slot, repeaterView.fnName, op.decls!, op.vars!, op.tag, op.attributes,
224+
op.trackByFn!, op.usesComponentInstance, emptyViewFnName, emptyDecls, emptyVars,
225+
op.sourceSpan));
225226
break;
226227
case ir.OpKind.Statement:
227228
// Pass statement operations directly through.

0 commit comments

Comments
 (0)