Skip to content

Commit eb15358

Browse files
crisbetoalxhub
authored andcommitted
fix(compiler): project control flow root elements into correct slot (angular#52414)
With the directive-based control flow users were able to conditionally project content using the `*` syntax. E.g. `<div *ngIf="expr" projectMe></div>` will be projected into `<ng-content select="[projectMe]"/>`, because the attributes and tag name from the `div` are copied to the template via the template creation instruction. With `@if` and `@for` that is not the case, because the conditional is placed *around* elements, rather than *on* them. The result is that content projection won't work in the same way if a user converts from `*ngIf` to `@if`. These changes aim to cover the most common case by doing the same copying when a control flow node has *one and only one* root element or template node. This approach comes with some caveats: 1. As soon as any other node is added to the root, the copying behavior won't work anymore. A diagnostic will be added to flag cases like this and to explain how to work around it. 2. If `preserveWhitespaces` is enabled, it's very likely that indentation will break this workaround, because it'll include an additional text node as the first child. We can work around it here, but in a discussion it was decided not to, because the user explicitly opted into preserving the whitespace and we would have to drop it from the generated code. The diagnostic mentioned point #1 will flag such cases to users. Fixes angular#52277. PR Close angular#52414
1 parent 078ebea commit eb15358

30 files changed

+883
-35
lines changed

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1652,3 +1652,77 @@ export declare class MyApp {
16521652
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
16531653
}
16541654

1655+
/****************************************************************************************************
1656+
* PARTIAL FILE: if_element_root_node.js
1657+
****************************************************************************************************/
1658+
import { Component } from '@angular/core';
1659+
import * as i0 from "@angular/core";
1660+
export class MyApp {
1661+
constructor() {
1662+
this.expr = true;
1663+
}
1664+
}
1665+
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
1666+
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: `
1667+
@if (expr) {
1668+
<div foo="1" bar="2">{{expr}}</div>
1669+
}
1670+
`, isInline: true });
1671+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
1672+
type: Component,
1673+
args: [{
1674+
template: `
1675+
@if (expr) {
1676+
<div foo="1" bar="2">{{expr}}</div>
1677+
}
1678+
`,
1679+
}]
1680+
}] });
1681+
1682+
/****************************************************************************************************
1683+
* PARTIAL FILE: if_element_root_node.d.ts
1684+
****************************************************************************************************/
1685+
import * as i0 from "@angular/core";
1686+
export declare class MyApp {
1687+
expr: boolean;
1688+
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
1689+
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
1690+
}
1691+
1692+
/****************************************************************************************************
1693+
* PARTIAL FILE: for_element_root_node.js
1694+
****************************************************************************************************/
1695+
import { Component } from '@angular/core';
1696+
import * as i0 from "@angular/core";
1697+
export class MyApp {
1698+
constructor() {
1699+
this.items = [1, 2, 3];
1700+
}
1701+
}
1702+
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
1703+
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: `
1704+
@for (item of items; track item) {
1705+
<div foo="1" bar="2">{{item}}</div>
1706+
}
1707+
`, isInline: true });
1708+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
1709+
type: Component,
1710+
args: [{
1711+
template: `
1712+
@for (item of items; track item) {
1713+
<div foo="1" bar="2">{{item}}</div>
1714+
}
1715+
`,
1716+
}]
1717+
}] });
1718+
1719+
/****************************************************************************************************
1720+
* PARTIAL FILE: for_element_root_node.d.ts
1721+
****************************************************************************************************/
1722+
import * as i0 from "@angular/core";
1723+
export declare class MyApp {
1724+
items: number[];
1725+
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
1726+
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
1727+
}
1728+

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/TEST_CASES.json

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,40 @@
513513
"failureMessage": "Incorrect template"
514514
}
515515
]
516+
},
517+
{
518+
"description": "should generate an if block with an element root node",
519+
"inputFiles": [
520+
"if_element_root_node.ts"
521+
],
522+
"expectations": [
523+
{
524+
"files": [
525+
{
526+
"expected": "if_element_root_node_template.js",
527+
"generated": "if_element_root_node.js"
528+
}
529+
],
530+
"failureMessage": "Incorrect template"
531+
}
532+
]
533+
},
534+
{
535+
"description": "should generate a for block with an element root node",
536+
"inputFiles": [
537+
"for_element_root_node.ts"
538+
],
539+
"expectations": [
540+
{
541+
"files": [
542+
{
543+
"expected": "for_element_root_node_template.js",
544+
"generated": "for_element_root_node.js"
545+
}
546+
],
547+
"failureMessage": "Incorrect template"
548+
}
549+
]
516550
}
517551
]
518-
}
552+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ function MyApp_Template(rf, ctx) {
1212
if (rf & 1) {
1313
$r3$.ɵɵelementStart(0, "div");
1414
$r3$.ɵɵtext(1);
15-
$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, $r3$.ɵɵrepeaterTrackByIdentity);
15+
$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $r3$.ɵɵrepeaterTrackByIdentity);
1616
$r3$.ɵɵelementEnd();
1717
}
1818
if (rf & 2) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function MyApp_Template(rf, ctx) {
1313
if (rf & 1) {
1414
$r3$.ɵɵelementStart(0, "div");
1515
$r3$.ɵɵtext(1);
16-
$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 6, $r3$.ɵɵrepeaterTrackByIdentity);
16+
$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 6, null, null, $r3$.ɵɵrepeaterTrackByIdentity);
1717
$r3$.ɵɵelementEnd();
1818
}
1919
if (rf & 2) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
function MyApp_Template(rf, ctx) {
22
if (rf & 1) {
33
$r3$.ɵɵtemplate(0, MyApp_ng_template_0_Template, 0, 0, "ng-template");
4-
$r3$.ɵɵrepeaterCreate(1, MyApp_For_2_Template, 1, 1, $r3$.ɵɵrepeaterTrackByIdentity, false, MyApp_ForEmpty_3_Template, 1, 0);
4+
$r3$.ɵɵrepeaterCreate(1, MyApp_For_2_Template, 1, 1, null, null, $r3$.ɵɵrepeaterTrackByIdentity, false, MyApp_ForEmpty_3_Template, 1, 0);
55
$r3$.ɵɵtemplate(4, MyApp_ng_template_4_Template, 0, 0, "ng-template");
66
}
77
if (rf & 2) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
template: `
5+
@for (item of items; track item) {
6+
<div foo="1" bar="2">{{item}}</div>
7+
}
8+
`,
9+
})
10+
export class MyApp {
11+
items = [1, 2, 3];
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
consts: [["foo", "1", "bar", "2"]]
2+
3+
$r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 2, 1, "div", 0, i0.ɵɵrepeaterTrackByIdentity);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ function $_forTrack0$($index, $item) {
44
55
function MyApp_Template(rf, ctx) {
66
if (rf & 1) {
7-
$r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 1, $_forTrack0$, true);
8-
$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, $_forTrack0$, true);
7+
$r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 1, null, null, $_forTrack0$, true);
8+
$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $_forTrack0$, true);
99
}
1010
if (rf & 2) {
1111
$r3$.ɵɵrepeater(0, ctx.items);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ const $_forTrack0$ = ($index, $item) => $item.name[0].toUpperCase();
22
33
function MyApp_Template(rf, ctx) {
44
if (rf & 1) {
5-
$r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 1, $_forTrack0$);
6-
$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, $_forTrack0$);
5+
$r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 1, null, null, $_forTrack0$);
6+
$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $_forTrack0$);
77
}
88
if (rf & 2) {
99
$r3$.ɵɵrepeater(0, ctx.items);
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
$r3$.ɵɵrepeaterCreate(0, MyApp_ng_template_2_For_1_Template, 0, 0, $r3$.ɵɵcomponentInstance().trackFn);
1+
$r3$.ɵɵrepeaterCreate(0, MyApp_ng_template_2_For_1_Template, 0, 0, null, null, $r3$.ɵɵcomponentInstance().trackFn);

0 commit comments

Comments
 (0)