mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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.
This commit is contained in:
committed by
Andrew Kushnir
parent
a7e8abbb7e
commit
18003a33bb
@@ -729,10 +729,12 @@ export class NgSwitchDefault {
|
||||
export class NgTemplateOutlet<C = unknown> implements OnChanges {
|
||||
constructor(_viewContainerRef: ViewContainerRef);
|
||||
// (undocumented)
|
||||
protected injector: Injector;
|
||||
// (undocumented)
|
||||
ngOnChanges(changes: SimpleChanges): void;
|
||||
ngTemplateOutlet: TemplateRef<C> | null | undefined;
|
||||
ngTemplateOutletContext: C | null | undefined;
|
||||
ngTemplateOutletInjector: Injector | null | undefined;
|
||||
ngTemplateOutletInjector: Injector | 'outlet' | null | undefined;
|
||||
// (undocumented)
|
||||
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>;
|
||||
// (undocumented)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import {
|
||||
Directive,
|
||||
EmbeddedViewRef,
|
||||
inject,
|
||||
Injector,
|
||||
Input,
|
||||
OnChanges,
|
||||
@@ -60,8 +61,13 @@ export class NgTemplateOutlet<C = unknown> implements OnChanges {
|
||||
*/
|
||||
@Input() public ngTemplateOutlet: TemplateRef<C> | 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<C = unknown> 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.
|
||||
|
||||
@@ -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<any>;
|
||||
@@ -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 = `
|
||||
<ng-template #tpl><inject-value></inject-value></ng-template>
|
||||
<provide-value>
|
||||
<ng-container *ngTemplateOutlet="tpl; injector: 'outlet'"></ng-container>
|
||||
</provide-value>
|
||||
`;
|
||||
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):
|
||||
// <nesting-counter> 1
|
||||
// <nesting counter> 2
|
||||
// <nesting-counter> 3
|
||||
// <nesting-counter> 4 </nesting-counter>
|
||||
// </nesting-counter>
|
||||
// </nesting-counter>
|
||||
// <nesting-counter> 2 </nesting-counter>
|
||||
// </nesting-counter>
|
||||
const template = `
|
||||
<ng-container *ngTemplateOutlet="node; context: {$implicit: [[[[]]], []]}" />
|
||||
|
||||
<ng-template #node let-data>
|
||||
<nesting-counter>
|
||||
@for (item of data; track $index) {
|
||||
<ng-container *ngTemplateOutlet="node; context: {$implicit: item}; injector: 'outlet'" />
|
||||
}
|
||||
</nesting-counter>
|
||||
</ng-template>
|
||||
`;
|
||||
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: '<ng-content />',
|
||||
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<number>('NESTING_DEPTH');
|
||||
|
||||
@Component({
|
||||
selector: 'nesting-counter',
|
||||
template: '{{depth}}<ng-content />',
|
||||
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[] = [],
|
||||
|
||||
Reference in New Issue
Block a user