Skip to content

Commit c078820

Browse files
crisbetoalxhub
authored andcommitted
fix(compiler): capture data bindings for content projection purposes in blocks (#54876)
Fixes a regression in the template pipeline where data bindings weren't being captured for content projection purposes. Fixes #54872. PR Close #54876
1 parent 42318e7 commit c078820

File tree

9 files changed

+177
-33
lines changed

9 files changed

+177
-33
lines changed

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,38 +1696,58 @@ export declare class MyApp {
16961696
/****************************************************************************************************
16971697
* PARTIAL FILE: if_element_root_node.js
16981698
****************************************************************************************************/
1699-
import { Component } from '@angular/core';
1699+
import { Component, Directive, Input } from '@angular/core';
17001700
import * as i0 from "@angular/core";
1701+
export class Binding {
1702+
constructor() {
1703+
this.binding = 0;
1704+
}
1705+
}
1706+
Binding.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Binding, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1707+
Binding.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: Binding, isStandalone: true, selector: "[binding]", inputs: { binding: "binding" }, ngImport: i0 });
1708+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Binding, decorators: [{
1709+
type: Directive,
1710+
args: [{ standalone: true, selector: '[binding]' }]
1711+
}], propDecorators: { binding: [{
1712+
type: Input
1713+
}] } });
17011714
export class MyApp {
17021715
constructor() {
17031716
this.expr = true;
17041717
}
17051718
}
17061719
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
1707-
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: `
1720+
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
17081721
@if (expr) {
1709-
<div foo="1" bar="2">{{expr}}</div>
1722+
<div foo="1" bar="2" [binding]="3">{{expr}}</div>
17101723
}
1711-
`, isInline: true });
1724+
`, isInline: true, dependencies: [{ kind: "directive", type: Binding, selector: "[binding]", inputs: ["binding"] }] });
17121725
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
17131726
type: Component,
17141727
args: [{
17151728
template: `
17161729
@if (expr) {
1717-
<div foo="1" bar="2">{{expr}}</div>
1730+
<div foo="1" bar="2" [binding]="3">{{expr}}</div>
17181731
}
17191732
`,
1733+
standalone: true,
1734+
imports: [Binding],
17201735
}]
17211736
}] });
17221737

17231738
/****************************************************************************************************
17241739
* PARTIAL FILE: if_element_root_node.d.ts
17251740
****************************************************************************************************/
17261741
import * as i0 from "@angular/core";
1742+
export declare class Binding {
1743+
binding: number;
1744+
static ɵfac: i0.ɵɵFactoryDeclaration<Binding, never>;
1745+
static ɵdir: i0.ɵɵDirectiveDeclaration<Binding, "[binding]", never, { "binding": { "alias": "binding"; "required": false; }; }, {}, never, never, true, never>;
1746+
}
17271747
export declare class MyApp {
17281748
expr: boolean;
17291749
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
1730-
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
1750+
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
17311751
}
17321752

17331753
/****************************************************************************************************
@@ -1770,42 +1790,62 @@ export declare class MyApp {
17701790
/****************************************************************************************************
17711791
* PARTIAL FILE: for_element_root_node.js
17721792
****************************************************************************************************/
1773-
import { Component } from '@angular/core';
1793+
import { Component, Directive, Input } from '@angular/core';
17741794
import * as i0 from "@angular/core";
1795+
export class Binding {
1796+
constructor() {
1797+
this.binding = 0;
1798+
}
1799+
}
1800+
Binding.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Binding, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1801+
Binding.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: Binding, isStandalone: true, selector: "[binding]", inputs: { binding: "binding" }, ngImport: i0 });
1802+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Binding, decorators: [{
1803+
type: Directive,
1804+
args: [{ standalone: true, selector: '[binding]' }]
1805+
}], propDecorators: { binding: [{
1806+
type: Input
1807+
}] } });
17751808
export class MyApp {
17761809
constructor() {
17771810
this.items = [1, 2, 3];
17781811
}
17791812
}
17801813
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
1781-
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: `
1814+
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
17821815
@for (item of items; track item) {
1783-
<div foo="1" bar="2">{{item}}</div>
1816+
<div foo="1" bar="2" [binding]="3">{{item}}</div>
17841817
} @empty {
1785-
<span empty-foo="1" empty-bar="2">Empty!</span>
1818+
<span empty-foo="1" empty-bar="2" [binding]="3">Empty!</span>
17861819
}
1787-
`, isInline: true });
1820+
`, isInline: true, dependencies: [{ kind: "directive", type: Binding, selector: "[binding]", inputs: ["binding"] }] });
17881821
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
17891822
type: Component,
17901823
args: [{
17911824
template: `
17921825
@for (item of items; track item) {
1793-
<div foo="1" bar="2">{{item}}</div>
1826+
<div foo="1" bar="2" [binding]="3">{{item}}</div>
17941827
} @empty {
1795-
<span empty-foo="1" empty-bar="2">Empty!</span>
1828+
<span empty-foo="1" empty-bar="2" [binding]="3">Empty!</span>
17961829
}
17971830
`,
1831+
standalone: true,
1832+
imports: [Binding],
17981833
}]
17991834
}] });
18001835

18011836
/****************************************************************************************************
18021837
* PARTIAL FILE: for_element_root_node.d.ts
18031838
****************************************************************************************************/
18041839
import * as i0 from "@angular/core";
1840+
export declare class Binding {
1841+
binding: number;
1842+
static ɵfac: i0.ɵɵFactoryDeclaration<Binding, never>;
1843+
static ɵdir: i0.ɵɵDirectiveDeclaration<Binding, "[binding]", never, { "binding": { "alias": "binding"; "required": false; }; }, {}, never, never, true, never>;
1844+
}
18051845
export declare class MyApp {
18061846
items: number[];
18071847
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
1808-
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
1848+
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
18091849
}
18101850

18111851
/****************************************************************************************************

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_element_root_node.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
import {Component} from '@angular/core';
1+
import {Component, Directive, Input} from '@angular/core';
2+
3+
@Directive({standalone: true, selector: '[binding]'})
4+
export class Binding {
5+
@Input() binding = 0;
6+
}
27

38
@Component({
49
template: `
510
@for (item of items; track item) {
6-
<div foo="1" bar="2">{{item}}</div>
11+
<div foo="1" bar="2" [binding]="3">{{item}}</div>
712
} @empty {
8-
<span empty-foo="1" empty-bar="2">Empty!</span>
13+
<span empty-foo="1" empty-bar="2" [binding]="3">Empty!</span>
914
}
1015
`,
16+
standalone: true,
17+
imports: [Binding],
1118
})
1219
export class MyApp {
1320
items = [1, 2, 3];
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
consts: [["foo", "1", "bar", "2"], ["empty-foo", "1", "empty-bar", "2"]]
1+
consts: [["foo", "1", "bar", "2", 3, "binding"], ["empty-foo", "1", "empty-bar", "2", 3, "binding"]]
22
3-
$r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 2, 1, "div", 0, i0.ɵɵrepeaterTrackByIdentity, false, MyApp_ForEmpty_2_Template, 2, 0, "span", 1);
3+
$r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 2, 2, "div", 0, i0.ɵɵrepeaterTrackByIdentity, false, MyApp_ForEmpty_2_Template, 2, 1, "span", 1);

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_element_root_node.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
import {Component} from '@angular/core';
1+
import {Component, Directive, Input} from '@angular/core';
2+
3+
@Directive({standalone: true, selector: '[binding]'})
4+
export class Binding {
5+
@Input() binding = 0;
6+
}
27

38
@Component({
49
template: `
510
@if (expr) {
6-
<div foo="1" bar="2">{{expr}}</div>
11+
<div foo="1" bar="2" [binding]="3">{{expr}}</div>
712
}
813
`,
14+
standalone: true,
15+
imports: [Binding],
916
})
1017
export class MyApp {
1118
expr = true;
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
consts: [["foo", "1", "bar", "2"]]
1+
consts: [["foo", "1", "bar", "2", 3, "binding"]]
22
3-
$r3$.ɵɵtemplate(0, MyApp_Conditional_0_Template, 2, 1, "div", 0);
3+
$r3$.ɵɵtemplate(0, MyApp_Conditional_0_Template, 2, 2, "div", 0);

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/two_way_binding_to_signal_loop_variable_template.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
function TestCmp_For_1_Template(rf, ctx) {
22
if (rf & 1) {
33
const $_r1$ = $r3$.ɵɵgetCurrentView();
4-
$r3$.ɵɵelementStart(0, "input", 0);
4+
$r3$.ɵɵelementStart(0, "input", 1);
55
$r3$.ɵɵtwoWayListener("ngModelChange", function TestCmp_For_1_Template_input_ngModelChange_0_listener($event) {
66
const $name_r2$ = $r3$.ɵɵrestoreView($_r1$).$implicit;
77
$r3$.ɵɵtwoWayBindingSet($name_r2$, $event);
@@ -19,7 +19,7 @@ function TestCmp_For_1_Template(rf, ctx) {
1919

2020
function TestCmp_Template(rf, ctx) {
2121
if (rf & 1) {
22-
$r3$.ɵɵrepeaterCreate(0, TestCmp_For_1_Template, 1, 1, "input", null, $r3$.ɵɵrepeaterTrackByIndex);
22+
$r3$.ɵɵrepeaterCreate(0, TestCmp_For_1_Template, 1, 1, "input", 0, $r3$.ɵɵrepeaterTrackByIndex);
2323
}
2424
if (rf & 2) {
2525
$r3$.ɵɵrepeater(ctx.names);

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ function ingestIfBlock(unit: ViewCompilationUnit, ifBlock: t.IfBlock): void {
351351
cView.contextVariables.set(ifCase.expressionAlias.name, ir.CTX_REF);
352352
}
353353

354-
let ifCaseI18nMeta = undefined;
354+
let ifCaseI18nMeta: i18n.BlockPlaceholder|undefined = undefined;
355355
if (ifCase.i18n !== undefined) {
356356
if (!(ifCase.i18n instanceof i18n.BlockPlaceholder)) {
357357
throw Error(`Unhandled i18n metadata type for if block: ${ifCase.i18n?.constructor.name}`);
@@ -394,7 +394,7 @@ function ingestSwitchBlock(unit: ViewCompilationUnit, switchBlock: t.SwitchBlock
394394
let conditions: Array<ir.ConditionalCaseExpr> = [];
395395
for (const switchCase of switchBlock.cases) {
396396
const cView = unit.job.allocateView(unit.xref);
397-
let switchCaseI18nMeta = undefined;
397+
let switchCaseI18nMeta: i18n.BlockPlaceholder|undefined = undefined;
398398
if (switchCase.i18n !== undefined) {
399399
if (!(switchCase.i18n instanceof i18n.BlockPlaceholder)) {
400400
throw Error(
@@ -1256,18 +1256,28 @@ function ingestControlFlowInsertionPoint(
12561256
}
12571257
}
12581258

1259-
// If we've found a single root node, its tag name and *static* attributes can be copied
1260-
// to the surrounding template to be used for content projection. Note that it's important
1261-
// that we don't copy any bound attributes since they don't participate in content projection
1262-
// and they can be used in directive matching (in the case of `Template.templateAttrs`).
1259+
// If we've found a single root node, its tag name and attributes can be
1260+
// copied to the surrounding template to be used for content projection.
12631261
if (root !== null) {
1262+
// Collect the static attributes for content projection purposes.
12641263
for (const attr of root.attributes) {
12651264
const securityContext = domSchema.securityContext(NG_TEMPLATE_TAG_NAME, attr.name, true);
12661265
unit.update.push(ir.createBindingOp(
12671266
xref, ir.BindingKind.Attribute, attr.name, o.literal(attr.value), null, securityContext,
12681267
true, false, null, asMessage(attr.i18n), attr.sourceSpan));
12691268
}
12701269

1270+
// Also collect the inputs since they participate in content projection as well.
1271+
// Note that TDB used to collect the outputs as well, but it wasn't passing them into
1272+
// the template instruction. Here we just don't collect them.
1273+
for (const attr of root.inputs) {
1274+
if (attr.type !== e.BindingType.Animation && attr.type !== e.BindingType.Attribute) {
1275+
const securityContext = domSchema.securityContext(NG_TEMPLATE_TAG_NAME, attr.name, true);
1276+
unit.create.push(ir.createExtractedAttributeOp(
1277+
xref, ir.BindingKind.Property, null, attr.name, null, null, null, securityContext));
1278+
}
1279+
}
1280+
12711281
const tagName = root instanceof t.Element ? root.name : root.tagName;
12721282

12731283
// Don't pass along `ng-template` tag name since it enables directive matching.

packages/core/test/acceptance/control_flow_for_spec.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
import {NgIf} from '@angular/common';
11-
import {ChangeDetectorRef, Component, Directive, inject, OnInit, Pipe, PipeTransform, TemplateRef, ViewContainerRef} from '@angular/core';
11+
import {ChangeDetectorRef, Component, Directive, inject, Input, OnInit, Pipe, PipeTransform, TemplateRef, ViewContainerRef} from '@angular/core';
1212
import {TestBed} from '@angular/core/testing';
1313

1414
describe('control flow - for', () => {
@@ -482,6 +482,46 @@ describe('control flow - for', () => {
482482
expect(fixture.nativeElement.textContent).toBe('Main: Before one1two1one2two2 After Slot: ');
483483
});
484484

485+
it('should project an @for with a single root node with a data binding', () => {
486+
let directiveCount = 0;
487+
488+
@Directive({standalone: true, selector: '[foo]'})
489+
class Foo {
490+
@Input('foo') value: any;
491+
492+
constructor() {
493+
directiveCount++;
494+
}
495+
}
496+
497+
@Component({
498+
standalone: true,
499+
selector: 'test',
500+
template: 'Main: <ng-content/> Slot: <ng-content select="[foo]"/>',
501+
})
502+
class TestComponent {
503+
}
504+
505+
@Component({
506+
standalone: true,
507+
imports: [TestComponent, Foo],
508+
template: `
509+
<test>Before @for (item of items; track $index) {
510+
<span [foo]="item">{{item}}</span>
511+
} After</test>
512+
`
513+
})
514+
class App {
515+
items = [1, 2, 3];
516+
}
517+
518+
const fixture = TestBed.createComponent(App);
519+
fixture.detectChanges();
520+
521+
expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: 123');
522+
expect(directiveCount).toBe(3);
523+
});
524+
485525
it('should project an @for with an ng-container root node', () => {
486526
@Component({
487527
standalone: true,

packages/core/test/acceptance/control_flow_if_spec.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
import {NgFor} from '@angular/common';
11-
import {ChangeDetectorRef, Component, Directive, inject, OnInit, Pipe, PipeTransform, TemplateRef, ViewContainerRef} from '@angular/core';
11+
import {ChangeDetectorRef, Component, Directive, inject, Input, OnInit, Pipe, PipeTransform, TemplateRef, ViewContainerRef} from '@angular/core';
1212
import {TestBed} from '@angular/core/testing';
1313

1414
// Basic shared pipe used during testing.
@@ -289,6 +289,46 @@ describe('control flow - if', () => {
289289
expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: foo');
290290
});
291291

292+
it('should project an @if with a single root node with a data binding', () => {
293+
let directiveCount = 0;
294+
295+
@Directive({standalone: true, selector: '[foo]'})
296+
class Foo {
297+
@Input('foo') value: any;
298+
299+
constructor() {
300+
directiveCount++;
301+
}
302+
}
303+
304+
@Component({
305+
standalone: true,
306+
selector: 'test',
307+
template: 'Main: <ng-content/> Slot: <ng-content select="[foo]"/>',
308+
})
309+
class TestComponent {
310+
}
311+
312+
@Component({
313+
standalone: true,
314+
imports: [TestComponent, Foo],
315+
template: `
316+
<test>Before @if (true) {
317+
<span [foo]="value">foo</span>
318+
} After</test>
319+
`
320+
})
321+
class App {
322+
value = 1;
323+
}
324+
325+
const fixture = TestBed.createComponent(App);
326+
fixture.detectChanges();
327+
328+
expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: foo');
329+
expect(directiveCount).toBe(1);
330+
});
331+
292332
it('should project an @if with multiple root nodes into the catch-all slot', () => {
293333
@Component({
294334
standalone: true,

0 commit comments

Comments
 (0)