Skip to content

Commit 18003a3

Browse files
mmalerbaAndrewKushnir
authored andcommitted
feat(common): add an 'outlet' injector option for ngTemplateOutlet
Adds an option (`ngTemplateOutletInjector="outlet"`) that instructs the ngTemplateOutlet to inherit its injector from the outlet's place in the instantiated DOM.
1 parent a7e8abb commit 18003a3

File tree

3 files changed

+91
-5
lines changed

3 files changed

+91
-5
lines changed

goldens/public-api/common/index.api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,10 +729,12 @@ export class NgSwitchDefault {
729729
export class NgTemplateOutlet<C = unknown> implements OnChanges {
730730
constructor(_viewContainerRef: ViewContainerRef);
731731
// (undocumented)
732+
protected injector: Injector;
733+
// (undocumented)
732734
ngOnChanges(changes: SimpleChanges): void;
733735
ngTemplateOutlet: TemplateRef<C> | null | undefined;
734736
ngTemplateOutletContext: C | null | undefined;
735-
ngTemplateOutletInjector: Injector | null | undefined;
737+
ngTemplateOutletInjector: Injector | 'outlet' | null | undefined;
736738
// (undocumented)
737739
static ɵdir: i0.ɵɵDirectiveDeclaration<NgTemplateOutlet<any>, "[ngTemplateOutlet]", never, { "ngTemplateOutletContext": { "alias": "ngTemplateOutletContext"; "required": false; }; "ngTemplateOutlet": { "alias": "ngTemplateOutlet"; "required": false; }; "ngTemplateOutletInjector": { "alias": "ngTemplateOutletInjector"; "required": false; }; }, {}, never, never, true, never>;
738740
// (undocumented)

packages/common/src/directives/ng_template_outlet.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {
1010
Directive,
1111
EmbeddedViewRef,
12+
inject,
1213
Injector,
1314
Input,
1415
OnChanges,
@@ -60,8 +61,13 @@ export class NgTemplateOutlet<C = unknown> implements OnChanges {
6061
*/
6162
@Input() public ngTemplateOutlet: TemplateRef<C> | null | undefined = null;
6263

63-
/** Injector to be used within the embedded view. */
64-
@Input() public ngTemplateOutletInjector: Injector | null | undefined = null;
64+
/**
65+
* Injector to be used within the embedded view. A value of "outlet" can be used to indicate
66+
* that the injector should be inherited from the template outlet's location in the instantiated DOM.
67+
*/
68+
@Input() public ngTemplateOutletInjector: Injector | 'outlet' | null | undefined = null;
69+
70+
protected injector = inject(Injector);
6571

6672
constructor(private _viewContainerRef: ViewContainerRef) {}
6773

@@ -83,11 +89,21 @@ export class NgTemplateOutlet<C = unknown> implements OnChanges {
8389
// without having to destroy and re-create views whenever the context changes.
8490
const viewContext = this._createContextForwardProxy();
8591
this._viewRef = viewContainerRef.createEmbeddedView(this.ngTemplateOutlet, viewContext, {
86-
injector: this.ngTemplateOutletInjector ?? undefined,
92+
injector: this._getInjector(),
8793
});
8894
}
8995
}
9096

97+
/**
98+
* Gets the injector to use for the template outlet based on ngTemplateOutletInjector.
99+
*/
100+
private _getInjector(): Injector | undefined {
101+
if (this.ngTemplateOutletInjector === 'outlet') {
102+
return this.injector;
103+
}
104+
return this.ngTemplateOutletInjector ?? undefined;
105+
}
106+
91107
/**
92108
* We need to re-create existing embedded view if either is true:
93109
* - the outlet changed.

packages/common/test/directives/ng_template_outlet_spec.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {CommonModule, NgTemplateOutlet} from '../../index';
109
import {
1110
Component,
1211
ContentChildren,
@@ -17,12 +16,16 @@ import {
1716
Injector,
1817
NO_ERRORS_SCHEMA,
1918
OnDestroy,
19+
Optional,
2020
Provider,
2121
QueryList,
22+
SkipSelf,
2223
TemplateRef,
24+
inject,
2325
} from '@angular/core';
2426
import {ComponentFixture, TestBed} from '@angular/core/testing';
2527
import {expect} from '@angular/private/testing/matchers';
28+
import {CommonModule, NgTemplateOutlet} from '../../index';
2629

2730
describe('NgTemplateOutlet', () => {
2831
let fixture: ComponentFixture<any>;
@@ -49,6 +52,8 @@ describe('NgTemplateOutlet', () => {
4952
DestroyableCmpt,
5053
MultiContextComponent,
5154
InjectValueComponent,
55+
ProvideValueComponent,
56+
NestingCounter,
5257
],
5358
imports: [CommonModule],
5459
providers: [DestroyedSpyService],
@@ -360,6 +365,43 @@ describe('NgTemplateOutlet', () => {
360365
detectChangesAndExpectText('Hello world');
361366
});
362367

368+
it('should be able to inherit outlet injector', () => {
369+
const template = `
370+
<ng-template #tpl><inject-value></inject-value></ng-template>
371+
<provide-value>
372+
<ng-container *ngTemplateOutlet="tpl; injector: 'outlet'"></ng-container>
373+
</provide-value>
374+
`;
375+
fixture = createTestComponent(template, [{provide: templateToken, useValue: 'root'}]);
376+
detectChangesAndExpectText('Hello provide-value');
377+
});
378+
379+
it('should be able to inherit outlet injector in a deeply nested structure', () => {
380+
// This template should create the following rendered structure
381+
// (Spaces & newlines added for readability):
382+
// <nesting-counter> 1
383+
// <nesting counter> 2
384+
// <nesting-counter> 3
385+
// <nesting-counter> 4 </nesting-counter>
386+
// </nesting-counter>
387+
// </nesting-counter>
388+
// <nesting-counter> 2 </nesting-counter>
389+
// </nesting-counter>
390+
const template = `
391+
<ng-container *ngTemplateOutlet="node; context: {$implicit: [[[[]]], []]}" />
392+
393+
<ng-template #node let-data>
394+
<nesting-counter>
395+
@for (item of data; track $index) {
396+
<ng-container *ngTemplateOutlet="node; context: {$implicit: item}; injector: 'outlet'" />
397+
}
398+
</nesting-counter>
399+
</ng-template>
400+
`;
401+
fixture = createTestComponent(template);
402+
detectChangesAndExpectText('12342');
403+
});
404+
363405
it('should be available as a standalone directive', () => {
364406
@Component({
365407
selector: 'test-component',
@@ -443,6 +485,14 @@ class TestComponent {
443485
injector: Injector | null = null;
444486
}
445487

488+
@Component({
489+
selector: 'provide-value',
490+
template: '<ng-content />',
491+
providers: [{provide: templateToken, useValue: 'provide-value'}],
492+
standalone: false,
493+
})
494+
class ProvideValueComponent {}
495+
446496
@Component({
447497
selector: 'inject-value',
448498
template: 'Hello {{tokenValue}}',
@@ -466,6 +516,24 @@ class MultiContextComponent {
466516
context2: {name: string} | undefined;
467517
}
468518

519+
const NESTING_DEPTH = new InjectionToken<number>('NESTING_DEPTH');
520+
521+
@Component({
522+
selector: 'nesting-counter',
523+
template: '{{depth}}<ng-content />',
524+
providers: [
525+
{
526+
provide: NESTING_DEPTH,
527+
useFactory: (l: number) => (l ? l + 1 : 1),
528+
deps: [[new Optional(), new SkipSelf(), NESTING_DEPTH]],
529+
},
530+
],
531+
standalone: false,
532+
})
533+
class NestingCounter {
534+
depth = inject(NESTING_DEPTH);
535+
}
536+
469537
function createTestComponent(
470538
template: string,
471539
providers: Provider[] = [],

0 commit comments

Comments
 (0)