feat(core): allow for injector to be specified when creating an embedded view (#44666)

Adds support for passing in an optional injector when creating an embedded view through `ViewContainerRef.createEmbeddedView` and `TemplateRef.createEmbeddedView`. The injector allows for the DI behavior to be customized within the specific template.

Fixes #14935.

PR Close #44666
This commit is contained in:
Kristiyan Kostadinov 2022-01-19 09:28:18 +01:00 committed by Dylan Hunn
parent fb27867ab8
commit b49ffcd50e
11 changed files with 716 additions and 34 deletions

View file

@ -1237,7 +1237,7 @@ export type StaticProvider = ValueProvider | ExistingProvider | StaticClassProvi
// @public
export abstract class TemplateRef<C> {
abstract createEmbeddedView(context: C): EmbeddedViewRef<C>;
abstract createEmbeddedView(context: C, injector?: Injector): EmbeddedViewRef<C>;
abstract readonly elementRef: ElementRef;
}
@ -1383,6 +1383,10 @@ export abstract class ViewContainerRef {
}): ComponentRef<C>;
// @deprecated
abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModuleRef?: NgModuleRef<any>): ComponentRef<C>;
abstract createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, options?: {
index?: number;
injector?: Injector;
}): EmbeddedViewRef<C>;
abstract createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number): EmbeddedViewRef<C>;
abstract detach(index?: number): ViewRef | null;
abstract get element(): ElementRef;

View file

@ -6,10 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Injector} from '../di/injector';
import {assertLContainer} from '../render3/assert';
import {ChainedInjector} from '../render3/chained_injector';
import {createLView, renderView} from '../render3/instructions/shared';
import {TContainerNode, TNode, TNodeType} from '../render3/interfaces/node';
import {DECLARATION_LCONTAINER, LView, LViewFlags, QUERIES, TView} from '../render3/interfaces/view';
import {DECLARATION_LCONTAINER, INJECTOR, LView, LViewFlags, QUERIES, TView} from '../render3/interfaces/view';
import {getCurrentTNode, getLView} from '../render3/state';
import {ViewRef as R3_ViewRef} from '../render3/view_ref';
import {assertDefined} from '../util/assert';
@ -55,9 +57,10 @@ export abstract class TemplateRef<C> {
* and attaches it to the view container.
* @param context The data-binding context of the embedded view, as declared
* in the `<ng-template>` usage.
* @param injector Injector to be used within the embedded view.
* @returns The new embedded view object.
*/
abstract createEmbeddedView(context: C): EmbeddedViewRef<C>;
abstract createEmbeddedView(context: C, injector?: Injector): EmbeddedViewRef<C>;
/**
* @internal
@ -77,11 +80,12 @@ const R3TemplateRef = class TemplateRef<T> extends ViewEngineTemplateRef<T> {
super();
}
override createEmbeddedView(context: T): EmbeddedViewRef<T> {
override createEmbeddedView(context: T, injector?: Injector): EmbeddedViewRef<T> {
const embeddedTView = this._declarationTContainer.tViews as TView;
const embeddedLView = createLView(
this._declarationLView, embeddedTView, context, LViewFlags.CheckAlways, null,
embeddedTView.declTNode, null, null, null, null);
embeddedTView.declTNode, null, null, null,
createEmbeddedViewInjector(injector, this._declarationLView[INJECTOR]));
const declarationLContainer = this._declarationLView[this._declarationTContainer.index];
ngDevMode && assertLContainer(declarationLContainer);
@ -98,6 +102,18 @@ const R3TemplateRef = class TemplateRef<T> extends ViewEngineTemplateRef<T> {
}
};
function createEmbeddedViewInjector(
embeddedViewInjector: Injector|undefined, declarationViewInjector: Injector|null): Injector|
null {
if (!embeddedViewInjector) {
return null;
}
return declarationViewInjector ?
new ChainedInjector(embeddedViewInjector, declarationViewInjector) :
embeddedViewInjector;
}
/**
* Creates a TemplateRef given a node.
*

View file

@ -90,6 +90,24 @@ export abstract class ViewContainerRef {
*/
abstract get length(): number;
/**
* Instantiates an embedded view and inserts it
* into this container.
* @param templateRef The HTML template that defines the view.
* @param context The data-binding context of the embedded view, as declared
* in the `<ng-template>` usage.
* @param options Extra configuration for the created view. Includes:
* * index: The 0-based index at which to insert the new view into this container.
* If not specified, appends the new view as the last entry.
* * injector: Injector to be used within the embedded view.
*
* @returns The `ViewRef` instance for the newly created view.
*/
abstract createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, options?: {
index?: number,
injector?: Injector
}): EmbeddedViewRef<C>;
/**
* Instantiates an embedded view and inserts it
* into this container.
@ -258,9 +276,27 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
return this._lContainer.length - CONTAINER_HEADER_OFFSET;
}
override createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, options?: {
index?: number,
injector?: Injector
}): EmbeddedViewRef<C>;
override createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number):
EmbeddedViewRef<C> {
const viewRef = templateRef.createEmbeddedView(context || <any>{});
EmbeddedViewRef<C>;
override createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, indexOrOptions?: number|{
index?: number,
injector?: Injector
}): EmbeddedViewRef<C> {
let index: number|undefined;
let injector: Injector|undefined;
if (typeof indexOrOptions === 'number') {
index = indexOrOptions;
} else if (indexOrOptions != null) {
index = indexOrOptions.index;
injector = indexOrOptions.injector;
}
const viewRef = templateRef.createEmbeddedView(context || <any>{}, injector);
this.insert(viewRef, index);
return viewRef;
}

View file

@ -0,0 +1,36 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector} from '../di/injector';
import {InjectFlags} from '../di/interface/injector';
import {ProviderToken} from '../di/provider_token';
import {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '../view/provider_flags';
/**
* Injector that looks up a value using a specific injector, before falling back to the module
* injector. Used primarily when creating components or embedded views dynamically.
*/
export class ChainedInjector implements Injector {
constructor(private injector: Injector, private parentInjector: Injector) {}
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T {
const value = this.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as T, flags);
if (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR ||
notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) {
// Return the value from the root element injector when
// - it provides it
// (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
// - the module injector should not be checked
// (notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
return value;
}
return this.parentInjector.get(token, notFoundValue, flags);
}
}

View file

@ -9,8 +9,6 @@
import {ChangeDetectorRef as ViewEngine_ChangeDetectorRef} from '../change_detection/change_detector_ref';
import {InjectionToken} from '../di/injection_token';
import {Injector} from '../di/injector';
import {InjectFlags} from '../di/interface/injector';
import {ProviderToken} from '../di/provider_token';
import {Type} from '../interface/type';
import {ComponentFactory as viewEngine_ComponentFactory, ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory';
import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver';
@ -19,8 +17,9 @@ import {NgModuleRef as viewEngine_NgModuleRef} from '../linker/ng_module_factory
import {RendererFactory2} from '../render/api';
import {Sanitizer} from '../sanitization/sanitizer';
import {VERSION} from '../version';
import {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '../view/provider_flags';
import {assertComponentType} from './assert';
import {ChainedInjector} from './chained_injector';
import {createRootComponent, createRootComponentView, createRootContext, LifecycleHooksFeature} from './component';
import {getComponentDef} from './definition';
import {NodeInjector} from './di';
@ -79,26 +78,6 @@ export const SCHEDULER = new InjectionToken<((fn: () => void) => void)>('SCHEDUL
factory: () => defaultScheduler,
});
function createChainedInjector(rootViewInjector: Injector, moduleInjector: Injector): Injector {
return {
get: <T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T => {
const value = rootViewInjector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as T, flags);
if (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR ||
notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) {
// Return the value from the root element injector when
// - it provides it
// (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
// - the module injector should not be checked
// (notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
return value;
}
return moduleInjector.get(token, notFoundValue, flags);
}
};
}
/**
* Render3 implementation of {@link viewEngine_ComponentFactory}.
*/
@ -135,11 +114,11 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
ngModule?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<T> {
ngModule = ngModule || this.ngModule;
const rootViewInjector =
ngModule ? createChainedInjector(injector, ngModule.injector) : injector;
const rootViewInjector = ngModule ? new ChainedInjector(injector, ngModule.injector) : injector;
const rendererFactory =
rootViewInjector.get(RendererFactory2, domRendererFactory3) as RendererFactory3;
rootViewInjector.get(RendererFactory2, domRendererFactory3 as RendererFactory2) as
RendererFactory3;
const sanitizer = rootViewInjector.get(Sanitizer, null);
const hostRenderer = rendererFactory.createRenderer(null, this.componentDef);

View file

@ -48,7 +48,7 @@ import {LView, TData} from './view';
* index + 7: cumulative bloom filter
* index + 8: cumulative bloom filter
* index + TNODE: TNode associated with this `NodeInjector`
* `canst tNode = tView.data[index + NodeInjectorOffset.TNODE]`
* `const tNode = tView.data[index + NodeInjectorOffset.TNODE]`
* ```
*/
export const enum NodeInjectorOffset {

View file

@ -3546,4 +3546,603 @@ describe('di', () => {
TestBed.configureTestingModule({declarations: [App]});
expect(() => TestBed.createComponent(App)).toThrowError(/NullInjectorError/);
});
describe('injector when creating embedded view', () => {
const token = new InjectionToken<string>('greeting');
@Directive({selector: 'menu-trigger'})
class MenuTrigger {
@Input('triggerFor') menu!: TemplateRef<unknown>;
constructor(private viewContainerRef: ViewContainerRef) {}
open(injector: Injector|undefined) {
this.viewContainerRef.createEmbeddedView(this.menu, undefined, {injector});
}
}
it('should be able to provide an injection token through a custom injector', () => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
TestBed.configureTestingModule({declarations: [App, MenuTrigger, Menu]});
const injector = Injector.create({providers: [{provide: token, useValue: 'hello'}]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.tokenValue).toBe('hello');
});
it('should be able to provide an injection token to a nested template through a custom injector',
() => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger #outerTrigger [triggerFor]="outerTemplate"></menu-trigger>
<ng-template #outerTemplate>
<menu></menu>
<menu-trigger #innerTrigger [triggerFor]="innerTemplate"></menu-trigger>
<ng-template #innerTemplate>
<menu #innerMenu></menu>
</ng-template>
</ng-template>
`
})
class App {
@ViewChild('outerTrigger', {read: MenuTrigger}) outerTrigger!: MenuTrigger;
@ViewChild('innerTrigger', {read: MenuTrigger}) innerTrigger!: MenuTrigger;
@ViewChild('innerMenu', {read: Menu}) innerMenu!: Menu;
}
TestBed.configureTestingModule({declarations: [App, MenuTrigger, Menu]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.outerTrigger.open(
Injector.create({providers: [{provide: token, useValue: 'hello'}]}));
fixture.detectChanges();
fixture.componentInstance.innerTrigger.open(undefined);
fixture.detectChanges();
expect(fixture.componentInstance.innerMenu.tokenValue).toBe('hello');
});
it('should be able to resolve a token from a custom grandparent injector if the token is not provided in the parent',
() => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger #grandparentTrigger [triggerFor]="grandparentTemplate"></menu-trigger>
<ng-template #grandparentTemplate>
<menu></menu>
<menu-trigger #parentTrigger [triggerFor]="parentTemplate"></menu-trigger>
<ng-template #parentTemplate>
<menu></menu>
<menu-trigger #childTrigger [triggerFor]="childTemplate"></menu-trigger>
<ng-template #childTemplate>
<menu #childMenu></menu>
</ng-template>
</ng-template>
</ng-template>
`
})
class App {
@ViewChild('grandparentTrigger', {read: MenuTrigger}) grandparentTrigger!: MenuTrigger;
@ViewChild('parentTrigger', {read: MenuTrigger}) parentTrigger!: MenuTrigger;
@ViewChild('childTrigger', {read: MenuTrigger}) childTrigger!: MenuTrigger;
@ViewChild('childMenu', {read: Menu}) childMenu!: Menu;
}
TestBed.configureTestingModule({declarations: [App, MenuTrigger, Menu]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.grandparentTrigger.open(
Injector.create({providers: [{provide: token, useValue: 'hello'}]}));
fixture.detectChanges();
fixture.componentInstance.parentTrigger.open(Injector.create({providers: []}));
fixture.detectChanges();
fixture.componentInstance.childTrigger.open(undefined);
fixture.detectChanges();
expect(fixture.componentInstance.childMenu.tokenValue).toBe('hello');
});
it('should resolve value from node injector if it is lower than embedded view injector', () => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
selector: 'wrapper',
providers: [{provide: token, useValue: 'hello from wrapper'}],
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class Wrapper {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<wrapper></wrapper>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Wrapper) wrapper!: Wrapper;
}
TestBed.configureTestingModule({declarations: [App, MenuTrigger, Menu, Wrapper]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(
Injector.create({providers: [{provide: token, useValue: 'hello from injector'}]}));
fixture.detectChanges();
fixture.componentInstance.wrapper.trigger.open(undefined);
fixture.detectChanges();
expect(fixture.componentInstance.wrapper.menu.tokenValue).toBe('hello from wrapper');
});
it('should be able to inject a value provided at the module level', () => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@NgModule({
declarations: [App, MenuTrigger, Menu],
exports: [App, MenuTrigger, Menu],
providers: [{provide: token, useValue: 'hello'}]
})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const injector = Injector.create({providers: []});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.tokenValue).toBe('hello');
});
it('should have value from custom injector take precedence over module injector', () => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@NgModule({
declarations: [App, MenuTrigger, Menu],
exports: [App, MenuTrigger, Menu],
providers: [{provide: token, useValue: 'hello from module'}]
})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const injector =
Injector.create({providers: [{provide: token, useValue: 'hello from injector'}]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.tokenValue).toBe('hello from injector');
});
it('should be able to inject built-in tokens when a custom injector is provided', () => {
@Directive({selector: 'menu'})
class Menu {
constructor(public elementRef: ElementRef, public changeDetectorRef: ChangeDetectorRef) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
TestBed.configureTestingModule({declarations: [App, MenuTrigger, Menu]});
const injector = Injector.create({providers: []});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.elementRef.nativeElement)
.toBe(fixture.nativeElement.querySelector('menu'));
expect(fixture.componentInstance.menu.changeDetectorRef).toBeTruthy();
});
it('should have value from parent component injector take precedence over module injector',
() => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`,
providers: [{provide: token, useValue: 'hello from parent'}]
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@NgModule({
declarations: [App, MenuTrigger, Menu],
exports: [App, MenuTrigger, Menu],
providers: [{provide: token, useValue: 'hello from module'}]
})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const injector = Injector.create({providers: []});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.tokenValue).toBe('hello from parent');
});
it('should be able to inject an injectable with dependencies', () => {
@Injectable()
class Greeter {
constructor(@Inject(token) private tokenValue: string) {}
greet() {
return `hello from ${this.tokenValue}`;
}
}
@Directive({selector: 'menu'})
class Menu {
constructor(public greeter: Greeter) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@NgModule({
declarations: [App, MenuTrigger, Menu],
exports: [App, MenuTrigger, Menu],
providers: [{provide: token, useValue: 'module'}]
})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const injector = Injector.create({
providers: [
{provide: Greeter, useClass: Greeter},
{provide: token, useValue: 'injector'},
]
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.greeter.greet()).toBe('hello from injector');
});
it('should be able to inject a value from a grandparent component when a custom injector is provided',
() => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
selector: 'parent',
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class Parent {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@Component({
template: '<parent></parent>',
providers: [{provide: token, useValue: 'hello from grandparent'}]
})
class GrandParent {
@ViewChild(Parent) parent!: Parent;
}
TestBed.configureTestingModule({declarations: [GrandParent, Parent, MenuTrigger, Menu]});
const injector = Injector.create({providers: []});
const fixture = TestBed.createComponent(GrandParent);
fixture.detectChanges();
fixture.componentInstance.parent.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.parent.menu.tokenValue).toBe('hello from grandparent');
});
it('should be able to use a custom injector when created through TemplateRef', () => {
let injectedValue: string|undefined;
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) tokenValue: string) {
injectedValue = tokenValue;
}
}
@Component({
template: `
<ng-template>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(TemplateRef) template!: TemplateRef<unknown>;
}
@NgModule({
declarations: [App, Menu],
exports: [App, Menu],
providers: [{provide: token, useValue: 'hello from module'}]
})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const injector =
Injector.create({providers: [{provide: token, useValue: 'hello from injector'}]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.template.createEmbeddedView({}, injector);
fixture.detectChanges();
expect(injectedValue).toBe('hello from injector');
});
it('should use a custom injector when the view is created outside of the declaration view',
() => {
const declarerToken = new InjectionToken<string>('declarerToken');
const creatorToken = new InjectionToken<string>('creatorToken');
@Directive({selector: 'menu'})
class Menu {
constructor(
@Inject(token) public tokenValue: string,
@Optional() @Inject(declarerToken) public declarerTokenValue: string,
@Optional() @Inject(creatorToken) public creatorTokenValue: string) {}
}
@Component({
selector: 'declarer',
template: '<ng-template><menu></menu></ng-template>',
providers: [{provide: declarerToken, useValue: 'hello from declarer'}]
})
class Declarer {
@ViewChild(Menu) menu!: Menu;
@ViewChild(TemplateRef) template!: TemplateRef<unknown>;
}
@Component({
selector: 'creator',
template: '<menu-trigger></menu-trigger>',
providers: [{provide: creatorToken, useValue: 'hello from creator'}]
})
class Creator {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
}
@Component({
template: `
<declarer></declarer>
<creator></creator>
`
})
class App {
@ViewChild(Declarer) declarer!: Declarer;
@ViewChild(Creator) creator!: Creator;
}
TestBed.configureTestingModule(
{declarations: [App, MenuTrigger, Menu, Declarer, Creator]});
const injector = Injector.create({providers: [{provide: token, useValue: 'hello'}]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const {declarer, creator} = fixture.componentInstance;
creator.trigger.menu = declarer.template;
creator.trigger.open(injector);
fixture.detectChanges();
expect(declarer.menu.tokenValue).toBe('hello');
expect(declarer.menu.declarerTokenValue).toBe('hello from declarer');
expect(declarer.menu.creatorTokenValue).toBeNull();
});
it('should behave consistently with `createComponent` when token is shadowed in node injector',
() => {
@Directive({selector: 'trigger'})
class Trigger {
constructor(public viewContainerRef: ViewContainerRef) {}
}
@Directive({selector: 'overlay'})
class Overlay {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
selector: 'overlay-host',
template: '<overlay></overlay>',
providers: [{provide: token, useValue: 'hello from parent'}],
})
class OverlayHost {
@ViewChild(Overlay) overlay!: Overlay;
}
@Component({
selector: 'wrapper',
template: '<ng-content></ng-content>',
providers: [{provide: token, useValue: 'hello from parent'}],
})
class Wrapper {
}
@Component({
template: `
<trigger></trigger>
<wrapper>
<ng-template #template>
<overlay></overlay>
</ng-template>
</wrapper>
`
})
class App {
@ViewChild(Trigger) trigger!: Trigger;
@ViewChild('template', {read: TemplateRef}) template!: TemplateRef<any>;
@ViewChild(Overlay) overlayInTemplate!: Overlay;
openFromTemplate(injector: Injector) {
this.trigger.viewContainerRef.createEmbeddedView(this.template, null, {injector});
}
openFromComponent(injector: Injector) {
return this.trigger.viewContainerRef.createComponent(OverlayHost, {injector});
}
}
TestBed.configureTestingModule(
{declarations: [App, Trigger, Overlay, OverlayHost, Wrapper]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const providers = [{provide: token, useValue: 'hello from custom injector'}];
fixture.componentInstance.openFromTemplate(Injector.create({providers}));
fixture.detectChanges();
const componentRef =
fixture.componentInstance.openFromComponent(Injector.create({providers}));
fixture.detectChanges();
// The node injector is expected to take precedence over the provided injector, despite
// technically being higher in the tree, because the custom one is provided as a module
// injector. This is consistent with how `createComponent` has always worked and avoids
// ambiguity as to whether the provided injector should be in the declaration or insertion
// node injector tree.
expect(fixture.componentInstance.overlayInTemplate.tokenValue).toBe('hello from parent');
expect(componentRef.instance.overlay.tokenValue).toBe('hello from parent');
});
});
});

View file

@ -77,6 +77,9 @@
{
"name": "COMPOSITION_BUFFER_MODE"
},
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionStrategy"
},

View file

@ -77,6 +77,9 @@
{
"name": "COMPOSITION_BUFFER_MODE"
},
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionStrategy"
},

View file

@ -92,6 +92,9 @@
{
"name": "CatchSubscriber"
},
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionStrategy"
},

View file

@ -2,6 +2,9 @@
{
"name": "CLEAN_PROMISE"
},
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionStrategy"
},