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 ae75ec0c9c302..93279976ce460 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 @@ -1689,6 +1689,43 @@ export declare class MyApp { static ɵcmp: i0.ɵɵComponentDeclaration; } +/**************************************************************************************************** + * PARTIAL FILE: if_template_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_template_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 ****************************************************************************************************/ @@ -1726,3 +1763,40 @@ export declare class MyApp { static ɵcmp: i0.ɵɵComponentDeclaration; } +/**************************************************************************************************** + * PARTIAL FILE: for_template_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_template_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 e2518c4c9a557..38c176dcc6feb 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 @@ -531,6 +531,23 @@ } ] }, + { + "description": "should generate an if block with an ng-template root node", + "inputFiles": [ + "if_template_root_node.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "if_template_root_node_template.js", + "generated": "if_template_root_node.js" + } + ], + "failureMessage": "Incorrect template" + } + ] + }, { "description": "should generate a for block with an element root node", "inputFiles": [ @@ -547,6 +564,23 @@ "failureMessage": "Incorrect template" } ] + }, + { + "description": "should generate a for block with an ng-template root node", + "inputFiles": [ + "for_template_root_node.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "for_template_root_node_template.js", + "generated": "for_template_root_node.js" + } + ], + "failureMessage": "Incorrect template" + } + ] } ] } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_root_node.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_root_node.ts new file mode 100644 index 0000000000000..9d47c2a65aa5e --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_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_template_root_node_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_root_node_template.js new file mode 100644 index 0000000000000..d62ceea15f416 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_root_node_template.js @@ -0,0 +1,3 @@ +consts: [["foo", "1", "bar", "2"]] +… +$r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 0, null, 0, i0.ɵɵrepeaterTrackByIdentity); \ No newline at end of file diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_template_root_node.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_template_root_node.ts new file mode 100644 index 0000000000000..52f176a7065b2 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_template_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_template_root_node_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_template_root_node_template.js new file mode 100644 index 0000000000000..55c4fd3af53af --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_template_root_node_template.js @@ -0,0 +1,3 @@ +consts: [["foo", "1", "bar", "2"]] +… +$r3$.ɵɵtemplate(0, MyApp_Conditional_0_Template, 1, 0, null, 0); \ No newline at end of file diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 49dedf2efc565..8906de8b49bfd 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -1508,7 +1508,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // 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; + const name = root instanceof t.Element ? root.name : root.tagName; + // Don't pass along `ng-template` tag name since it enables directive matching. + tagName = name === NG_TEMPLATE_TAG_NAME ? null : name; attrsExprs = this.getAttributeExpressions(NG_TEMPLATE_TAG_NAME, root.attributes, root.inputs, []); } diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index 9ab783aa98cdf..955a1b9321072 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -907,7 +907,10 @@ function ingestControlFlowInsertionPoint( SecurityContext.NONE, attr.sourceSpan, BindingFlags.TextValue); } - return root instanceof t.Element ? root.name : root.tagName; + const tagName = root instanceof t.Element ? root.name : root.tagName; + + // Don't pass along `ng-template` tag name since it enables directive matching. + return tagName === 'ng-template' ? null : tagName; } return null; diff --git a/packages/core/test/acceptance/control_flow_for_spec.ts b/packages/core/test/acceptance/control_flow_for_spec.ts index e3f2457a4d88e..2ae74b9bf3184 100644 --- a/packages/core/test/acceptance/control_flow_for_spec.ts +++ b/packages/core/test/acceptance/control_flow_for_spec.ts @@ -572,5 +572,53 @@ describe('control flow - for', () => { expect(directiveCount).toBe(1); expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: 1'); }); + + it('should invoke a directive on a projected ng-template 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 fc79db9d8586f..3f63f6d9472ee 100644 --- a/packages/core/test/acceptance/control_flow_if_spec.ts +++ b/packages/core/test/acceptance/control_flow_if_spec.ts @@ -582,5 +582,52 @@ describe('control flow - if', () => { expect(directiveCount).toBe(1); expect(fixture.nativeElement.textContent).toBe('Main: Before After Slot: foo'); }); + + it('should invoke a directive on a projected ng-template 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'); + }); }); });