diff --git a/goldens/public-api/core/core.md b/goldens/public-api/core/core.md index 5d09b955120..387bfbd03b8 100644 --- a/goldens/public-api/core/core.md +++ b/goldens/public-api/core/core.md @@ -1237,7 +1237,7 @@ export type StaticProvider = ValueProvider | ExistingProvider | StaticClassProvi // @public export abstract class TemplateRef { - abstract createEmbeddedView(context: C): EmbeddedViewRef; + abstract createEmbeddedView(context: C, injector?: Injector): EmbeddedViewRef; abstract readonly elementRef: ElementRef; } @@ -1383,6 +1383,10 @@ export abstract class ViewContainerRef { }): ComponentRef; // @deprecated abstract createComponent(componentFactory: ComponentFactory, index?: number, injector?: Injector, projectableNodes?: any[][], ngModuleRef?: NgModuleRef): ComponentRef; + abstract createEmbeddedView(templateRef: TemplateRef, context?: C, options?: { + index?: number; + injector?: Injector; + }): EmbeddedViewRef; abstract createEmbeddedView(templateRef: TemplateRef, context?: C, index?: number): EmbeddedViewRef; abstract detach(index?: number): ViewRef | null; abstract get element(): ElementRef; diff --git a/packages/core/src/linker/template_ref.ts b/packages/core/src/linker/template_ref.ts index c7ecd07751c..aae7da7e6ff 100644 --- a/packages/core/src/linker/template_ref.ts +++ b/packages/core/src/linker/template_ref.ts @@ -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 { * and attaches it to the view container. * @param context The data-binding context of the embedded view, as declared * in the `` usage. + * @param injector Injector to be used within the embedded view. * @returns The new embedded view object. */ - abstract createEmbeddedView(context: C): EmbeddedViewRef; + abstract createEmbeddedView(context: C, injector?: Injector): EmbeddedViewRef; /** * @internal @@ -77,11 +80,12 @@ const R3TemplateRef = class TemplateRef extends ViewEngineTemplateRef { super(); } - override createEmbeddedView(context: T): EmbeddedViewRef { + override createEmbeddedView(context: T, injector?: Injector): EmbeddedViewRef { 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 extends ViewEngineTemplateRef { } }; +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. * diff --git a/packages/core/src/linker/view_container_ref.ts b/packages/core/src/linker/view_container_ref.ts index 3c54f2eff09..45bb73d1e1d 100644 --- a/packages/core/src/linker/view_container_ref.ts +++ b/packages/core/src/linker/view_container_ref.ts @@ -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 `` 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(templateRef: TemplateRef, context?: C, options?: { + index?: number, + injector?: Injector + }): EmbeddedViewRef; + /** * 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(templateRef: TemplateRef, context?: C, options?: { + index?: number, + injector?: Injector + }): EmbeddedViewRef; override createEmbeddedView(templateRef: TemplateRef, context?: C, index?: number): - EmbeddedViewRef { - const viewRef = templateRef.createEmbeddedView(context || {}); + EmbeddedViewRef; + override createEmbeddedView(templateRef: TemplateRef, context?: C, indexOrOptions?: number|{ + index?: number, + injector?: Injector + }): EmbeddedViewRef { + 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 || {}, injector); this.insert(viewRef, index); return viewRef; } diff --git a/packages/core/src/render3/chained_injector.ts b/packages/core/src/render3/chained_injector.ts new file mode 100644 index 00000000000..7a1b841156b --- /dev/null +++ b/packages/core/src/render3/chained_injector.ts @@ -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(token: ProviderToken, 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); + } +} diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 121ef3a847f..488a46f0e38 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -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: (token: ProviderToken, 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 extends viewEngine_ComponentFactory { ngModule?: viewEngine_NgModuleRef|undefined): viewEngine_ComponentRef { 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); diff --git a/packages/core/src/render3/interfaces/injector.ts b/packages/core/src/render3/interfaces/injector.ts index 09b7fcd10af..e4a7255ac76 100644 --- a/packages/core/src/render3/interfaces/injector.ts +++ b/packages/core/src/render3/interfaces/injector.ts @@ -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 { diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index a348745229c..4450a874536 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -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('greeting'); + + @Directive({selector: 'menu-trigger'}) + class MenuTrigger { + @Input('triggerFor') menu!: TemplateRef; + + 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: ` + + + + + ` + }) + 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: ` + + + + + + + + + + ` + }) + 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: ` + + + + + + + + + + + + + + + ` + }) + 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: ` + + + + + ` + }) + class Wrapper { + @ViewChild(MenuTrigger) trigger!: MenuTrigger; + @ViewChild(Menu) menu!: Menu; + } + + @Component({ + 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: ` + + + + + ` + }) + 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: ` + + + + + ` + }) + 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: ` + + + + + ` + }) + 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: ` + + + + + `, + 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: ` + + + + + ` + }) + 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: ` + + + + + ` + }) + class Parent { + @ViewChild(MenuTrigger) trigger!: MenuTrigger; + @ViewChild(Menu) menu!: Menu; + } + + @Component({ + template: '', + 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: ` + + + + ` + }) + class App { + @ViewChild(TemplateRef) template!: TemplateRef; + } + + @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('declarerToken'); + const creatorToken = new InjectionToken('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: '', + providers: [{provide: declarerToken, useValue: 'hello from declarer'}] + }) + class Declarer { + @ViewChild(Menu) menu!: Menu; + @ViewChild(TemplateRef) template!: TemplateRef; + } + + @Component({ + selector: 'creator', + template: '', + providers: [{provide: creatorToken, useValue: 'hello from creator'}] + }) + class Creator { + @ViewChild(MenuTrigger) trigger!: MenuTrigger; + } + + @Component({ + template: ` + + + ` + }) + 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: '', + providers: [{provide: token, useValue: 'hello from parent'}], + }) + class OverlayHost { + @ViewChild(Overlay) overlay!: Overlay; + } + + @Component({ + selector: 'wrapper', + template: '', + providers: [{provide: token, useValue: 'hello from parent'}], + }) + class Wrapper { + } + + @Component({ + template: ` + + + + + + + + ` + }) + class App { + @ViewChild(Trigger) trigger!: Trigger; + @ViewChild('template', {read: TemplateRef}) template!: TemplateRef; + @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'); + }); + }); }); diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index afe8691c865..81664ac20b5 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -77,6 +77,9 @@ { "name": "COMPOSITION_BUFFER_MODE" }, + { + "name": "ChainedInjector" + }, { "name": "ChangeDetectionStrategy" }, diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index add4b427c14..d0ae40574d8 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -77,6 +77,9 @@ { "name": "COMPOSITION_BUFFER_MODE" }, + { + "name": "ChainedInjector" + }, { "name": "ChangeDetectionStrategy" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index 3331cc8aaf1..788d072262c 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -92,6 +92,9 @@ { "name": "CatchSubscriber" }, + { + "name": "ChainedInjector" + }, { "name": "ChangeDetectionStrategy" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index f2904bb9b5e..a530f251ded 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -2,6 +2,9 @@ { "name": "CLEAN_PROMISE" }, + { + "name": "ChainedInjector" + }, { "name": "ChangeDetectionStrategy" },