Skip to content

Commit

Permalink
feat(common): Add an inherit injector option for ngTemplateOutlet
Browse files Browse the repository at this point in the history
Adds an option (`ngTemplateOutletInjector="inherit"`) that instructs the
ngTemplateOutlet to inherit its injector from its place in the
instantiated DOM.
  • Loading branch information
mmalerba committed Apr 17, 2024
1 parent e1eae84 commit 0bcd470
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 4 deletions.
27 changes: 23 additions & 4 deletions packages/common/src/directives/ng_template_outlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import {
Directive,
EmbeddedViewRef,
inject,
Injector,
Input,
OnChanges,
SimpleChange,
SimpleChanges,
TemplateRef,
ViewContainerRef,
Expand Down Expand Up @@ -62,8 +62,15 @@ export class NgTemplateOutlet<C = unknown> implements OnChanges {
*/
@Input() public ngTemplateOutlet: TemplateRef<C> | null = null;

/** Injector to be used within the embedded view. */
@Input() public ngTemplateOutletInjector: Injector | null = null;
/**
* Injector to be used within the embedded view. A value of "inherit" can be used to indicate
* that the injector should be inherited from the template's location in the instantiated DOM.
*/
@Input() public ngTemplateOutletInjector: Injector | 'inherit' | null = null;

protected injector = inject(Injector);

private _childInjector: Injector | undefined;

constructor(private _viewContainerRef: ViewContainerRef) {}

Expand All @@ -85,11 +92,23 @@ 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 === 'inherit') {
this._childInjector =
this._childInjector ?? Injector.create({providers: [], parent: this.injector});
return this._childInjector;
}
return this.ngTemplateOutletInjector ?? undefined;
}

/**
* We need to re-create existing embedded view if either is true:
* - the outlet changed.
Expand Down
19 changes: 19 additions & 0 deletions packages/common/test/directives/ng_template_outlet_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe('NgTemplateOutlet', () => {
DestroyableCmpt,
MultiContextComponent,
InjectValueComponent,
ProvideValueComponent,
],
imports: [CommonModule],
providers: [DestroyedSpyService],
Expand Down Expand Up @@ -329,6 +330,17 @@ describe('NgTemplateOutlet', () => {
detectChangesAndExpectText('Hello world');
}));

it('should be able to inherit injector', () => {
const template = `
<ng-template #tpl><inject-value></inject-value></ng-template>
<provide-value>
<ng-container *ngTemplateOutlet="tpl; injector: 'inherit'"></ng-container>
</provide-value>
`;
fixture = createTestComponent(template, [{provide: templateToken, useValue: 'root'}]);
detectChangesAndExpectText('Hello provide-value');
});

it('should be available as a standalone directive', () => {
@Component({
selector: 'test-component',
Expand Down Expand Up @@ -401,6 +413,13 @@ class TestComponent {
injector: Injector | null = null;
}

@Component({
selector: 'provide-value',
template: '<ng-content />',
providers: [{provide: templateToken, useValue: 'provide-value'}],
})
class ProvideValueComponent {}

@Component({
selector: 'inject-value',
template: 'Hello {{tokenValue}}',
Expand Down

0 comments on commit 0bcd470

Please sign in to comment.