diff --git a/goldens/public-api/common/index.api.md b/goldens/public-api/common/index.api.md index 3e13fef9a34..f3defb0135d 100644 --- a/goldens/public-api/common/index.api.md +++ b/goldens/public-api/common/index.api.md @@ -729,10 +729,12 @@ export class NgSwitchDefault { export class NgTemplateOutlet implements OnChanges { constructor(_viewContainerRef: ViewContainerRef); // (undocumented) + protected injector: Injector; + // (undocumented) ngOnChanges(changes: SimpleChanges): void; ngTemplateOutlet: TemplateRef | null | undefined; ngTemplateOutletContext: C | null | undefined; - ngTemplateOutletInjector: Injector | null | undefined; + ngTemplateOutletInjector: Injector | 'outlet' | null | undefined; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration, "[ngTemplateOutlet]", never, { "ngTemplateOutletContext": { "alias": "ngTemplateOutletContext"; "required": false; }; "ngTemplateOutlet": { "alias": "ngTemplateOutlet"; "required": false; }; "ngTemplateOutletInjector": { "alias": "ngTemplateOutletInjector"; "required": false; }; }, {}, never, never, true, never>; // (undocumented) diff --git a/packages/common/src/directives/ng_template_outlet.ts b/packages/common/src/directives/ng_template_outlet.ts index 2615a059f3a..3f191fb01f9 100644 --- a/packages/common/src/directives/ng_template_outlet.ts +++ b/packages/common/src/directives/ng_template_outlet.ts @@ -9,6 +9,7 @@ import { Directive, EmbeddedViewRef, + inject, Injector, Input, OnChanges, @@ -60,8 +61,13 @@ export class NgTemplateOutlet implements OnChanges { */ @Input() public ngTemplateOutlet: TemplateRef | null | undefined = null; - /** Injector to be used within the embedded view. */ - @Input() public ngTemplateOutletInjector: Injector | null | undefined = null; + /** + * Injector to be used within the embedded view. A value of "outlet" can be used to indicate + * that the injector should be inherited from the template outlet's location in the instantiated DOM. + */ + @Input() public ngTemplateOutletInjector: Injector | 'outlet' | null | undefined = null; + + protected injector = inject(Injector); constructor(private _viewContainerRef: ViewContainerRef) {} @@ -83,11 +89,21 @@ export class NgTemplateOutlet implements OnChanges { // without having to destroy and re-create views whenever the context changes. const viewContext = this._createContextForwardProxy(); this._viewRef = viewContainerRef.createEmbeddedView(this.ngTemplateOutlet, viewContext, { - injector: this.ngTemplateOutletInjector ?? undefined, + injector: this._getInjector(), }); } } + /** + * Gets the injector to use for the template outlet based on ngTemplateOutletInjector. + */ + private _getInjector(): Injector | undefined { + if (this.ngTemplateOutletInjector === 'outlet') { + return this.injector; + } + return this.ngTemplateOutletInjector ?? undefined; + } + /** * We need to re-create existing embedded view if either is true: * - the outlet changed. diff --git a/packages/common/test/directives/ng_template_outlet_spec.ts b/packages/common/test/directives/ng_template_outlet_spec.ts index c748f5cd229..5de0a425592 100644 --- a/packages/common/test/directives/ng_template_outlet_spec.ts +++ b/packages/common/test/directives/ng_template_outlet_spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import {CommonModule, NgTemplateOutlet} from '../../index'; import { Component, ContentChildren, @@ -17,12 +16,16 @@ import { Injector, NO_ERRORS_SCHEMA, OnDestroy, + Optional, Provider, QueryList, + SkipSelf, TemplateRef, + inject, } from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {expect} from '@angular/private/testing/matchers'; +import {CommonModule, NgTemplateOutlet} from '../../index'; describe('NgTemplateOutlet', () => { let fixture: ComponentFixture; @@ -49,6 +52,8 @@ describe('NgTemplateOutlet', () => { DestroyableCmpt, MultiContextComponent, InjectValueComponent, + ProvideValueComponent, + NestingCounter, ], imports: [CommonModule], providers: [DestroyedSpyService], @@ -360,6 +365,43 @@ describe('NgTemplateOutlet', () => { detectChangesAndExpectText('Hello world'); }); + it('should be able to inherit outlet injector', () => { + const template = ` + + + + + `; + fixture = createTestComponent(template, [{provide: templateToken, useValue: 'root'}]); + detectChangesAndExpectText('Hello provide-value'); + }); + + it('should be able to inherit outlet injector in a deeply nested structure', () => { + // This template should create the following rendered structure + // (Spaces & newlines added for readability): + // 1 + // 2 + // 3 + // 4 + // + // + // 2 + // + const template = ` + + + + + @for (item of data; track $index) { + + } + + + `; + fixture = createTestComponent(template); + detectChangesAndExpectText('12342'); + }); + it('should be available as a standalone directive', () => { @Component({ selector: 'test-component', @@ -443,6 +485,14 @@ class TestComponent { injector: Injector | null = null; } +@Component({ + selector: 'provide-value', + template: '', + providers: [{provide: templateToken, useValue: 'provide-value'}], + standalone: false, +}) +class ProvideValueComponent {} + @Component({ selector: 'inject-value', template: 'Hello {{tokenValue}}', @@ -466,6 +516,24 @@ class MultiContextComponent { context2: {name: string} | undefined; } +const NESTING_DEPTH = new InjectionToken('NESTING_DEPTH'); + +@Component({ + selector: 'nesting-counter', + template: '{{depth}}', + providers: [ + { + provide: NESTING_DEPTH, + useFactory: (l: number) => (l ? l + 1 : 1), + deps: [[new Optional(), new SkipSelf(), NESTING_DEPTH]], + }, + ], + standalone: false, +}) +class NestingCounter { + depth = inject(NESTING_DEPTH); +} + function createTestComponent( template: string, providers: Provider[] = [],