diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js index c1f199d0915a9..ae75ec0c9c302 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js @@ -1652,3 +1652,77 @@ export declare class MyApp { static ɵcmp: i0.ɵɵComponentDeclaration; } +/**************************************************************************************************** + * PARTIAL FILE: if_element_root_node.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyApp { + constructor() { + this.expr = true; + } +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: ` + @if (expr) { +
{{expr}}
+ } + `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + template: ` + @if (expr) { +
{{expr}}
+ } + `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: if_element_root_node.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyApp { + expr: boolean; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: for_element_root_node.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyApp { + constructor() { + this.items = [1, 2, 3]; + } +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: ` + @for (item of items; track item) { +
{{item}}
+ } + `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + template: ` + @for (item of items; track item) { +
{{item}}
+ } + `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: for_element_root_node.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyApp { + items: number[]; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/TEST_CASES.json index 408a5e3e11db6..e2518c4c9a557 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/TEST_CASES.json @@ -513,6 +513,40 @@ "failureMessage": "Incorrect template" } ] + }, + { + "description": "should generate an if block with an element root node", + "inputFiles": [ + "if_element_root_node.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "if_element_root_node_template.js", + "generated": "if_element_root_node.js" + } + ], + "failureMessage": "Incorrect template" + } + ] + }, + { + "description": "should generate a for block with an element root node", + "inputFiles": [ + "for_element_root_node.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "for_element_root_node_template.js", + "generated": "for_element_root_node.js" + } + ], + "failureMessage": "Incorrect template" + } + ] } ] -} \ No newline at end of file +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for_template.js index 31c3f65439ac6..fd44ceaa68f43 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for_template.js @@ -12,7 +12,7 @@ function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵtext(1); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, $r3$.ɵɵrepeaterTrackByIdentity); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $r3$.ɵɵrepeaterTrackByIdentity); $r3$.ɵɵelementEnd(); } if (rf & 2) { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables_template.js index 37bb40bff2c88..4fca43229b52a 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables_template.js @@ -13,7 +13,7 @@ function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵtext(1); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 6, $r3$.ɵɵrepeaterTrackByIdentity); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 6, null, null, $r3$.ɵɵrepeaterTrackByIdentity); $r3$.ɵɵelementEnd(); } if (rf & 2) { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots_template.js index 3b8be52e4c5c8..8a8fd335f5727 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots_template.js @@ -1,7 +1,7 @@ function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵtemplate(0, MyApp_ng_template_0_Template, 0, 0, "ng-template"); - $r3$.ɵɵrepeaterCreate(1, MyApp_For_2_Template, 1, 1, $r3$.ɵɵrepeaterTrackByIdentity, false, MyApp_ForEmpty_3_Template, 1, 0); + $r3$.ɵɵrepeaterCreate(1, MyApp_For_2_Template, 1, 1, null, null, $r3$.ɵɵrepeaterTrackByIdentity, false, MyApp_ForEmpty_3_Template, 1, 0); $r3$.ɵɵtemplate(4, MyApp_ng_template_4_Template, 0, 0, "ng-template"); } if (rf & 2) { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_element_root_node.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_element_root_node.ts new file mode 100644 index 0000000000000..06aaa1c307cc4 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_element_root_node.ts @@ -0,0 +1,12 @@ +import {Component} from '@angular/core'; + +@Component({ + template: ` + @for (item of items; track item) { +
{{item}}
+ } + `, +}) +export class MyApp { + items = [1, 2, 3]; +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_element_root_node_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_element_root_node_template.js new file mode 100644 index 0000000000000..dd8338a797cc5 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_element_root_node_template.js @@ -0,0 +1,3 @@ +consts: [["foo", "1", "bar", "2"]] +… +$r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 2, 1, "div", 0, i0.ɵɵrepeaterTrackByIdentity); \ No newline at end of file diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_impure_track_reuse_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_impure_track_reuse_template.js index d379c718fb978..8c0e1e1172a56 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_impure_track_reuse_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_impure_track_reuse_template.js @@ -4,8 +4,8 @@ function $_forTrack0$($index, $item) { … function MyApp_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 1, $_forTrack0$, true); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, $_forTrack0$, true); + $r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 1, null, null, $_forTrack0$, true); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $_forTrack0$, true); } if (rf & 2) { $r3$.ɵɵrepeater(0, ctx.items); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_pure_track_reuse_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_pure_track_reuse_template.js index f6106c45c27ac..9591ce6e32a85 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_pure_track_reuse_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_pure_track_reuse_template.js @@ -2,8 +2,8 @@ const $_forTrack0$ = ($index, $item) => $item.name[0].toUpperCase(); … function MyApp_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 1, $_forTrack0$); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, $_forTrack0$); + $r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 1, null, null, $_forTrack0$); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $_forTrack0$); } if (rf & 2) { $r3$.ɵɵrepeater(0, ctx.items); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_track_method_nested_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_track_method_nested_template.js index 0e6d3e7a51ea7..455b9815c71cd 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_track_method_nested_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_track_method_nested_template.js @@ -1 +1 @@ -$r3$.ɵɵrepeaterCreate(0, MyApp_ng_template_2_For_1_Template, 0, 0, $r3$.ɵɵcomponentInstance().trackFn); +$r3$.ɵɵrepeaterCreate(0, MyApp_ng_template_2_For_1_Template, 0, 0, null, null, $r3$.ɵɵcomponentInstance().trackFn); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_track_method_root_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_track_method_root_template.js index d4060f074cbeb..115571f10f797 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_track_method_root_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_track_method_root_template.js @@ -1 +1 @@ -$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 0, 0, ctx.trackFn); +$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 0, 0, null, null, ctx.trackFn); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener_template.js index 7d92b1a16adf7..45846900ac0b9 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener_template.js @@ -17,7 +17,7 @@ function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵtext(1); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 0, $r3$.ɵɵrepeaterTrackByIdentity); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 0, "div", null, $r3$.ɵɵrepeaterTrackByIdentity); $r3$.ɵɵelementEnd(); } if (rf & 2) { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_scope_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_scope_template.js index 318d36737e95b..a94b4d6936ca8 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_scope_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_scope_template.js @@ -12,7 +12,7 @@ function MyApp_For_2_Template(rf, ctx) { function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵtext(0); - $r3$.ɵɵrepeaterCreate(1, MyApp_For_2_Template, 1, 4, $r3$.ɵɵrepeaterTrackByIdentity); + $r3$.ɵɵrepeaterCreate(1, MyApp_For_2_Template, 1, 4, null, null, $r3$.ɵɵrepeaterTrackByIdentity); $r3$.ɵɵtext(3); } if (rf & 2) { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_template.js index 2b59b289be744..6399e6cddf7ef 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_template.js @@ -13,7 +13,7 @@ function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵtext(1); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 6, $r3$.ɵɵrepeaterTrackByIdentity); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 6, null, null, $r3$.ɵɵrepeaterTrackByIdentity); $r3$.ɵɵelementEnd(); } if (rf & 2) { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field_template.js index 90371fa666bde..52a63bffe47c9 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field_template.js @@ -13,7 +13,7 @@ function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵtext(1); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, $_forTrack0$); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $_forTrack0$); $r3$.ɵɵelementEnd(); } if (rf & 2) { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index_template.js index d34618a703d69..adc50556651dc 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index_template.js @@ -12,7 +12,7 @@ function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵtext(1); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, $r3$.ɵɵrepeaterTrackByIndex); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $r3$.ɵɵrepeaterTrackByIndex); $r3$.ɵɵelementEnd(); } if (rf & 2) { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_literals_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_literals_template.js index 0f9b7bc192442..83c18b778068a 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_literals_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_literals_template.js @@ -7,7 +7,7 @@ function $_forTrack0$($index, $item) { … function MyApp_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 1, $_forTrack0$, true); + $r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 1, null, null, $_forTrack0$, true); } if (rf & 2) { $r3$.ɵɵrepeater(0, ctx.items); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty_template.js index f4f411557d8a0..59273aaa1add2 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty_template.js @@ -18,7 +18,7 @@ function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵtext(1); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, $r3$.ɵɵrepeaterTrackByIdentity, false, MyApp_ForEmpty_4_Template, 1, 0); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $r3$.ɵɵrepeaterTrackByIdentity, false, MyApp_ForEmpty_4_Template, 1, 0); $r3$.ɵɵelementEnd(); } if (rf & 2) { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_pipe_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_pipe_template.js index f48267ae32b85..11a64cfb7d004 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_pipe_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_pipe_template.js @@ -2,7 +2,7 @@ function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵtext(1); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, $r3$.ɵɵrepeaterTrackByIdentity); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $r3$.ɵɵrepeaterTrackByIdentity); $r3$.ɵɵpipe(4, "test"); $r3$.ɵɵelementEnd(); } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_element_root_node.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_element_root_node.ts new file mode 100644 index 0000000000000..7b18ca80b11cb --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_element_root_node.ts @@ -0,0 +1,12 @@ +import {Component} from '@angular/core'; + +@Component({ + template: ` + @if (expr) { +
{{expr}}
+ } + `, +}) +export class MyApp { + expr = true; +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_element_root_node_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_element_root_node_template.js new file mode 100644 index 0000000000000..e45728626643c --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_element_root_node_template.js @@ -0,0 +1,3 @@ +consts: [["foo", "1", "bar", "2"]] +… +$r3$.ɵɵtemplate(0, MyApp_Conditional_0_Template, 2, 1, "div", 0); \ No newline at end of file diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners_template.js index f4b5295d40173..a83502f3eac68 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners_template.js @@ -26,7 +26,7 @@ function MyApp_Conditional_0_Conditional_1_Template(rf, ctx) { return $r3$.ɵɵresetView($ctx_r10$.log($ctx_r10$.value(), $root_r1$, $inner_r3$)); }); $r3$.ɵɵelementEnd(); - $r3$.ɵɵtemplate(1, MyApp_Conditional_0_Conditional_1_Conditional_1_Template, 1, 0); + $r3$.ɵɵtemplate(1, MyApp_Conditional_0_Conditional_1_Conditional_1_Template, 1, 0, "button"); } if (rf & 2) { const $ctx_r2$ = $r3$.ɵɵnextContext(2); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners_template.pipeline.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners_template.pipeline.js index 8f9c6c5799991..52123af67115e 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners_template.pipeline.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners_template.pipeline.js @@ -24,7 +24,7 @@ function MyApp_Conditional_0_Conditional_1_Conditional_1_Template(rf, ctx) { return $r3$.ɵɵresetView($ctx_r10$.log($ctx_r10$.value(), $root_r1$, $inner_r3$)); }); $r3$.ɵɵelementEnd(); - $r3$.ɵɵtemplate(1, MyApp_Conditional_0_Conditional_1_Conditional_1_Template, 1, 0); + $r3$.ɵɵtemplate(1, MyApp_Conditional_0_Conditional_1_Conditional_1_Template, 1, 0, "button"); } if (rf & 2) { let $MyApp_Conditional_0_Conditional_1_contFlowTmp$; diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template.js index bd60b00b9577f..0f82c2acdbe49 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template.js @@ -12,7 +12,7 @@ function MyApp_For_3_For_2_Template(rf, ctx) { function MyApp_For_3_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵtext(0); - $r3$.ɵɵrepeaterCreate(1, MyApp_For_3_For_2_Template, 1, 2, $r3$.ɵɵrepeaterTrackByIndex); + $r3$.ɵɵrepeaterCreate(1, MyApp_For_3_For_2_Template, 1, 2, null, null, $r3$.ɵɵrepeaterTrackByIndex); } if (rf & 2) { const $item_r1$ = ctx.$implicit; @@ -25,7 +25,7 @@ function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵtext(1); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 3, 1, $r3$.ɵɵrepeaterTrackByIdentity); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 3, 1, null, null, $r3$.ɵɵrepeaterTrackByIdentity); $r3$.ɵɵelementEnd(); } if (rf & 2) { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables_template.js index 40a371c87b6ae..12bbc9d44a88c 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables_template.js @@ -12,7 +12,7 @@ function MyApp_For_3_For_2_Template(rf, ctx) { function MyApp_For_3_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵtext(0); - $r3$.ɵɵrepeaterCreate(1, MyApp_For_3_For_2_Template, 1, 2, $r3$.ɵɵrepeaterTrackByIdentity); + $r3$.ɵɵrepeaterCreate(1, MyApp_For_3_For_2_Template, 1, 2, null, null, $r3$.ɵɵrepeaterTrackByIdentity); } if (rf & 2) { const $item_r1$ = ctx.$implicit; @@ -25,7 +25,7 @@ function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵtext(1); - $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 3, 1, $r3$.ɵɵrepeaterTrackByIdentity); + $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 3, 1, null, null, $r3$.ɵɵrepeaterTrackByIdentity); $r3$.ɵɵelementEnd(); } if (rf & 2) { diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 84740788924a6..49dedf2efc565 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -53,6 +53,9 @@ const NG_PROJECT_AS_ATTR_NAME = 'ngProjectAs'; // Global symbols available only inside event bindings. const EVENT_BINDING_SCOPE_GLOBALS = new Set(['$event']); +// Tag name of the `ng-template` element. +const NG_TEMPLATE_TAG_NAME = 'ng-template'; + // List of supported global targets for event listeners const GLOBAL_TARGET_RESOLVERS = new Map( [['window', R3.resolveWindow], ['document', R3.resolveDocument], ['body', R3.resolveBody]]); @@ -998,7 +1001,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const tagNameWithoutNamespace = template.tagName ? splitNsName(template.tagName)[1] : template.tagName; const contextNameSuffix = template.tagName ? '_' + sanitizeIdentifier(template.tagName) : ''; - const NG_TEMPLATE_TAG_NAME = 'ng-template'; // prepare attributes parameter (including attributes used for directive matching) const attrsExprs: o.Expression[] = this.getAttributeExpressions( @@ -1148,7 +1150,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // We have to process the block in two steps: once here and again in the update instruction // callback in order to generate the correct expressions when pipes or pure functions are // used inside the branch expressions. - const branchData = block.branches.map(({expression, expressionAlias, children, sourceSpan}) => { + const branchData = block.branches.map((branch, branchIndex) => { + const {expression, expressionAlias, children, sourceSpan} = branch; + // If the branch has an alias, it'll be assigned directly to the container's context. // We define a variable referring directly to the context so that any nested usages can be // rewritten to refer to it. @@ -1158,13 +1162,24 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver expressionAlias.keySpan)] : undefined; + let tagName: string|null = null; + let attrsExprs: o.Expression[]|undefined; + + // Only the first branch can be used for projection, because the conditional + // uses the container of the first branch as the insertion point for all branches. + if (branchIndex === 0) { + const inferredData = this.inferProjectionDataFromInsertionPoint(branch); + tagName = inferredData.tagName; + attrsExprs = inferredData.attrsExprs; + } + // Note: the template needs to be created *before* we process the expression, // otherwise pipes injecting some symbols won't work (see #52102). - const index = - this.createEmbeddedTemplateFn(null, children, '_Conditional', sourceSpan, variables); + const templateIndex = this.createEmbeddedTemplateFn( + tagName, children, '_Conditional', sourceSpan, variables, attrsExprs); const processedExpression = expression === null ? null : expression.visit(this._valueConverter); - return {index, expression: processedExpression, alias: expressionAlias}; + return {index: templateIndex, expression: processedExpression, alias: expressionAlias}; }); // Use the index of the first block as the index for the entire container. @@ -1460,6 +1475,47 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); } + /** + * Infers the data used for content projection (tag name and attributes) from the content of a + * node. + * @param node Node for which to infer the projection data. + */ + private inferProjectionDataFromInsertionPoint(node: t.IfBlockBranch|t.ForLoopBlock) { + let root: t.Element|t.Template|null = null; + let tagName: string|null = null; + let attrsExprs: o.Expression[]|undefined; + + for (const child of node.children) { + // Skip over comment nodes. + if (child instanceof t.Comment) { + continue; + } + + // We can only infer the tag name/attributes if there's a single root node. + if (root !== null) { + root = null; + break; + } + + // Root nodes can only elements or templates with a tag name (e.g. `
`). + if (child instanceof t.Element || (child instanceof t.Template && child.tagName !== null)) { + root = child; + } + } + + // If we've found a single root node, its tag name and *static* attributes can be copied + // to the surrounding template to be used for content projection. Note that it's important + // that we don't copy any bound attributes since they don't participate in content projection + // and they can be used in directive matching (in the case of `Template.templateAttrs`). + if (root !== null) { + tagName = root instanceof t.Element ? root.name : root.tagName; + attrsExprs = + this.getAttributeExpressions(NG_TEMPLATE_TAG_NAME, root.attributes, root.inputs, []); + } + + return {tagName, attrsExprs}; + } + private allocateDataSlot() { return this._dataIndex++; } @@ -1468,6 +1524,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Allocate one slot for the repeater metadata. The slots for the primary and empty block // are implicitly inferred by the runtime to index + 1 and index + 2. const blockIndex = this.allocateDataSlot(); + const {tagName, attrsExprs} = this.inferProjectionDataFromInsertionPoint(block); const primaryData = this.prepareEmbeddedTemplateFn( block.children, '_For', [block.item, block.contextVariables.$index, block.contextVariables.$count]); @@ -1490,6 +1547,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver o.variable(primaryData.name), o.literal(primaryData.getConstCount()), o.literal(primaryData.getVarCount()), + o.literal(tagName), + this.addAttrsToConsts(attrsExprs || null), trackByExpression, ]; diff --git a/packages/core/src/render3/instructions/control_flow.ts b/packages/core/src/render3/instructions/control_flow.ts index 78fa79006f284..2c44e9ad8fcb6 100644 --- a/packages/core/src/render3/instructions/control_flow.ts +++ b/packages/core/src/render3/instructions/control_flow.ts @@ -125,6 +125,8 @@ class RepeaterMetadata { * @param templateFn Reference to the template of the main repeater block. * @param decls The number of nodes, local refs, and pipes for the main block. * @param vars The number of bindings for the main block. + * @param tagName The name of the container element, if applicable + * @param attrsIndex Index of template attributes in the `consts` array. * @param trackByFn Reference to the tracking function. * @param trackByUsesComponentInstance Whether the tracking function has any references to the * component instance. If it doesn't, we can avoid rebinding it. @@ -136,10 +138,10 @@ class RepeaterMetadata { */ export function ɵɵrepeaterCreate( index: number, templateFn: ComponentTemplate, decls: number, vars: number, - trackByFn: TrackByFunction, trackByUsesComponentInstance?: boolean, - emptyTemplateFn?: ComponentTemplate, emptyDecls?: number, emptyVars?: number): void { + tagName: string|null, attrsIndex: number|null, trackByFn: TrackByFunction, + trackByUsesComponentInstance?: boolean, emptyTemplateFn?: ComponentTemplate, + emptyDecls?: number, emptyVars?: number): void { performance.mark('mark_use_counter', PERF_MARK_CONTROL_FLOW); - const hasEmptyBlock = emptyTemplateFn !== undefined; const hostLView = getLView(); const boundTrackBy = trackByUsesComponentInstance ? @@ -150,7 +152,7 @@ export function ɵɵrepeaterCreate( const metadata = new RepeaterMetadata(hasEmptyBlock, boundTrackBy); hostLView[HEADER_OFFSET + index] = metadata; - ɵɵtemplate(index + 1, templateFn, decls, vars); + ɵɵtemplate(index + 1, templateFn, decls, vars, tagName, attrsIndex); if (hasEmptyBlock) { ngDevMode && diff --git a/packages/core/test/acceptance/control_flow_for_spec.ts b/packages/core/test/acceptance/control_flow_for_spec.ts index 1666ba84fb4b8..e3f2457a4d88e 100644 --- a/packages/core/test/acceptance/control_flow_for_spec.ts +++ b/packages/core/test/acceptance/control_flow_for_spec.ts @@ -7,7 +7,8 @@ */ -import {ChangeDetectorRef, Component, inject, Pipe, PipeTransform} from '@angular/core'; +import {NgIf} from '@angular/common'; +import {ChangeDetectorRef, Component, Directive, inject, OnInit, Pipe, PipeTransform, TemplateRef, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; describe('control flow - for', () => { @@ -277,4 +278,299 @@ describe('control flow - for', () => { expect(fixture.nativeElement.textContent).toBe('5(0)|3(1)|7(2)|'); }); }); + + describe('content projection', () => { + it('should project an @for with a single root node into the root node slot', () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + template: ` + Before @for (item of items; track $index) { + {{item}} + } After + ` + }) + class App { + items = [1, 2, 3]; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: 123'); + }); + + it('should project an @for with multiple root nodes into the catch-all slot', () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + template: ` + Before @for (item of items; track $index) { + one{{item}} +
two{{item}}
+ } After
+ ` + }) + class App { + items = [1, 2]; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Main: Before one1two1one2two2 After Slot: '); + }); + + // Right now the template compiler doesn't collect comment nodes. + // This test is to ensure that we don't regress if it happens in the future. + it('should project an @for with single root node and comments into the root node slot', () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + template: ` + Before @for (item of items; track $index) { + + {{item}} + + } After + ` + }) + class App { + items = [1, 2, 3]; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: 123'); + }); + + it('should project the root node when preserveWhitespaces is enabled and there are no whitespace nodes', + () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + preserveWhitespaces: true, + // Note the whitespace due to the indentation inside @for. + template: + 'Before @for (item of items; track $index) {{{item}}} After' + }) + class App { + items = [1, 2, 3]; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: 123'); + }); + + it('should not project the root node when preserveWhitespaces is enabled and there are whitespace nodes', + () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + preserveWhitespaces: true, + // Note the whitespace due to the indentation inside @for. + template: ` + Before @for (item of items; track $index) { + {{item}} + } After + ` + }) + class App { + items = [1, 2, 3]; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent) + .toMatch(/Main: Before\s+1\s+2\s+3\s+After Slot:/); + }); + + it('should not project the root node across multiple layers of @for', () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + template: ` + Before @for (item of items; track $index) { + @for (item of items; track $index) { + {{item}} + } + } After + ` + }) + class App { + items = [1, 2]; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('Main: Before 1212 After Slot: '); + }); + + it('should project an @for with a single root template node into the root node slot', () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent, NgIf], + template: `Before @for (item of items; track $index) { + {{item}} + } After` + }) + class App { + items = [1, 2]; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: 12'); + + fixture.componentInstance.items.push(3); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: 123'); + }); + + it('should invoke a projected attribute directive at the root of an @for once', () => { + let directiveCount = 0; + + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Directive({ + selector: '[foo]', + standalone: true, + }) + class FooDirective { + constructor() { + directiveCount++; + } + } + + @Component({ + standalone: true, + imports: [TestComponent, FooDirective], + template: `Before @for (item of items; track $index) { + {{item}} + } After + ` + }) + class App { + items = [1]; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(directiveCount).toBe(1); + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: 1'); + }); + + it('should invoke a projected template directive at the root of an @for once', () => { + let directiveCount = 0; + + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Directive({ + selector: '[templateDir]', + standalone: true, + }) + class TemplateDirective implements OnInit { + constructor( + private viewContainerRef: ViewContainerRef, + private templateRef: TemplateRef, + ) { + directiveCount++; + } + + ngOnInit(): void { + const view = this.viewContainerRef.createEmbeddedView(this.templateRef); + this.viewContainerRef.insert(view); + } + } + + @Component({ + standalone: true, + imports: [TestComponent, TemplateDirective], + template: `Before @for (item of items; track $index) { + {{item}} + } After + ` + }) + class App { + items = [1]; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(directiveCount).toBe(1); + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: 1'); + }); + }); }); diff --git a/packages/core/test/acceptance/control_flow_if_spec.ts b/packages/core/test/acceptance/control_flow_if_spec.ts index 56bf414d5498b..fc79db9d8586f 100644 --- a/packages/core/test/acceptance/control_flow_if_spec.ts +++ b/packages/core/test/acceptance/control_flow_if_spec.ts @@ -7,7 +7,8 @@ */ -import {ChangeDetectorRef, Component, inject, Pipe, PipeTransform} from '@angular/core'; +import {NgFor} from '@angular/common'; +import {ChangeDetectorRef, Component, Directive, inject, OnInit, Pipe, PipeTransform, TemplateRef, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; // Basic shared pipe used during testing. @@ -259,4 +260,327 @@ describe('control flow - if', () => { fixture.detectChanges(); expect(fixture.nativeElement.textContent).toBe('Something'); }); + + describe('content projection', () => { + it('should project an @if with a single root node into the root node slot', () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + template: ` + Before @if (true) { + foo + } After + ` + }) + class App { + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: foo'); + }); + + it('should project an @if with multiple root nodes into the catch-all slot', () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + template: ` + Before @if (true) { + one +
two
+ } After
+ ` + }) + class App { + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Main: Before onetwo After Slot: '); + }); + + // Right now the template compiler doesn't collect comment nodes. + // This test is to ensure that we don't regress if it happens in the future. + it('should project an @if with a single root node and comments into the root node slot', () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + template: ` + Before @if (true) { + + foo + + } After + ` + }) + class App { + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: foo'); + }); + + // Note: the behavior in this test is *not* intuitive, but it's meant to capture + // the projection behavior from `*ngIf` with `@if`. The test can be updated if we + // change how content projection works in the future. + it('should project an @else content into the slot of @if', () => { + @Component({ + standalone: true, + selector: 'test', + template: + 'Main: One: Two: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + template: ` + Before @if (value) { + one + } @else { + two + } After + ` + }) + class App { + value = true; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Main: Before After One: one Two: '); + + fixture.componentInstance.value = false; + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Main: Before After One: two Two: '); + }); + + it('should project the root node when preserveWhitespaces is enabled and there are no whitespace nodes', + () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + preserveWhitespaces: true, + template: 'Before @if (true) {one} After' + }) + class App { + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: one'); + }); + + it('should not project the root node when preserveWhitespaces is enabled and there are whitespace nodes', + () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + preserveWhitespaces: true, + // Note the whitespace due to the indentation inside @if. + template: ` + Before @if (true) { + one + } After + ` + }) + class App { + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toMatch(/Main: Before\s+one\s+After Slot:/); + }); + + it('should not project the root node across multiple layers of @if', () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + template: ` + Before @if (true) { + @if (true) { + one + } + } After + ` + }) + class App { + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toMatch(/Main: Before\s+one\s+After Slot:/); + }); + + it('should project an @if with a single root template node into the root node slot', () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent, NgFor], + template: `Before @if (true) { + {{item}} + } After` + }) + class App { + items = [1, 2]; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: 12'); + + fixture.componentInstance.items.push(3); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: 123'); + }); + + it('should invoke a projected attribute directive at the root of an @if once', () => { + let directiveCount = 0; + + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Directive({ + selector: '[foo]', + standalone: true, + }) + class FooDirective { + constructor() { + directiveCount++; + } + } + + @Component({ + standalone: true, + imports: [TestComponent, FooDirective], + template: `Before @if (true) { + foo + } After + ` + }) + class App { + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(directiveCount).toBe(1); + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: foo'); + }); + + it('should invoke a projected template directive at the root of an @if once', () => { + let directiveCount = 0; + + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Directive({ + selector: '[templateDir]', + standalone: true, + }) + class TemplateDirective implements OnInit { + constructor( + private viewContainerRef: ViewContainerRef, + private templateRef: TemplateRef, + ) { + directiveCount++; + } + + ngOnInit(): void { + const view = this.viewContainerRef.createEmbeddedView(this.templateRef); + this.viewContainerRef.insert(view); + } + } + + @Component({ + standalone: true, + imports: [TestComponent, TemplateDirective], + template: `Before @if (true) { + foo + } After + ` + }) + class App { + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(directiveCount).toBe(1); + expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: foo'); + }); + }); }); diff --git a/packages/core/test/acceptance/control_flow_switch_spec.ts b/packages/core/test/acceptance/control_flow_switch_spec.ts index d002e7ac1ac3c..134e799139d1c 100644 --- a/packages/core/test/acceptance/control_flow_switch_spec.ts +++ b/packages/core/test/acceptance/control_flow_switch_spec.ts @@ -135,4 +135,33 @@ describe('control flow - switch', () => { fixture.detectChanges(); expect(fixture.nativeElement.textContent).toBe('One'); }); + + it('should project an @switch block into the catch-all slot', () => { + @Component({ + standalone: true, + selector: 'test', + template: 'Main: Slot: ', + }) + class TestComponent { + } + + @Component({ + standalone: true, + imports: [TestComponent], + template: ` + Before @switch (1) { + @case (1) { + foo + } + } After + ` + }) + class App { + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Main: Before foo After Slot: '); + }); });