From 94c949a60ad0de1f5385ec97f2c1933cb1d55f22 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 28 Feb 2022 19:49:52 +0100 Subject: [PATCH] feat(core): allow for injector to be specified when creating an embedded view (#45156) 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. This is a second stab at the changes in #44666. The difference this time is that the new injector acts as a node injector, rather than a module injector. Fixes #14935. PR Close #45156 --- goldens/public-api/common/common.md | 3 +- goldens/public-api/core/core.md | 6 +- goldens/size-tracking/aio-payloads.json | 4 +- .../size-tracking/integration-payloads.json | 28 +- .../src/directives/ng_template_outlet.ts | 21 +- .../directives/ng_component_outlet_spec.ts | 21 +- .../directives/ng_template_outlet_spec.ts | 57 +- packages/core/src/linker/template_ref.ts | 8 +- .../core/src/linker/view_container_ref.ts | 40 +- packages/core/src/render3/component.ts | 4 +- packages/core/src/render3/component_ref.ts | 42 +- packages/core/src/render3/di.ts | 301 ++++--- .../core/src/render3/instructions/shared.ts | 12 +- .../core/src/render3/interfaces/injector.ts | 2 +- packages/core/src/render3/interfaces/view.ts | 18 +- packages/core/test/acceptance/di_spec.ts | 819 ++++++++++++++++++ .../animations/bundle.golden_symbols.json | 6 + .../cyclic_import/bundle.golden_symbols.json | 3 + .../forms_reactive/bundle.golden_symbols.json | 6 + .../bundle.golden_symbols.json | 6 + .../hello_world/bundle.golden_symbols.json | 3 + .../router/bundle.golden_symbols.json | 6 + .../bundling/todo/bundle.golden_symbols.json | 6 + packages/core/test/render3/di_spec.ts | 2 +- .../test/render3/instructions/shared_spec.ts | 2 +- packages/core/test/render3/perf/BUILD.bazel | 14 + .../perf/directive_instantiate/index.ts | 2 +- .../render3/perf/element_text_create/index.ts | 2 +- .../embedded_view_injector/app_component.ts | 89 ++ .../perf/embedded_view_injector/index.ts | 47 + .../injector_component.ts | 41 + .../embedded_view_injector/inner_component.ts | 86 ++ .../render_template_directive.ts | 45 + .../core/test/render3/perf/listeners/index.ts | 2 +- .../test/render3/perf/ng_template/index.ts | 3 +- .../core/test/render3/perf/noop_renderer.ts | 3 +- packages/core/test/render3/perf/setup.ts | 6 +- .../render3/perf/view_destroy_hook/index.ts | 2 +- packages/core/test/render3/render_util.ts | 4 +- packages/core/test/render3/view_fixture.ts | 4 +- 40 files changed, 1598 insertions(+), 178 deletions(-) create mode 100644 packages/core/test/render3/perf/embedded_view_injector/app_component.ts create mode 100644 packages/core/test/render3/perf/embedded_view_injector/index.ts create mode 100644 packages/core/test/render3/perf/embedded_view_injector/injector_component.ts create mode 100644 packages/core/test/render3/perf/embedded_view_injector/inner_component.ts create mode 100644 packages/core/test/render3/perf/embedded_view_injector/render_template_directive.ts diff --git a/goldens/public-api/common/common.md b/goldens/public-api/common/common.md index 19d4c52758c..baed7c515eb 100644 --- a/goldens/public-api/common/common.md +++ b/goldens/public-api/common/common.md @@ -585,8 +585,9 @@ export class NgTemplateOutlet implements OnChanges { ngOnChanges(changes: SimpleChanges): void; ngTemplateOutlet: TemplateRef | null; ngTemplateOutletContext: Object | null; + ngTemplateOutletInjector: Injector | null; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } diff --git a/goldens/public-api/core/core.md b/goldens/public-api/core/core.md index 2b188e431c3..002838082c7 100644 --- a/goldens/public-api/core/core.md +++ b/goldens/public-api/core/core.md @@ -1233,7 +1233,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; } @@ -1379,6 +1379,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/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json index 53a33f37e35..5d5b6c13de4 100755 --- a/goldens/size-tracking/aio-payloads.json +++ b/goldens/size-tracking/aio-payloads.json @@ -15,8 +15,8 @@ "master": { "uncompressed": { "runtime": 4343, - "main": 450179, - "polyfills": 37823, + "main": 450900, + "polyfills": 37817, "styles": 70416, "light-theme": 77582, "dark-theme": 77711 diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index c950f6df56f..f89db6b5626 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -3,8 +3,8 @@ "master": { "uncompressed": { "runtime": 1083, - "main": 126218, - "polyfills": 37778 + "main": 127046, + "polyfills": 37772 } } }, @@ -15,8 +15,8 @@ "main": "TODO(i): temporarily increase the payload size limit from 17597 - this needs to be investigate further what caused the increase.", "main": "Likely there is a missing PURE annotation https://github.com/angular/angular/pull/43344", "main": "Tracking issue: https://github.com/angular/angular/issues/43568", - "main": 20378, - "polyfills": 37802 + "main": 20601, + "polyfills": 37796 } } }, @@ -24,8 +24,8 @@ "master": { "uncompressed": { "runtime": 1105, - "main": 131882, - "polyfills": 37800 + "main": 132710, + "polyfills": 37794 } } }, @@ -33,8 +33,8 @@ "master": { "uncompressed": { "runtime": 929, - "main": 124544, - "polyfills": 38488 + "main": 125295, + "polyfills": 38482 } } }, @@ -42,8 +42,8 @@ "master": { "uncompressed": { "runtime": 2835, - "main": 233348, - "polyfills": 37796, + "main": 233902, + "polyfills": 37790, "src_app_lazy_lazy_module_ts": 795 } } @@ -52,8 +52,8 @@ "master": { "uncompressed": { "runtime": 1063, - "main": 158556, - "polyfills": 37758 + "main": 159384, + "polyfills": 37752 } } }, @@ -61,8 +61,8 @@ "master": { "uncompressed": { "runtime": 1070, - "main": 158300, - "polyfills": 37768 + "main": 159051, + "polyfills": 37762 } } }, diff --git a/packages/common/src/directives/ng_template_outlet.ts b/packages/common/src/directives/ng_template_outlet.ts index ed8a8d19842..ebb2a951b08 100644 --- a/packages/common/src/directives/ng_template_outlet.ts +++ b/packages/common/src/directives/ng_template_outlet.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core'; +import {Directive, EmbeddedViewRef, Injector, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core'; /** * @ngModule CommonModule @@ -49,20 +49,31 @@ export class NgTemplateOutlet implements OnChanges { */ @Input() public ngTemplateOutlet: TemplateRef|null = null; + /** Injector to be used within the embedded view. */ + @Input() public ngTemplateOutletInjector: Injector|null = null; + constructor(private _viewContainerRef: ViewContainerRef) {} /** @nodoc */ ngOnChanges(changes: SimpleChanges) { - if (changes['ngTemplateOutlet']) { + if (changes['ngTemplateOutlet'] || changes['ngTemplateOutletInjector']) { const viewContainerRef = this._viewContainerRef; if (this._viewRef) { viewContainerRef.remove(viewContainerRef.indexOf(this._viewRef)); } - this._viewRef = this.ngTemplateOutlet ? - viewContainerRef.createEmbeddedView(this.ngTemplateOutlet, this.ngTemplateOutletContext) : - null; + if (this.ngTemplateOutlet) { + const { + ngTemplateOutlet: template, + ngTemplateOutletContext: context, + ngTemplateOutletInjector: injector + } = this; + this._viewRef = viewContainerRef.createEmbeddedView( + template, context, injector ? {injector} : undefined); + } else { + this._viewRef = null; + } } else if ( this._viewRef && changes['ngTemplateOutletContext'] && this.ngTemplateOutletContext) { this._viewRef.context = this.ngTemplateOutletContext; diff --git a/packages/common/test/directives/ng_component_outlet_spec.ts b/packages/common/test/directives/ng_component_outlet_spec.ts index 4df27979994..854c85e27ff 100644 --- a/packages/common/test/directives/ng_component_outlet_spec.ts +++ b/packages/common/test/directives/ng_component_outlet_spec.ts @@ -96,8 +96,10 @@ describe('insert/remove', () => { const uniqueValue = {}; fixture.componentInstance.currentComponent = InjectedComponent; - fixture.componentInstance.injector = Injector.create( - [{provide: TEST_TOKEN, useValue: uniqueValue}], fixture.componentRef.injector); + fixture.componentInstance.injector = Injector.create({ + providers: [{provide: TEST_TOKEN, useValue: uniqueValue}], + parent: fixture.componentRef.injector, + }); fixture.detectChanges(); let cmpRef: ComponentRef = fixture.componentInstance.cmpRef!; @@ -250,6 +252,21 @@ describe('insert/remove', () => { expect(fixture.nativeElement).toHaveText('bat'); })); + + it('should override providers from parent component using custom injector', waitForAsync(() => { + TestBed.overrideComponent(InjectedComponent, {set: {template: 'Value: {{testToken}}'}}); + TestBed.overrideComponent( + TestComponent, {set: {providers: [{provide: TEST_TOKEN, useValue: 'parent'}]}}); + const fixture = TestBed.createComponent(TestComponent); + fixture.componentInstance.currentComponent = InjectedComponent; + fixture.componentInstance.injector = Injector.create({ + providers: [{provide: TEST_TOKEN, useValue: 'child'}], + parent: fixture.componentInstance.vcRef.injector + }); + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveText('Value: child'); + })); }); const TEST_TOKEN = new InjectionToken('TestToken'); diff --git a/packages/common/test/directives/ng_template_outlet_spec.ts b/packages/common/test/directives/ng_template_outlet_spec.ts index ee3132180eb..561d79dc813 100644 --- a/packages/common/test/directives/ng_template_outlet_spec.ts +++ b/packages/common/test/directives/ng_template_outlet_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {Component, ContentChildren, Directive, Injectable, NO_ERRORS_SCHEMA, OnDestroy, QueryList, TemplateRef} from '@angular/core'; +import {Component, ContentChildren, Directive, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, OnDestroy, Provider, QueryList, TemplateRef} from '@angular/core'; import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -29,7 +29,13 @@ describe('NgTemplateOutlet', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [TestComponent, CaptureTplRefs, DestroyableCmpt, MultiContextComponent], + declarations: [ + TestComponent, + CaptureTplRefs, + DestroyableCmpt, + MultiContextComponent, + InjectValueComponent, + ], imports: [CommonModule], providers: [DestroyedSpyService] }); @@ -262,8 +268,41 @@ describe('NgTemplateOutlet', () => { expect(componentInstance.context1).toEqual({name: 'two'}); expect(componentInstance.context2).toEqual({name: 'one'}); }); + + it('should be able to specify an injector', waitForAsync(() => { + const template = `` + + ``; + fixture = createTestComponent(template); + fixture.componentInstance.injector = + Injector.create({providers: [{provide: templateToken, useValue: 'world'}]}); + detectChangesAndExpectText('Hello world'); + })); + + it('should re-render if the injector changes', waitForAsync(() => { + const template = `` + + ``; + fixture = createTestComponent(template); + fixture.componentInstance.injector = + Injector.create({providers: [{provide: templateToken, useValue: 'world'}]}); + detectChangesAndExpectText('Hello world'); + + fixture.componentInstance.injector = + Injector.create({providers: [{provide: templateToken, useValue: 'there'}]}); + detectChangesAndExpectText('Hello there'); + })); + + it('should override providers from parent component using custom injector', waitForAsync(() => { + const template = `` + + ``; + fixture = createTestComponent(template, [{provide: templateToken, useValue: 'parent'}]); + fixture.componentInstance.injector = + Injector.create({providers: [{provide: templateToken, useValue: 'world'}]}); + detectChangesAndExpectText('Hello world'); + })); }); +const templateToken = new InjectionToken('templateToken'); + @Injectable() class DestroyedSpyService { destroyed = false; @@ -290,6 +329,15 @@ class TestComponent { currentTplRef!: TemplateRef; context: any = {foo: 'bar'}; value = 'bar'; + injector: Injector|null = null; +} + +@Component({ + selector: 'inject-value', + template: 'Hello {{tokenValue}}', +}) +class InjectValueComponent { + constructor(@Inject(templateToken) public tokenValue: string) {} } @Component({ @@ -305,8 +353,9 @@ class MultiContextComponent { context2: {name: string}|undefined; } -function createTestComponent(template: string): ComponentFixture { - return TestBed.overrideComponent(TestComponent, {set: {template: template}}) +function createTestComponent( + template: string, providers: Provider[] = []): ComponentFixture { + return TestBed.overrideComponent(TestComponent, {set: {template: template, providers}}) .configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}) .createComponent(TestComponent); } diff --git a/packages/core/src/linker/template_ref.ts b/packages/core/src/linker/template_ref.ts index c7ecd07751c..43c5cc2fc4c 100644 --- a/packages/core/src/linker/template_ref.ts +++ b/packages/core/src/linker/template_ref.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {Injector} from '../di/injector'; import {assertLContainer} from '../render3/assert'; import {createLView, renderView} from '../render3/instructions/shared'; import {TContainerNode, TNode, TNodeType} from '../render3/interfaces/node'; @@ -55,9 +56,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 +79,11 @@ 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, null, injector || null); const declarationLContainer = this._declarationLView[this._declarationTContainer.index]; ngDevMode && assertLContainer(declarationLContainer); diff --git a/packages/core/src/linker/view_container_ref.ts b/packages/core/src/linker/view_container_ref.ts index 2114ffaea61..720abd0aa93 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/component.ts b/packages/core/src/render3/component.ts index f2ba3baee49..e1ff73a1b3b 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -134,7 +134,7 @@ export function renderComponent( const rootTView = createTView(TViewType.Root, null, null, 1, 0, null, null, null, null, null); const rootView: LView = createLView( null, rootTView, rootContext, rootFlags, null, null, rendererFactory, renderer, null, - opts.injector || null); + opts.injector || null, null); enterView(rootView); let component: T; @@ -200,7 +200,7 @@ export function createRootComponentView( const componentView = createLView( rootView, getOrCreateTComponentView(def), null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, rootView[index], tNode, - rendererFactory, viewRenderer, sanitizer || null, null); + rendererFactory, viewRenderer, sanitizer || null, null, null); if (tView.firstCreatePass) { diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), tView, def.type); diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 69540a69aee..88fe0498b07 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -69,24 +69,28 @@ function getNamespace(elementName: string): string|null { return name === 'svg' ? SVG_NAMESPACE : (name === 'math' ? MATH_ML_NAMESPACE : null); } -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); +/** + * 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. + */ +class ChainedInjector implements Injector { + constructor(private injector: Injector, private parentInjector: Injector) {} - 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; - } + get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T { + const value = this.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as T, flags); - return moduleInjector.get(token, notFoundValue, 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); + } } /** @@ -125,11 +129,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); @@ -150,7 +154,7 @@ export class ComponentFactory extends viewEngine_ComponentFactory { const rootTView = createTView(TViewType.Root, null, null, 1, 0, null, null, null, null, null); const rootLView = createLView( null, rootTView, rootContext, rootFlags, null, null, rendererFactory, hostRenderer, - sanitizer, rootViewInjector); + sanitizer, rootViewInjector, null); // rootView is the parent when bootstrapping // TODO(misko): it looks like we are entering view here but we don't really need to as diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 87cea749c2d..1fee25dc9d6 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -25,7 +25,7 @@ import {DirectiveDef} from './interfaces/definition'; import {isFactory, NO_PARENT_INJECTOR, NodeInjectorFactory, NodeInjectorOffset, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector'; import {AttributeMarker, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TNode, TNodeProviderIndexes, TNodeType} from './interfaces/node'; import {isComponentDef, isComponentHost} from './interfaces/type_checks'; -import {DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, INJECTOR, LView, T_HOST, TData, TVIEW, TView, TViewType} from './interfaces/view'; +import {DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, EMBEDDED_VIEW_INJECTOR, FLAGS, INJECTOR, LView, LViewFlags, T_HOST, TData, TVIEW, TView, TViewType} from './interfaces/view'; import {assertTNodeType} from './node_assert'; import {enterDI, getCurrentTNode, getLView, leaveDI} from './state'; import {isNameOnlyAttributeMarker} from './util/attrs_utils'; @@ -96,6 +96,9 @@ const BLOOM_BUCKET_BITS = 5; /** Counter used to generate unique IDs for directives. */ let nextNgElementId = 0; +/** Value used when something wasn't found by an injector. */ +const NOT_FOUND = {}; + /** * Registers this directive as present in its node's injector by flipping the directive's * corresponding bit in the injector's bloom filter. @@ -222,21 +225,8 @@ export function getParentInjectorLocation(tNode: TNode, lView: LView): RelativeI // `LView` hierarchy and look for it. If we walk of the top, that means that there is no parent // `NodeInjector`. while (lViewCursor !== null) { - // First determine the `parentTNode` location. The parent pointer differs based on `TView.type`. - const tView = lViewCursor[TVIEW]; - const tViewType = tView.type; - if (tViewType === TViewType.Embedded) { - ngDevMode && - assertDefined(tView.declTNode, 'Embedded TNodes should have declaration parents.'); - parentTNode = tView.declTNode; - } else if (tViewType === TViewType.Component) { - // Components don't have `TView.declTNode` because each instance of component could be - // inserted in different location, hence `TView.declTNode` is meaningless. - parentTNode = lViewCursor[T_HOST]; - } else { - ngDevMode && assertEqual(tView.type, TViewType.Root, 'Root type expected'); - parentTNode = null; - } + parentTNode = getTNodeFromLView(lViewCursor); + if (parentTNode === null) { // If we have no parent, than we are done. return NO_PARENT_INJECTOR; @@ -406,100 +396,126 @@ export function getOrCreateInjectable( tNode: TDirectiveHostNode|null, lView: LView, token: ProviderToken, flags: InjectFlags = InjectFlags.Default, notFoundValue?: any): T|null { if (tNode !== null) { - const bloomHash = bloomHashBitOrFactory(token); - // If the ID stored here is a function, this is a special object like ElementRef or TemplateRef - // so just call the factory function to create it. - if (typeof bloomHash === 'function') { - if (!enterDI(lView, tNode, flags)) { - // Failed to enter DI, try module injector instead. If a token is injected with the @Host - // flag, the module injector is not searched for that token in Ivy. - return (flags & InjectFlags.Host) ? - notFoundValueOrThrow(notFoundValue, token, flags) : - lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue); + // If the view or any of its ancestors have an embedded + // view injector, we have to look it up there first. + if (lView[FLAGS] & LViewFlags.HasEmbeddedViewInjector) { + const embeddedInjectorValue = + lookupTokenUsingEmbeddedInjector(tNode, lView, token, flags, NOT_FOUND); + if (embeddedInjectorValue !== NOT_FOUND) { + return embeddedInjectorValue; } - try { - const value = bloomHash(flags); - if (value == null && !(flags & InjectFlags.Optional)) { - throwProviderNotFoundError(token); - } else { - return value; - } - } finally { - leaveDI(); + } + + // Otherwise try the node injector. + const value = lookupTokenUsingNodeInjector(tNode, lView, token, flags, NOT_FOUND); + if (value !== NOT_FOUND) { + return value; + } + } + + // Finally, fall back to the module injector. + return lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue); +} + +/** + * Returns the value associated to the given token from the node injector. + * + * @param tNode The Node where the search for the injector should start + * @param lView The `LView` that contains the `tNode` + * @param token The token to look for + * @param flags Injection flags + * @param notFoundValue The value to return when the injection flags is `InjectFlags.Optional` + * @returns the value from the injector, `null` when not found, or `notFoundValue` if provided + */ +function lookupTokenUsingNodeInjector( + tNode: TDirectiveHostNode, lView: LView, token: ProviderToken, flags: InjectFlags, + notFoundValue?: any) { + const bloomHash = bloomHashBitOrFactory(token); + // If the ID stored here is a function, this is a special object like ElementRef or TemplateRef + // so just call the factory function to create it. + if (typeof bloomHash === 'function') { + if (!enterDI(lView, tNode, flags)) { + // Failed to enter DI, try module injector instead. If a token is injected with the @Host + // flag, the module injector is not searched for that token in Ivy. + return (flags & InjectFlags.Host) ? + notFoundValueOrThrow(notFoundValue, token, flags) : + lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue); + } + try { + const value = bloomHash(flags); + if (value == null && !(flags & InjectFlags.Optional)) { + throwProviderNotFoundError(token); + } else { + return value; } - } else if (typeof bloomHash === 'number') { - // A reference to the previous injector TView that was found while climbing the element - // injector tree. This is used to know if viewProviders can be accessed on the current - // injector. - let previousTView: TView|null = null; - let injectorIndex = getInjectorIndex(tNode, lView); - let parentLocation: RelativeInjectorLocation = NO_PARENT_INJECTOR; - let hostTElementNode: TNode|null = - flags & InjectFlags.Host ? lView[DECLARATION_COMPONENT_VIEW][T_HOST] : null; + } finally { + leaveDI(); + } + } else if (typeof bloomHash === 'number') { + // A reference to the previous injector TView that was found while climbing the element + // injector tree. This is used to know if viewProviders can be accessed on the current + // injector. + let previousTView: TView|null = null; + let injectorIndex = getInjectorIndex(tNode, lView); + let parentLocation: RelativeInjectorLocation = NO_PARENT_INJECTOR; + let hostTElementNode: TNode|null = + flags & InjectFlags.Host ? lView[DECLARATION_COMPONENT_VIEW][T_HOST] : null; - // If we should skip this injector, or if there is no injector on this node, start by - // searching the parent injector. - if (injectorIndex === -1 || flags & InjectFlags.SkipSelf) { - parentLocation = injectorIndex === -1 ? getParentInjectorLocation(tNode, lView) : - lView[injectorIndex + NodeInjectorOffset.PARENT]; + // If we should skip this injector, or if there is no injector on this node, start by + // searching the parent injector. + if (injectorIndex === -1 || flags & InjectFlags.SkipSelf) { + parentLocation = injectorIndex === -1 ? getParentInjectorLocation(tNode, lView) : + lView[injectorIndex + NodeInjectorOffset.PARENT]; - if (parentLocation === NO_PARENT_INJECTOR || !shouldSearchParent(flags, false)) { - injectorIndex = -1; - } else { - previousTView = lView[TVIEW]; - injectorIndex = getParentInjectorIndex(parentLocation); - lView = getParentInjectorView(parentLocation, lView); + if (parentLocation === NO_PARENT_INJECTOR || !shouldSearchParent(flags, false)) { + injectorIndex = -1; + } else { + previousTView = lView[TVIEW]; + injectorIndex = getParentInjectorIndex(parentLocation); + lView = getParentInjectorView(parentLocation, lView); + } + } + + // Traverse up the injector tree until we find a potential match or until we know there + // *isn't* a match. + while (injectorIndex !== -1) { + ngDevMode && assertNodeInjector(lView, injectorIndex); + + // Check the current injector. If it matches, see if it contains token. + const tView = lView[TVIEW]; + ngDevMode && + assertTNodeForLView(tView.data[injectorIndex + NodeInjectorOffset.TNODE] as TNode, lView); + if (bloomHasToken(bloomHash, injectorIndex, tView.data)) { + // At this point, we have an injector which *may* contain the token, so we step through + // the providers and directives associated with the injector's corresponding node to get + // the instance. + const instance: T|null = searchTokensOnInjector( + injectorIndex, lView, token, previousTView, flags, hostTElementNode); + if (instance !== NOT_FOUND) { + return instance; } } - - // Traverse up the injector tree until we find a potential match or until we know there - // *isn't* a match. - while (injectorIndex !== -1) { - ngDevMode && assertNodeInjector(lView, injectorIndex); - - // Check the current injector. If it matches, see if it contains token. - const tView = lView[TVIEW]; - ngDevMode && - assertTNodeForLView( - tView.data[injectorIndex + NodeInjectorOffset.TNODE] as TNode, lView); - if (bloomHasToken(bloomHash, injectorIndex, tView.data)) { - // At this point, we have an injector which *may* contain the token, so we step through - // the providers and directives associated with the injector's corresponding node to get - // the instance. - const instance: T|null = searchTokensOnInjector( - injectorIndex, lView, token, previousTView, flags, hostTElementNode); - if (instance !== NOT_FOUND) { - return instance; - } - } - parentLocation = lView[injectorIndex + NodeInjectorOffset.PARENT]; - if (parentLocation !== NO_PARENT_INJECTOR && - shouldSearchParent( - flags, - lView[TVIEW].data[injectorIndex + NodeInjectorOffset.TNODE] === hostTElementNode) && - bloomHasToken(bloomHash, injectorIndex, lView)) { - // The def wasn't found anywhere on this node, so it was a false positive. - // Traverse up the tree and continue searching. - previousTView = tView; - injectorIndex = getParentInjectorIndex(parentLocation); - lView = getParentInjectorView(parentLocation, lView); - } else { - // If we should not search parent OR If the ancestor bloom filter value does not have the - // bit corresponding to the directive we can give up on traversing up to find the specific - // injector. - injectorIndex = -1; - } + parentLocation = lView[injectorIndex + NodeInjectorOffset.PARENT]; + if (parentLocation !== NO_PARENT_INJECTOR && + shouldSearchParent( + flags, + lView[TVIEW].data[injectorIndex + NodeInjectorOffset.TNODE] === hostTElementNode) && + bloomHasToken(bloomHash, injectorIndex, lView)) { + // The def wasn't found anywhere on this node, so it was a false positive. + // Traverse up the tree and continue searching. + previousTView = tView; + injectorIndex = getParentInjectorIndex(parentLocation); + lView = getParentInjectorView(parentLocation, lView); + } else { + // If we should not search parent OR If the ancestor bloom filter value does not have the + // bit corresponding to the directive we can give up on traversing up to find the specific + // injector. + injectorIndex = -1; } } } - return lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue); -} - -const NOT_FOUND = {}; - -export function createNodeInjector(): Injector { - return new NodeInjector(getCurrentTNode()! as TDirectiveHostNode, getLView()) as any; + return notFoundValue; } function searchTokensOnInjector( @@ -693,6 +709,11 @@ export class NodeInjector implements Injector { } } +/** Creates a `NodeInjector` for the current node. */ +export function createNodeInjector(): Injector { + return new NodeInjector(getCurrentTNode()! as TDirectiveHostNode, getLView()) as any; +} + /** * @codeGenApi */ @@ -736,3 +757,83 @@ function getFactoryOf(type: Type): ((type?: Type) => T | null)|null { } return getFactoryDef(type); } + +/** + * Returns a value from the closest embedded or node injector. + * + * @param tNode The Node where the search for the injector should start + * @param lView The `LView` that contains the `tNode` + * @param token The token to look for + * @param flags Injection flags + * @param notFoundValue The value to return when the injection flags is `InjectFlags.Optional` + * @returns the value from the injector, `null` when not found, or `notFoundValue` if provided + */ +function lookupTokenUsingEmbeddedInjector( + tNode: TDirectiveHostNode, lView: LView, token: ProviderToken, flags: InjectFlags, + notFoundValue?: any) { + let currentTNode: TDirectiveHostNode|null = tNode; + let currentLView: LView|null = lView; + + // When an LView with an embedded view injector is inserted, it'll likely be interlaced with + // nodes who may have injectors (e.g. node injector -> embedded view injector -> node injector). + // Since the bloom filters for the node injectors have already been constructed and we don't + // have a way of extracting the records from an injector, the only way to maintain the correct + // hierarchy when resolving the value is to walk it node-by-node while attempting to resolve + // the token at each level. + while (currentTNode !== null && currentLView !== null && + (currentLView[FLAGS] & LViewFlags.HasEmbeddedViewInjector) && + !(currentLView[FLAGS] & LViewFlags.IsRoot)) { + ngDevMode && assertTNodeForLView(currentTNode, currentLView); + + // Note that this lookup on the node injector is using the `Self` flag, because + // we don't want the node injector to look at any parent injectors since we + // may hit the embedded view injector first. + const nodeInjectorValue = lookupTokenUsingNodeInjector( + currentTNode, currentLView, token, flags | InjectFlags.Self, NOT_FOUND); + if (nodeInjectorValue !== NOT_FOUND) { + return nodeInjectorValue; + } + + // Has an explicit type due to a TS bug: https://github.com/microsoft/TypeScript/issues/33191 + let parentTNode: TElementNode|TContainerNode|null = currentTNode.parent; + + // `TNode.parent` includes the parent within the current view only. If it doesn't exist, + // it means that we've hit the view boundary and we need to go up to the next view. + if (!parentTNode) { + // Before we go to the next LView, check if the token exists on the current embedded injector. + const embeddedViewInjector = currentLView[EMBEDDED_VIEW_INJECTOR]; + if (embeddedViewInjector) { + const embeddedViewInjectorValue = embeddedViewInjector.get(token, NOT_FOUND as T, flags); + if (embeddedViewInjectorValue !== NOT_FOUND) { + return embeddedViewInjectorValue; + } + } + + // Otherwise keep going up the tree. + parentTNode = getTNodeFromLView(currentLView); + currentLView = currentLView[DECLARATION_VIEW]; + } + + currentTNode = parentTNode; + } + + return notFoundValue; +} + +/** Gets the TNode associated with an LView inside of the declaration view. */ +function getTNodeFromLView(lView: LView): TElementNode|TElementContainerNode|null { + const tView = lView[TVIEW]; + const tViewType = tView.type; + + // The parent pointer differs based on `TView.type`. + if (tViewType === TViewType.Embedded) { + ngDevMode && assertDefined(tView.declTNode, 'Embedded TNodes should have declaration parents.'); + return tView.declTNode as TElementContainerNode; + } else if (tViewType === TViewType.Component) { + // Components don't have `TView.declTNode` because each instance of component could be + // inserted in different location, hence `TView.declTNode` is meaningless. + return lView[T_HOST] as TElementNode; + } + + return null; +} diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 039143eac82..fb6fec38fbd 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -32,7 +32,7 @@ import {isProceduralRenderer, Renderer3, RendererFactory3} from '../interfaces/r import {RComment, RElement, RNode, RText} from '../interfaces/renderer_dom'; import {SanitizerFn} from '../interfaces/sanitization'; import {isComponentDef, isComponentHost, isContentQueryHost, isRootView} from '../interfaces/type_checks'; -import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HostBindingOpCodes, ID, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view'; +import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, EMBEDDED_VIEW_INJECTOR, FLAGS, HEADER_OFFSET, HOST, HostBindingOpCodes, ID, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view'; import {assertPureTNodeType, assertTNodeType} from '../node_assert'; import {updateTextNode} from '../node_manipulation'; import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher'; @@ -126,11 +126,16 @@ function renderChildComponents(hostLView: LView, components: number[]): void { export function createLView( parentLView: LView|null, tView: TView, context: T|null, flags: LViewFlags, host: RElement|null, tHostNode: TNode|null, rendererFactory: RendererFactory3|null, renderer: Renderer3|null, - sanitizer: Sanitizer|null, injector: Injector|null): LView { + sanitizer: Sanitizer|null, injector: Injector|null, + embeddedViewInjector: Injector|null): LView { const lView = ngDevMode ? cloneToLViewFromTViewBlueprint(tView) : tView.blueprint.slice() as LView; lView[HOST] = host; lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.FirstLViewPass; + if (embeddedViewInjector !== null || + (parentLView && (parentLView[FLAGS] & LViewFlags.HasEmbeddedViewInjector))) { + lView[FLAGS] |= LViewFlags.HasEmbeddedViewInjector; + } resetPreOrderHookFlags(lView); ngDevMode && tView.declTNode && parentLView && assertTNodeForLView(tView.declTNode, parentLView); lView[PARENT] = lView[DECLARATION_VIEW] = parentLView; @@ -143,6 +148,7 @@ export function createLView( lView[INJECTOR as any] = injector || parentLView && parentLView[INJECTOR] || null; lView[T_HOST] = tHostNode; lView[ID] = getUniqueLViewId(); + lView[EMBEDDED_VIEW_INJECTOR as any] = embeddedViewInjector; ngDevMode && assertEqual( tView.type == TViewType.Embedded ? parentLView !== null : true, true, @@ -1499,7 +1505,7 @@ function addComponentLogic(lView: LView, hostTNode: TElementNode, def: Compon createLView( lView, tView, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, native, hostTNode as TElementNode, rendererFactory, rendererFactory.createRenderer(native, def), - null, null)); + null, null, null)); // Component view will always be created before any injected LContainers, // so this is a regular element, wrap it with the component view 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/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 3019740aca8..c09ff165c4b 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -49,6 +49,7 @@ export const DECLARATION_LCONTAINER = 17; export const PREORDER_HOOK_FLAGS = 18; export const QUERIES = 19; export const ID = 20; +export const EMBEDDED_VIEW_INJECTOR = 21; /** * Size of LView's header. Necessary to adjust for it when setting slots. * @@ -56,7 +57,7 @@ export const ID = 20; * instruction index into `LView` index. All other indexes should be in the `LView` index space and * there should be no need to refer to `HEADER_OFFSET` anywhere else. */ -export const HEADER_OFFSET = 21; +export const HEADER_OFFSET = 22; // This interface replaces the real LView interface if it is an arg or a @@ -331,6 +332,12 @@ export interface LView extends Array { /** Unique ID of the view. Used for `__ngContext__` lookups in the `LView` registry. */ [ID]: number; + + /** + * Optional injector assigned to embedded views that takes + * precedence over the element and module injectors. + */ + readonly[EMBEDDED_VIEW_INJECTOR]: Injector|null; } /** Flags associated with an LView (saved in LView[FLAGS]) */ @@ -396,12 +403,15 @@ export const enum LViewFlags { */ RefreshTransplantedView = 0b0010000000000, + /** Indicates that the view **or any of its ancestors** have an embedded view injector. */ + HasEmbeddedViewInjector = 0b0100000000000, + /** * Index of the current init phase on last 21 bits */ - IndexWithinInitPhaseIncrementer = 0b0100000000000, - IndexWithinInitPhaseShift = 11, - IndexWithinInitPhaseReset = 0b0011111111111, + IndexWithinInitPhaseIncrementer = 0b1000000000000, + IndexWithinInitPhaseShift = 12, + IndexWithinInitPhaseReset = 0b001111111111111, } /** diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index a348745229c..1b304bc89a7 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -3546,4 +3546,823 @@ 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 have value from custom injector take precedence over parent 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], + }) + 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 give precedence to value provided lower in the tree over custom injector', () => { + @Directive({selector: 'menu'}) + class Menu { + constructor(@Inject(token) public tokenValue: string) {} + } + + @Directive({ + selector: '[provide-token]', + providers: [{provide: token, useValue: 'hello from directive'}] + }) + class ProvideToken { + } + + @Component({ + template: ` + + +
+
+ +
+
+
+ `, + providers: [{provide: token, useValue: 'hello from parent'}] + }) + class App { + @ViewChild(MenuTrigger) trigger!: MenuTrigger; + @ViewChild(Menu) menu!: Menu; + } + + @NgModule({ + declarations: [App, MenuTrigger, Menu, ProvideToken], + exports: [App, MenuTrigger, Menu, ProvideToken], + }) + 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 directive'); + }); + + it('should give precedence to value provided in custom injector over one provided higher', + () => { + @Directive({selector: 'menu'}) + class Menu { + constructor(@Inject(token) public tokenValue: string) {} + } + + @Directive({ + selector: '[provide-token]', + providers: [{provide: token, useValue: 'hello from directive'}] + }) + class ProvideToken { + } + + @Component({ + template: ` + +
+ + + +
+ `, + providers: [{provide: token, useValue: 'hello from parent'}] + }) + class App { + @ViewChild(MenuTrigger) trigger!: MenuTrigger; + @ViewChild(Menu) menu!: Menu; + } + + @NgModule({ + declarations: [App, MenuTrigger, Menu, ProvideToken], + exports: [App, MenuTrigger, Menu, ProvideToken], + }) + 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 give precedence to value provided lower in the tree over custom injector when crossing view boundaries', + () => { + @Directive({selector: 'menu'}) + class Menu { + constructor(@Inject(token) public tokenValue: string) {} + } + + @Directive({ + selector: '[provide-token]', + providers: [{provide: token, useValue: 'hello from directive'}] + }) + class ProvideToken { + } + + @Component({selector: 'wrapper', template: `
`}) + class Wrapper { + @ViewChild(Menu) menu!: Menu; + } + + @Component({ + template: ` + + +
+ +
+
+ `, + providers: [{provide: token, useValue: 'hello from parent'}] + }) + class App { + @ViewChild(MenuTrigger) trigger!: MenuTrigger; + @ViewChild(Wrapper) wrapper!: Wrapper; + } + + @NgModule({ + declarations: [App, MenuTrigger, Menu, ProvideToken, Wrapper], + exports: [App, MenuTrigger, Menu, ProvideToken, Wrapper], + }) + 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.wrapper.menu.tokenValue).toBe('hello from directive'); + }); + + it('should give precedence to value provided in custom injector over one provided higher when crossing view boundaries', + () => { + @Directive({selector: 'menu'}) + class Menu { + constructor(@Inject(token) public tokenValue: string) {} + } + + @Directive({ + selector: '[provide-token]', + providers: [{provide: token, useValue: 'hello from directive'}] + }) + class ProvideToken { + } + + @Component({selector: 'wrapper', template: `
`}) + class Wrapper { + @ViewChild(Menu) menu!: Menu; + } + + + @Component({ + template: ` + +
+ + + +
+ `, + providers: [{provide: token, useValue: 'hello from parent'}] + }) + class App { + @ViewChild(MenuTrigger) trigger!: MenuTrigger; + @ViewChild(Wrapper) wrapper!: Wrapper; + } + + @NgModule({ + declarations: [App, MenuTrigger, Menu, ProvideToken, Wrapper], + exports: [App, MenuTrigger, Menu, ProvideToken, Wrapper], + }) + 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.wrapper.menu.tokenValue).toBe('hello from injector'); + }); + + it('should not resolve value at insertion location', () => { + @Directive({selector: 'menu'}) + class Menu { + constructor(@Inject(token) public tokenValue: string) {} + } + + @Directive({ + selector: '[provide-token]', + providers: [{provide: token, useValue: 'hello from directive'}] + }) + class ProvideToken { + } + + @Component({ + template: ` +
+ +
+ + + + + `, + providers: [{provide: token, useValue: 'hello from parent'}] + }) + class App { + @ViewChild(MenuTrigger) trigger!: MenuTrigger; + @ViewChild(Menu) menu!: Menu; + } + + @NgModule({ + declarations: [App, MenuTrigger, Menu, ProvideToken], + exports: [App, MenuTrigger, Menu, ProvideToken], + }) + class Module { + } + + TestBed.configureTestingModule({imports: [Module]}); + // Provide an empty injector so we hit the new code path. + 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'); + }); + }); }); diff --git a/packages/core/test/bundling/animations/bundle.golden_symbols.json b/packages/core/test/bundling/animations/bundle.golden_symbols.json index f3973f82dde..a4e1c009a6d 100644 --- a/packages/core/test/bundling/animations/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations/bundle.golden_symbols.json @@ -923,6 +923,9 @@ { "name": "getTNode" }, + { + "name": "getTNodeFromLView" + }, { "name": "getTView" }, @@ -1088,6 +1091,9 @@ { "name": "lookupTokenUsingModuleInjector" }, + { + "name": "lookupTokenUsingNodeInjector" + }, { "name": "makeAnimationEvent" }, diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index 897720b1dea..23cea5238c1 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -206,6 +206,9 @@ { "name": "getSimpleChangesStore" }, + { + "name": "getTNodeFromLView" + }, { "name": "getTView" }, 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 a40a6042d2c..b75e6fe55f3 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -992,6 +992,9 @@ { "name": "getTNode" }, + { + "name": "getTNodeFromLView" + }, { "name": "getTStylingRangeNext" }, @@ -1211,6 +1214,9 @@ { "name": "lookupTokenUsingModuleInjector" }, + { + "name": "lookupTokenUsingNodeInjector" + }, { "name": "makeParamDecorator" }, 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 5eac6303230..c98639fa763 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 @@ -956,6 +956,9 @@ { "name": "getTNode" }, + { + "name": "getTNodeFromLView" + }, { "name": "getTStylingRangeNext" }, @@ -1169,6 +1172,9 @@ { "name": "lookupTokenUsingModuleInjector" }, + { + "name": "lookupTokenUsingNodeInjector" + }, { "name": "makeParamDecorator" }, diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 540527abc6d..495fb19232c 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -161,6 +161,9 @@ { "name": "getSimpleChangesStore" }, + { + "name": "getTNodeFromLView" + }, { "name": "includeViewProviders" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index bf928dec597..08e548644c8 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -1316,6 +1316,9 @@ { "name": "getTNode" }, + { + "name": "getTNodeFromLView" + }, { "name": "getTQuery" }, @@ -1526,6 +1529,9 @@ { "name": "lookupTokenUsingModuleInjector" }, + { + "name": "lookupTokenUsingNodeInjector" + }, { "name": "makeParamDecorator" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 921bfcd03eb..362e7f7159b 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -479,6 +479,9 @@ { "name": "getTNode" }, + { + "name": "getTNodeFromLView" + }, { "name": "getTStylingRangeNext" }, @@ -614,6 +617,9 @@ { "name": "lookupTokenUsingModuleInjector" }, + { + "name": "lookupTokenUsingNodeInjector" + }, { "name": "makeParamDecorator" }, diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 64d20d865f0..e926db11a63 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -236,7 +236,7 @@ describe('di', () => { it('should handle initial undefined state', () => { const contentView = createLView( null, createTView(TViewType.Component, null, null, 1, 0, null, null, null, null, null), - {}, LViewFlags.CheckAlways, null, null, {} as any, {} as any, null, null); + {}, LViewFlags.CheckAlways, null, null, {} as any, {} as any, null, null, null); enterView(contentView); try { const parentTNode = getOrCreateTNode(contentView[TVIEW], 0, TNodeType.Element, null, null); diff --git a/packages/core/test/render3/instructions/shared_spec.ts b/packages/core/test/render3/instructions/shared_spec.ts index c994820aced..6fba4423057 100644 --- a/packages/core/test/render3/instructions/shared_spec.ts +++ b/packages/core/test/render3/instructions/shared_spec.ts @@ -42,7 +42,7 @@ export function enterViewWithOneDiv() { const tNode = tView.firstChild = createTNode(tView, null!, TNodeType.Element, 0, 'div', null); const lView = createLView( null, tView, null, LViewFlags.CheckAlways, null, null, domRendererFactory3, renderer, null, - null); + null, null); lView[HEADER_OFFSET] = div; tView.data[HEADER_OFFSET] = tNode; enterView(lView); diff --git a/packages/core/test/render3/perf/BUILD.bazel b/packages/core/test/render3/perf/BUILD.bazel index 028d1cf456c..5000879a533 100644 --- a/packages/core/test/render3/perf/BUILD.bazel +++ b/packages/core/test/render3/perf/BUILD.bazel @@ -287,3 +287,17 @@ ng_benchmark( name = "render_stringify", bundle = ":render_stringify_lib", ) + +app_bundle( + name = "embedded_view_injector_lib", + entry_point = ":embedded_view_injector/index.ts", + external = ["perf_hooks"], + deps = [ + ":perf_lib", + ], +) + +ng_benchmark( + name = "embedded_view_injector", + bundle = ":embedded_view_injector_lib", +) diff --git a/packages/core/test/render3/perf/directive_instantiate/index.ts b/packages/core/test/render3/perf/directive_instantiate/index.ts index edbd04e4a0f..a1231e3c090 100644 --- a/packages/core/test/render3/perf/directive_instantiate/index.ts +++ b/packages/core/test/render3/perf/directive_instantiate/index.ts @@ -76,7 +76,7 @@ function testTemplate(rf: RenderFlags, ctx: any) { const rootLView = createLView( null, createTView(TViewType.Root, null, null, 0, 0, null, null, null, null, null), {}, - LViewFlags.IsRoot, null, null, null, null, null, null); + LViewFlags.IsRoot, null, null, null, null, null, null, null); const viewTNode = createTNode(null!, null, TNodeType.Element, -1, null, null); const embeddedTView = createTView( diff --git a/packages/core/test/render3/perf/element_text_create/index.ts b/packages/core/test/render3/perf/element_text_create/index.ts index bc7d7d6b910..5c3f69794d1 100644 --- a/packages/core/test/render3/perf/element_text_create/index.ts +++ b/packages/core/test/render3/perf/element_text_create/index.ts @@ -65,7 +65,7 @@ function testTemplate(rf: RenderFlags, ctx: any) { const rootLView = createLView( null, createTView(TViewType.Root, null, null, 0, 0, null, null, null, null, null), {}, - LViewFlags.IsRoot, null, null, null, null, null, null); + LViewFlags.IsRoot, null, null, null, null, null, null, null); const viewTNode = createTNode(null!, null, TNodeType.Element, -1, null, null); const embeddedTView = createTView( diff --git a/packages/core/test/render3/perf/embedded_view_injector/app_component.ts b/packages/core/test/render3/perf/embedded_view_injector/app_component.ts new file mode 100644 index 00000000000..b1c04279eef --- /dev/null +++ b/packages/core/test/render3/perf/embedded_view_injector/app_component.ts @@ -0,0 +1,89 @@ +/*! + * @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 '@angular/core'; +import {RenderFlags, ɵɵadvance, ɵɵdefineComponent, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵreference, ɵɵtemplate, ɵɵtemplateRefExtractor} from '@angular/core/src/render3'; + +import {createInnerComponent} from './inner_component'; +import {createRenderTemplateDirective} from './render_template_directive'; + +/** + * Creates the root component of the benchmark. The goal is to add a few more layers of elements + * between the root and the `ng-template` that renders out the `inner-comp`. + * The template corresponds to: + * + *
+ *
+ *
+ *
+ *
+ *
+ * + *
+ *
+ *
+ *
+ *
+ * + *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */ +export function createAppComponent(injector: Injector|undefined) { + const RenderTemplate = createRenderTemplateDirective(injector); + const InnerComp = createInnerComponent(RenderTemplate); + + function App_ng_template_6_Template(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵelementStart(0, 'div')(1, 'div')(2, 'div')(3, 'div')(4, 'div'); + ɵɵelement(5, 'inner-comp'); + ɵɵelementEnd()()()()(); + } + } + + return class App { + static ɵfac() { + return new App(); + } + + static ɵcmp = ɵɵdefineComponent({ + type: App, + selectors: [['app']], + decls: 8, + vars: 1, + consts: [[3, 'renderTemplate'], ['template', '']], + template: + function App_Template(rf, ctx) { + if (rf & 1) { + ɵɵelementStart(0, 'div')(1, 'div')(2, 'div')(3, 'div'); + ɵɵelement(4, 'div', 0); + ɵɵelementStart(5, 'div'); + ɵɵtemplate( + 6, App_ng_template_6_Template, 6, 0, 'ng-template', null, 1, + ɵɵtemplateRefExtractor); + ɵɵelementEnd()()()()(); + } + if (rf & 2) { + const _r0 = ɵɵreference(7); + ɵɵadvance(4); + ɵɵproperty('renderTemplate', _r0); + } + }, + directives: [RenderTemplate, InnerComp], + encapsulation: 2 + }) as never; + }; +} diff --git a/packages/core/test/render3/perf/embedded_view_injector/index.ts b/packages/core/test/render3/perf/embedded_view_injector/index.ts new file mode 100644 index 00000000000..33d15e57e27 --- /dev/null +++ b/packages/core/test/render3/perf/embedded_view_injector/index.ts @@ -0,0 +1,47 @@ +/** + * @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, ɵɵelement} from '@angular/core'; + +import {RenderFlags} from '../../../../src/render3/interfaces/definition'; +import {createBenchmark} from '../micro_bench'; +import {setupTestHarness} from '../setup'; + +import {createAppComponent} from './app_component'; + + +function template(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵelement(0, 'app'); + } +} + +// App where no injector is provided when creating the embedded views. +const noInjectorApp = createAppComponent(undefined); + +// App where an empty injector is provided when creating the embedded views. We provide an +// empty injector so that the entire view hierarchy has to be traversed during DI. +const withInjectorApp = createAppComponent(Injector.create({providers: []})); + +const noInjectorHarness = setupTestHarness(template, 1, 0, 1, {}, null, [noInjectorApp.ɵcmp]); +const withInjectorHarness = setupTestHarness(template, 1, 0, 1, {}, null, [withInjectorApp.ɵcmp]); + +const benchmark = createBenchmark('embedded_view_injector'); +const noEmbeddedInjectorTime = benchmark('no injector'); +const withEmbeddedInjectorTime = benchmark('with embedded view injector'); + +while (noEmbeddedInjectorTime()) { + noInjectorHarness.createEmbeddedLView(); + noInjectorHarness.detectChanges(); +} + +while (withEmbeddedInjectorTime()) { + withInjectorHarness.createEmbeddedLView(); + withInjectorHarness.detectChanges(); +} + +benchmark.report(); diff --git a/packages/core/test/render3/perf/embedded_view_injector/injector_component.ts b/packages/core/test/render3/perf/embedded_view_injector/injector_component.ts new file mode 100644 index 00000000000..8197826a390 --- /dev/null +++ b/packages/core/test/render3/perf/embedded_view_injector/injector_component.ts @@ -0,0 +1,41 @@ +/*! + * @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 {InjectFlags, InjectionToken, ɵɵdefineComponent, ɵɵtext} from '@angular/core'; +import {RenderFlags, ɵɵdirectiveInject, ɵɵtextInterpolate1} from '@angular/core/src/render3'; + +const token = new InjectionToken('token'); + +/** + * Leaf component that tries to inject a token. + * Template corresponds to `Hello {{tokenValue}}` + */ +export class InjectorComp { + static ɵcmp = ɵɵdefineComponent({ + type: InjectorComp, + selectors: [['injector-comp']], + decls: 1, + vars: 1, + encapsulation: 2, + template: + function InjectorComp_Template(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵtext(0); + } + if (rf & 2) { + ɵɵtextInterpolate1('Hello ', ctx.tokenValue, ''); + } + } + }); + + static ɵfac() { + return new InjectorComp(ɵɵdirectiveInject(token, InjectFlags.Optional)); + } + + constructor(public tokenValue: string) {} +} diff --git a/packages/core/test/render3/perf/embedded_view_injector/inner_component.ts b/packages/core/test/render3/perf/embedded_view_injector/inner_component.ts new file mode 100644 index 00000000000..4b01536b3f4 --- /dev/null +++ b/packages/core/test/render3/perf/embedded_view_injector/inner_component.ts @@ -0,0 +1,86 @@ +/*! + * @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 {Type, ɵɵadvance, ɵɵdefineComponent, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵreference, ɵɵtemplate, ɵɵtemplateRefExtractor} from '@angular/core'; +import {RenderFlags} from '@angular/core/src/render3'; + +import {InjectorComp} from './injector_component'; + +/** + * Creates a component that will be rendered inside the main app that adds a few layers of elements + * between the root and where the template with the injector component will be rendered. + * Template corresponds to: + * + * + *
+ *
+ *
+ *
+ *
+ *
+ * + *
+ *
+ *
+ *
+ *
+ * + *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */ +export function createInnerComponent(renderTemplateDirective: Type<{}>) { + function InnerComp_ng_template_6_Template(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵelementStart(0, 'div')(1, 'div')(2, 'div')(3, 'div')(4, 'div'); + ɵɵelement(5, 'injector-comp'); + ɵɵelementEnd()()()()(); + } + } + + return class InnerComp { + static ɵfac() { + return new InnerComp(); + } + + static ɵcmp = ɵɵdefineComponent({ + type: InnerComp, + selectors: [['inner-comp']], + decls: 8, + vars: 1, + consts: [[3, 'renderTemplate'], ['template', '']], + template: + function InnerComp_Template(rf, ctx: any) { + if (rf & 1) { + ɵɵelementStart(0, 'div')(1, 'div')(2, 'div')(3, 'div'); + ɵɵelement(4, 'div', 0); + ɵɵelementStart(5, 'div'); + ɵɵtemplate( + 6, InnerComp_ng_template_6_Template, 6, 0, 'ng-template', null, 1, + ɵɵtemplateRefExtractor); + ɵɵelementEnd()()()()(); + } + if (rf & 2) { + const _r0 = ɵɵreference(7); + ɵɵadvance(4); + ɵɵproperty('renderTemplate', _r0); + } + }, + directives: [renderTemplateDirective, InjectorComp], + encapsulation: 2 + }); + }; +} diff --git a/packages/core/test/render3/perf/embedded_view_injector/render_template_directive.ts b/packages/core/test/render3/perf/embedded_view_injector/render_template_directive.ts new file mode 100644 index 00000000000..cbaed8ecf7c --- /dev/null +++ b/packages/core/test/render3/perf/embedded_view_injector/render_template_directive.ts @@ -0,0 +1,45 @@ +/*! + * @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, TemplateRef, ViewContainerRef, ɵɵdefineDirective, ɵɵdirectiveInject} from '@angular/core'; +import {injectViewContainerRef} from '@angular/core/src/linker/view_container_ref'; + + +class ViewContainerRefToken { + /** + * @internal + * @nocollapse + */ + static __NG_ELEMENT_ID__(): ViewContainerRef { + return injectViewContainerRef(); + } +} + +/** + * Creates a helper directive that renders out a template + * reference that is passed in as an input. + */ +export function createRenderTemplateDirective(injector: Injector|undefined) { + return class RenderTemplate { + static ɵfac() { + return new RenderTemplate(ɵɵdirectiveInject(ViewContainerRefToken as any)); + } + + static ɵdir = ɵɵdefineDirective({ + type: RenderTemplate, + selectors: [['', 'renderTemplate', '']], + inputs: {template: ['renderTemplate', 'template']} + }); + + constructor(public viewContainerRef: ViewContainerRef) {} + + set template(template: TemplateRef) { + this.viewContainerRef.createEmbeddedView(template, undefined, {injector}); + } + }; +} diff --git a/packages/core/test/render3/perf/listeners/index.ts b/packages/core/test/render3/perf/listeners/index.ts index 834483b2ff2..db33c2f81d8 100644 --- a/packages/core/test/render3/perf/listeners/index.ts +++ b/packages/core/test/render3/perf/listeners/index.ts @@ -67,7 +67,7 @@ function testTemplate(rf: RenderFlags, ctx: any) { const rootLView = createLView( null, createTView(TViewType.Root, null, null, 0, 0, null, null, null, null, null), {}, - LViewFlags.IsRoot, null, null, null, null, null, null); + LViewFlags.IsRoot, null, null, null, null, null, null, null); const viewTNode = createTNode(null!, null, TNodeType.Element, -1, null, null); const embeddedTView = createTView( diff --git a/packages/core/test/render3/perf/ng_template/index.ts b/packages/core/test/render3/perf/ng_template/index.ts index 5e7b929683e..b6dabbe71ca 100644 --- a/packages/core/test/render3/perf/ng_template/index.ts +++ b/packages/core/test/render3/perf/ng_template/index.ts @@ -7,6 +7,7 @@ */ import {injectTemplateRef} from '@angular/core/src/linker/template_ref'; import {injectViewContainerRef} from '@angular/core/src/linker/view_container_ref'; + import {TemplateRef, ViewContainerRef} from '../../../../src/linker'; import {ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵtemplate} from '../../../../src/render3/index'; import {createLView, createTNode, createTView} from '../../../../src/render3/instructions/shared'; @@ -63,7 +64,7 @@ function testTemplate(rf: RenderFlags, ctx: any) { const rootLView = createLView( null, createTView(TViewType.Root, null, null, 0, 0, null, null, null, null, null), {}, - LViewFlags.IsRoot, null, null, null, null, null, null); + LViewFlags.IsRoot, null, null, null, null, null, null, null); const viewTNode = createTNode(null!, null, TNodeType.Element, -1, null, null); const embeddedTView = createTView( diff --git a/packages/core/test/render3/perf/noop_renderer.ts b/packages/core/test/render3/perf/noop_renderer.ts index 0d2795ea2af..0c1d6de9fad 100644 --- a/packages/core/test/render3/perf/noop_renderer.ts +++ b/packages/core/test/render3/perf/noop_renderer.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {RComment, RElement, RNode, RText} from '@angular/core/src/render3/interfaces/renderer_dom'; + import {ProceduralRenderer3, Renderer3, RendererFactory3, RendererStyleFlags3} from '../../../src/render3/interfaces/renderer'; export class MicroBenchmarkRenderNode implements RNode, RComment, RText { @@ -49,7 +50,7 @@ export class MicroBenchmarkRenderer implements ProceduralRenderer3 { return null; } nextSibling(node: RNode): RNode|null { - throw new Error('Method not implemented.'); + return null; } setAttribute(el: RElement, name: string, value: string, namespace?: string|null|undefined): void { if (name === 'class' && isOurNode(el)) { diff --git a/packages/core/test/render3/perf/setup.ts b/packages/core/test/render3/perf/setup.ts index 60043f1a2f6..7c899272705 100644 --- a/packages/core/test/render3/perf/setup.ts +++ b/packages/core/test/render3/perf/setup.ts @@ -23,7 +23,7 @@ export function createAndRenderLView( parentLView: LView, tView: TView, hostTNode: TElementNode): LView { const embeddedLView = createLView( parentLView, tView, {}, LViewFlags.CheckAlways, null, hostTNode, rendererFactory, renderer, - null, null); + null, null, null); renderView(tView, embeddedLView, null); return embeddedLView; } @@ -55,7 +55,7 @@ export function setupTestHarness( const hostNode = renderer.createElement('div'); const hostLView = createLView( null, hostTView, {}, LViewFlags.CheckAlways | LViewFlags.IsRoot, hostNode, null, - rendererFactory, renderer, null, null); + rendererFactory, renderer, null, null, null); const mockRCommentNode = renderer.createComment(''); const lContainer = createLContainer(mockRCommentNode, hostLView, mockRCommentNode, tContainerNode); @@ -71,7 +71,7 @@ export function setupTestHarness( function createEmbeddedLView(): LView { const embeddedLView = createLView( hostLView, embeddedTView, embeddedViewContext, LViewFlags.CheckAlways, null, viewTNode, - rendererFactory, renderer, null, null); + rendererFactory, renderer, null, null, null); renderView(embeddedTView, embeddedLView, embeddedViewContext); return embeddedLView; } diff --git a/packages/core/test/render3/perf/view_destroy_hook/index.ts b/packages/core/test/render3/perf/view_destroy_hook/index.ts index e96c76f9c02..360147f6139 100644 --- a/packages/core/test/render3/perf/view_destroy_hook/index.ts +++ b/packages/core/test/render3/perf/view_destroy_hook/index.ts @@ -53,7 +53,7 @@ function testTemplate(rf: RenderFlags, ctx: any) { const rootLView = createLView( null, createTView(TViewType.Root, null, null, 0, 0, null, null, null, null, null), {}, - LViewFlags.IsRoot, null, null, null, null, null, null); + LViewFlags.IsRoot, null, null, null, null, null, null, null); const viewTNode = createTNode(null!, null, TNodeType.Element, -1, null, null); const embeddedTView = createTView( diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index f9779379d5a..bc274ed4565 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -292,7 +292,7 @@ export function renderTemplate( const tView = createTView(TViewType.Root, null, null, 1, 0, null, null, null, null, null); const hostLView = createLView( null, tView, {}, LViewFlags.CheckAlways | LViewFlags.IsRoot, null, null, - providedRendererFactory, renderer, null, null); + providedRendererFactory, renderer, null, null, null); enterView(hostLView); const def = ɵɵdefineComponent({ @@ -310,7 +310,7 @@ export function renderTemplate( hostLView[hostTNode.index] = hostNode; componentView = createLView( hostLView, componentTView, context, LViewFlags.CheckAlways, hostNode, hostTNode, - providedRendererFactory, renderer, sanitizer || null, null); + providedRendererFactory, renderer, sanitizer || null, null, null); } renderComponentOrTemplate(componentView[TVIEW], componentView, templateFn, context); return componentView; diff --git a/packages/core/test/render3/view_fixture.ts b/packages/core/test/render3/view_fixture.ts index e7dd36706be..90345c19536 100644 --- a/packages/core/test/render3/view_fixture.ts +++ b/packages/core/test/render3/view_fixture.ts @@ -48,7 +48,7 @@ export class ViewFixture { const hostTView = createTView(TViewType.Root, null, null, 1, 0, null, null, null, null, null); const hostLView = createLView( null, hostTView, {}, LViewFlags.CheckAlways | LViewFlags.IsRoot, null, null, - domRendererFactory3, hostRenderer, null, null); + domRendererFactory3, hostRenderer, null, null, null); this.tView = createTView( @@ -58,7 +58,7 @@ export class ViewFixture { createTNode(hostTView, null, TNodeType.Element, 0, 'host-element', null) as TElementNode; this.lView = createLView( hostLView, this.tView, context || {}, LViewFlags.CheckAlways, this.host, hostTNode, - domRendererFactory3, hostRenderer, null, null); + domRendererFactory3, hostRenderer, null, null, null); } /**