Skip to content

Commit a369f43

Browse files
crisbetodylhunn
authored andcommitted
fix(compiler): capture switch block cases for content projection (#54921)
Captures the individual cases in `switch` blocks for content projection purposes. PR Close #54921
1 parent 7fc7f3f commit a369f43

File tree

11 files changed

+425
-100
lines changed

11 files changed

+425
-100
lines changed

packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {AbsoluteSourceSpan, BindingPipe, PropertyRead, PropertyWrite, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstHoverDeferredTrigger, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstReference, TmplAstTemplate, TmplAstVariable, TmplAstViewportDeferredTrigger} from '@angular/compiler';
9+
import {AbsoluteSourceSpan, BindingPipe, PropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstHoverDeferredTrigger, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstReference, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstVariable, TmplAstViewportDeferredTrigger} from '@angular/compiler';
1010
import ts from 'typescript';
1111

1212
import {ErrorCode, makeDiagnostic, makeRelatedInformation, ngErrorCode} from '../../diagnostics';
@@ -124,7 +124,8 @@ export interface OutOfBandDiagnosticRecorder {
124124
controlFlowPreventingContentProjection(
125125
templateId: TemplateId, category: ts.DiagnosticCategory,
126126
projectionNode: TmplAstElement|TmplAstTemplate, componentName: string, slotSelector: string,
127-
controlFlowNode: TmplAstIfBlockBranch|TmplAstForLoopBlock|TmplAstForLoopBlockEmpty,
127+
controlFlowNode: TmplAstIfBlockBranch|TmplAstSwitchBlockCase|TmplAstForLoopBlock|
128+
TmplAstForLoopBlockEmpty,
128129
preservesWhitespaces: boolean): void;
129130
}
130131

@@ -393,7 +394,8 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
393394
controlFlowPreventingContentProjection(
394395
templateId: TemplateId, category: ts.DiagnosticCategory,
395396
projectionNode: TmplAstElement|TmplAstTemplate, componentName: string, slotSelector: string,
396-
controlFlowNode: TmplAstIfBlockBranch|TmplAstForLoopBlock|TmplAstForLoopBlockEmpty,
397+
controlFlowNode: TmplAstIfBlockBranch|TmplAstSwitchBlockCase|TmplAstForLoopBlock|
398+
TmplAstForLoopBlockEmpty,
397399
preservesWhitespaces: boolean): void {
398400
const blockName = controlFlowNode.nameSpan.toString().trim();
399401
const lines = [

packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -944,7 +944,7 @@ class TcbDomSchemaCheckerOp extends TcbOp {
944944
* A `TcbOp` that finds and flags control flow nodes that interfere with content projection.
945945
*
946946
* Context:
947-
* `@if` and `@for` try to emulate the content projection behavior of `*ngIf` and `*ngFor`
947+
* Control flow blocks try to emulate the content projection behavior of `*ngIf` and `*ngFor`
948948
* in order to reduce breakages when moving from one syntax to the other (see #52414), however the
949949
* approach only works if there's only one element at the root of the control flow expression.
950950
* This means that a stray sibling node (e.g. text) can prevent an element from being projected
@@ -999,7 +999,8 @@ class TcbControlFlowContentProjectionOp extends TcbOp {
999999
}
10001000

10011001
private findPotentialControlFlowNodes() {
1002-
const result: Array<TmplAstIfBlockBranch|TmplAstForLoopBlock|TmplAstForLoopBlockEmpty> = [];
1002+
const result: Array<TmplAstIfBlockBranch|TmplAstSwitchBlockCase|TmplAstForLoopBlock|
1003+
TmplAstForLoopBlockEmpty> = [];
10031004

10041005
for (const child of this.element.children) {
10051006
if (child instanceof TmplAstForLoopBlock) {
@@ -1015,6 +1016,12 @@ class TcbControlFlowContentProjectionOp extends TcbOp {
10151016
result.push(branch);
10161017
}
10171018
}
1019+
} else if (child instanceof TmplAstSwitchBlock) {
1020+
for (const current of child.cases) {
1021+
if (this.shouldCheck(current)) {
1022+
result.push(current);
1023+
}
1024+
}
10181025
}
10191026
}
10201027

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

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2014,6 +2014,152 @@ export declare class MyApp {
20142014
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
20152015
}
20162016

2017+
/****************************************************************************************************
2018+
* PARTIAL FILE: switch_element_root_node.js
2019+
****************************************************************************************************/
2020+
import { Component, Directive, Input } from '@angular/core';
2021+
import * as i0 from "@angular/core";
2022+
export class Binding {
2023+
constructor() {
2024+
this.binding = 0;
2025+
}
2026+
}
2027+
Binding.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Binding, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2028+
Binding.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: Binding, isStandalone: true, selector: "[binding]", inputs: { binding: "binding" }, ngImport: i0 });
2029+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Binding, decorators: [{
2030+
type: Directive,
2031+
args: [{ standalone: true, selector: '[binding]' }]
2032+
}], propDecorators: { binding: [{
2033+
type: Input
2034+
}] } });
2035+
export class MyApp {
2036+
constructor() {
2037+
this.expr = 0;
2038+
}
2039+
}
2040+
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
2041+
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
2042+
@switch (expr) {
2043+
@case (0) {
2044+
<div foo="1" bar="2" [binding]="3">{{expr}}</div>
2045+
}
2046+
@case (1) {
2047+
<div foo="4" bar="5" [binding]="6">{{expr}}</div>
2048+
}
2049+
@default {
2050+
<div foo="7" bar="8" [binding]="9">{{expr}}</div>
2051+
}
2052+
}
2053+
`, isInline: true, dependencies: [{ kind: "directive", type: Binding, selector: "[binding]", inputs: ["binding"] }] });
2054+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
2055+
type: Component,
2056+
args: [{
2057+
template: `
2058+
@switch (expr) {
2059+
@case (0) {
2060+
<div foo="1" bar="2" [binding]="3">{{expr}}</div>
2061+
}
2062+
@case (1) {
2063+
<div foo="4" bar="5" [binding]="6">{{expr}}</div>
2064+
}
2065+
@default {
2066+
<div foo="7" bar="8" [binding]="9">{{expr}}</div>
2067+
}
2068+
}
2069+
`,
2070+
standalone: true,
2071+
imports: [Binding],
2072+
}]
2073+
}] });
2074+
2075+
/****************************************************************************************************
2076+
* PARTIAL FILE: switch_element_root_node.d.ts
2077+
****************************************************************************************************/
2078+
import * as i0 from "@angular/core";
2079+
export declare class Binding {
2080+
binding: number;
2081+
static ɵfac: i0.ɵɵFactoryDeclaration<Binding, never>;
2082+
static ɵdir: i0.ɵɵDirectiveDeclaration<Binding, "[binding]", never, { "binding": { "alias": "binding"; "required": false; }; }, {}, never, never, true, never>;
2083+
}
2084+
export declare class MyApp {
2085+
expr: number;
2086+
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
2087+
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
2088+
}
2089+
2090+
/****************************************************************************************************
2091+
* PARTIAL FILE: switch_template_root_node.js
2092+
****************************************************************************************************/
2093+
import { Component, Directive, Input } from '@angular/core';
2094+
import * as i0 from "@angular/core";
2095+
export class Binding {
2096+
constructor() {
2097+
this.binding = 0;
2098+
}
2099+
}
2100+
Binding.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Binding, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2101+
Binding.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: Binding, isStandalone: true, selector: "[binding]", inputs: { binding: "binding" }, ngImport: i0 });
2102+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Binding, decorators: [{
2103+
type: Directive,
2104+
args: [{ standalone: true, selector: '[binding]' }]
2105+
}], propDecorators: { binding: [{
2106+
type: Input
2107+
}] } });
2108+
export class MyApp {
2109+
constructor() {
2110+
this.expr = 0;
2111+
}
2112+
}
2113+
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
2114+
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
2115+
@switch (expr) {
2116+
@case (0) {
2117+
<ng-template foo="1" bar="2" [binding]="3">{{expr}}</ng-template>
2118+
}
2119+
@case (1) {
2120+
<ng-template foo="4" bar="5" [binding]="6">{{expr}}</ng-template>
2121+
}
2122+
@default {
2123+
<ng-template foo="7" bar="8" [binding]="9">{{expr}}</ng-template>
2124+
}
2125+
}
2126+
`, isInline: true, dependencies: [{ kind: "directive", type: Binding, selector: "[binding]", inputs: ["binding"] }] });
2127+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
2128+
type: Component,
2129+
args: [{
2130+
template: `
2131+
@switch (expr) {
2132+
@case (0) {
2133+
<ng-template foo="1" bar="2" [binding]="3">{{expr}}</ng-template>
2134+
}
2135+
@case (1) {
2136+
<ng-template foo="4" bar="5" [binding]="6">{{expr}}</ng-template>
2137+
}
2138+
@default {
2139+
<ng-template foo="7" bar="8" [binding]="9">{{expr}}</ng-template>
2140+
}
2141+
}
2142+
`,
2143+
standalone: true,
2144+
imports: [Binding],
2145+
}]
2146+
}] });
2147+
2148+
/****************************************************************************************************
2149+
* PARTIAL FILE: switch_template_root_node.d.ts
2150+
****************************************************************************************************/
2151+
import * as i0 from "@angular/core";
2152+
export declare class Binding {
2153+
binding: number;
2154+
static ɵfac: i0.ɵɵFactoryDeclaration<Binding, never>;
2155+
static ɵdir: i0.ɵɵDirectiveDeclaration<Binding, "[binding]", never, { "binding": { "alias": "binding"; "required": false; }; }, {}, never, never, true, never>;
2156+
}
2157+
export declare class MyApp {
2158+
expr: number;
2159+
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
2160+
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
2161+
}
2162+
20172163
/****************************************************************************************************
20182164
* PARTIAL FILE: nested_for_computed_template_variables.js
20192165
****************************************************************************************************/

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,36 @@
541541
}
542542
]
543543
},
544+
{
545+
"description": "should generate a switch block with cases that have element root nodes",
546+
"inputFiles": ["switch_element_root_node.ts"],
547+
"expectations": [
548+
{
549+
"files": [
550+
{
551+
"expected": "switch_element_root_node_template.js",
552+
"generated": "switch_element_root_node.js"
553+
}
554+
],
555+
"failureMessage": "Incorrect template"
556+
}
557+
]
558+
},
559+
{
560+
"description": "should generate a switch block with cases that have ng-template root nodes",
561+
"inputFiles": ["switch_template_root_node.ts"],
562+
"expectations": [
563+
{
564+
"files": [
565+
{
566+
"expected": "switch_template_root_node_template.js",
567+
"generated": "switch_template_root_node.js"
568+
}
569+
],
570+
"failureMessage": "Incorrect template"
571+
}
572+
]
573+
},
544574
{
545575
"description": "should generate computed for loop variables that depend on shadowed $index and $count",
546576
"inputFiles": ["nested_for_computed_template_variables.ts"],
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {Component, Directive, Input} from '@angular/core';
2+
3+
@Directive({standalone: true, selector: '[binding]'})
4+
export class Binding {
5+
@Input() binding = 0;
6+
}
7+
8+
@Component({
9+
template: `
10+
@switch (expr) {
11+
@case (0) {
12+
<div foo="1" bar="2" [binding]="3">{{expr}}</div>
13+
}
14+
@case (1) {
15+
<div foo="4" bar="5" [binding]="6">{{expr}}</div>
16+
}
17+
@default {
18+
<div foo="7" bar="8" [binding]="9">{{expr}}</div>
19+
}
20+
}
21+
`,
22+
standalone: true,
23+
imports: [Binding],
24+
})
25+
export class MyApp {
26+
expr = 0;
27+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
consts: [["foo", "1", "bar", "2", 3, "binding"], ["foo", "4", "bar", "5", 3, "binding"], ["foo", "7", "bar", "8", 3, "binding"]],
2+
3+
$r3$.ɵɵtemplate(0, MyApp_Case_0_Template, 2, 2, "div", 0)(1, MyApp_Case_1_Template, 2, 2, "div", 1)(2, MyApp_Case_2_Template, 2, 2, "div", 2);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {Component, Directive, Input} from '@angular/core';
2+
3+
@Directive({standalone: true, selector: '[binding]'})
4+
export class Binding {
5+
@Input() binding = 0;
6+
}
7+
8+
@Component({
9+
template: `
10+
@switch (expr) {
11+
@case (0) {
12+
<ng-template foo="1" bar="2" [binding]="3">{{expr}}</ng-template>
13+
}
14+
@case (1) {
15+
<ng-template foo="4" bar="5" [binding]="6">{{expr}}</ng-template>
16+
}
17+
@default {
18+
<ng-template foo="7" bar="8" [binding]="9">{{expr}}</ng-template>
19+
}
20+
}
21+
`,
22+
standalone: true,
23+
imports: [Binding],
24+
})
25+
export class MyApp {
26+
expr = 0;
27+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
consts: [["foo", "1", "bar", "2", 3, "binding"], ["foo", "4", "bar", "5", 3, "binding"], ["foo", "7", "bar", "8", 3, "binding"]],
2+
3+
$r3$.ɵɵtemplate(0, MyApp_Case_0_Template, 1, 1, null, 0)(1, MyApp_Case_1_Template, 1, 1, null, 1)(2, MyApp_Case_2_Template, 1, 1, null, 2);

0 commit comments

Comments
 (0)