mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
fb27867ab8
commit
b49ffcd50e
11 changed files with 716 additions and 34 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
36
packages/core/src/render3/chained_injector.ts
Normal file
36
packages/core/src/render3/chained_injector.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@
|
|||
{
|
||||
"name": "COMPOSITION_BUFFER_MODE"
|
||||
},
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@
|
|||
{
|
||||
"name": "COMPOSITION_BUFFER_MODE"
|
||||
},
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -92,6 +92,9 @@
|
|||
{
|
||||
"name": "CatchSubscriber"
|
||||
},
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
{
|
||||
"name": "CLEAN_PROMISE"
|
||||
},
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue