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
This commit is contained in:
Kristiyan Kostadinov 2022-02-28 19:49:52 +01:00 committed by Jessica Janiuk
parent 9366a3c5f3
commit 94c949a60a
40 changed files with 1598 additions and 178 deletions

View file

@ -585,8 +585,9 @@ export class NgTemplateOutlet implements OnChanges {
ngOnChanges(changes: SimpleChanges): void;
ngTemplateOutlet: TemplateRef<any> | null;
ngTemplateOutletContext: Object | null;
ngTemplateOutletInjector: Injector | null;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<NgTemplateOutlet, "[ngTemplateOutlet]", never, { "ngTemplateOutletContext": "ngTemplateOutletContext"; "ngTemplateOutlet": "ngTemplateOutlet"; }, {}, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<NgTemplateOutlet, "[ngTemplateOutlet]", never, { "ngTemplateOutletContext": "ngTemplateOutletContext"; "ngTemplateOutlet": "ngTemplateOutlet"; "ngTemplateOutletInjector": "ngTemplateOutletInjector"; }, {}, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<NgTemplateOutlet, never>;
}

View file

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

View file

@ -15,8 +15,8 @@
"master": {
"uncompressed": {
"runtime": 4343,
"main": 450179,
"polyfills": 37823,
"main": 450900,
"polyfills": 37817,
"styles": 70416,
"light-theme": 77582,
"dark-theme": 77711

View file

@ -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
}
}
},

View file

@ -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<any>|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;

View file

@ -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<InjectedComponent> = 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');

View file

@ -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 = `<ng-template #tpl><inject-value></inject-value></ng-template>` +
`<ng-container *ngTemplateOutlet="tpl; injector: injector"></ng-container>`;
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 = `<ng-template #tpl><inject-value></inject-value></ng-template>` +
`<ng-container *ngTemplateOutlet="tpl; injector: injector"></ng-container>`;
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 = `<ng-template #tpl><inject-value></inject-value></ng-template>` +
`<ng-container *ngTemplateOutlet="tpl; injector: injector"></ng-container>`;
fixture = createTestComponent(template, [{provide: templateToken, useValue: 'parent'}]);
fixture.componentInstance.injector =
Injector.create({providers: [{provide: templateToken, useValue: 'world'}]});
detectChangesAndExpectText('Hello world');
}));
});
const templateToken = new InjectionToken<string>('templateToken');
@Injectable()
class DestroyedSpyService {
destroyed = false;
@ -290,6 +329,15 @@ class TestComponent {
currentTplRef!: TemplateRef<any>;
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<TestComponent> {
return TestBed.overrideComponent(TestComponent, {set: {template: template}})
function createTestComponent(
template: string, providers: Provider[] = []): ComponentFixture<TestComponent> {
return TestBed.overrideComponent(TestComponent, {set: {template: template, providers}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]})
.createComponent(TestComponent);
}

View file

@ -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<C> {
* and attaches it to the view container.
* @param context The data-binding context of the embedded view, as declared
* in the `<ng-template>` usage.
* @param injector Injector to be used within the embedded view.
* @returns The new embedded view object.
*/
abstract createEmbeddedView(context: C): EmbeddedViewRef<C>;
abstract createEmbeddedView(context: C, injector?: Injector): EmbeddedViewRef<C>;
/**
* @internal
@ -77,11 +79,11 @@ const R3TemplateRef = class TemplateRef<T> extends ViewEngineTemplateRef<T> {
super();
}
override createEmbeddedView(context: T): EmbeddedViewRef<T> {
override createEmbeddedView(context: T, injector?: Injector): EmbeddedViewRef<T> {
const embeddedTView = this._declarationTContainer.tViews as TView;
const embeddedLView = createLView(
this._declarationLView, embeddedTView, context, LViewFlags.CheckAlways, null,
embeddedTView.declTNode, null, null, null, null);
embeddedTView.declTNode, null, null, null, null, injector || null);
const declarationLContainer = this._declarationLView[this._declarationTContainer.index];
ngDevMode && assertLContainer(declarationLContainer);

View file

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

View file

@ -134,7 +134,7 @@ export function renderComponent<T>(
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);

View file

@ -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: <T>(token: ProviderToken<T>, 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<T>(token: ProviderToken<T>, 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<T> extends viewEngine_ComponentFactory<T> {
ngModule?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<T> {
ngModule = ngModule || this.ngModule;
const rootViewInjector =
ngModule ? createChainedInjector(injector, ngModule.injector) : injector;
const rootViewInjector = ngModule ? new ChainedInjector(injector, ngModule.injector) : injector;
const rendererFactory =
rootViewInjector.get(RendererFactory2, domRendererFactory3) as RendererFactory3;
rootViewInjector.get(RendererFactory2, domRendererFactory3 as RendererFactory2) as
RendererFactory3;
const sanitizer = rootViewInjector.get(Sanitizer, null);
const hostRenderer = rendererFactory.createRenderer(null, this.componentDef);
@ -150,7 +154,7 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
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

View file

@ -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<T>(
tNode: TDirectiveHostNode|null, lView: LView, token: ProviderToken<T>,
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<T>(notFoundValue, token, flags) :
lookupTokenUsingModuleInjector<T>(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<T>(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<T>(
tNode: TDirectiveHostNode, lView: LView, token: ProviderToken<T>, 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<T>(notFoundValue, token, flags) :
lookupTokenUsingModuleInjector<T>(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<T>(
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<T>(
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<T>(lView, token, flags, notFoundValue);
}
const NOT_FOUND = {};
export function createNodeInjector(): Injector {
return new NodeInjector(getCurrentTNode()! as TDirectiveHostNode, getLView()) as any;
return notFoundValue;
}
function searchTokensOnInjector<T>(
@ -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<T>(type: Type<any>): ((type?: Type<T>) => T | null)|null {
}
return getFactoryDef<T>(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<T>(
tNode: TDirectiveHostNode, lView: LView, token: ProviderToken<T>, 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;
}

View file

@ -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<T>(
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<T>(
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<T>(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

View file

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

View file

@ -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<any> {
/** 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,
}
/**

View file

@ -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<string>('greeting');
@Directive({selector: 'menu-trigger'})
class MenuTrigger {
@Input('triggerFor') menu!: TemplateRef<unknown>;
constructor(private viewContainerRef: ViewContainerRef) {}
open(injector: Injector|undefined) {
this.viewContainerRef.createEmbeddedView(this.menu, undefined, {injector});
}
}
it('should be able to provide an injection token through a custom injector', () => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
TestBed.configureTestingModule({declarations: [App, MenuTrigger, Menu]});
const injector = Injector.create({providers: [{provide: token, useValue: 'hello'}]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.tokenValue).toBe('hello');
});
it('should be able to provide an injection token to a nested template through a custom injector',
() => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger #outerTrigger [triggerFor]="outerTemplate"></menu-trigger>
<ng-template #outerTemplate>
<menu></menu>
<menu-trigger #innerTrigger [triggerFor]="innerTemplate"></menu-trigger>
<ng-template #innerTemplate>
<menu #innerMenu></menu>
</ng-template>
</ng-template>
`
})
class App {
@ViewChild('outerTrigger', {read: MenuTrigger}) outerTrigger!: MenuTrigger;
@ViewChild('innerTrigger', {read: MenuTrigger}) innerTrigger!: MenuTrigger;
@ViewChild('innerMenu', {read: Menu}) innerMenu!: Menu;
}
TestBed.configureTestingModule({declarations: [App, MenuTrigger, Menu]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.outerTrigger.open(
Injector.create({providers: [{provide: token, useValue: 'hello'}]}));
fixture.detectChanges();
fixture.componentInstance.innerTrigger.open(undefined);
fixture.detectChanges();
expect(fixture.componentInstance.innerMenu.tokenValue).toBe('hello');
});
it('should be able to resolve a token from a custom grandparent injector if the token is not provided in the parent',
() => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger #grandparentTrigger [triggerFor]="grandparentTemplate"></menu-trigger>
<ng-template #grandparentTemplate>
<menu></menu>
<menu-trigger #parentTrigger [triggerFor]="parentTemplate"></menu-trigger>
<ng-template #parentTemplate>
<menu></menu>
<menu-trigger #childTrigger [triggerFor]="childTemplate"></menu-trigger>
<ng-template #childTemplate>
<menu #childMenu></menu>
</ng-template>
</ng-template>
</ng-template>
`
})
class App {
@ViewChild('grandparentTrigger', {read: MenuTrigger}) grandparentTrigger!: MenuTrigger;
@ViewChild('parentTrigger', {read: MenuTrigger}) parentTrigger!: MenuTrigger;
@ViewChild('childTrigger', {read: MenuTrigger}) childTrigger!: MenuTrigger;
@ViewChild('childMenu', {read: Menu}) childMenu!: Menu;
}
TestBed.configureTestingModule({declarations: [App, MenuTrigger, Menu]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.grandparentTrigger.open(
Injector.create({providers: [{provide: token, useValue: 'hello'}]}));
fixture.detectChanges();
fixture.componentInstance.parentTrigger.open(Injector.create({providers: []}));
fixture.detectChanges();
fixture.componentInstance.childTrigger.open(undefined);
fixture.detectChanges();
expect(fixture.componentInstance.childMenu.tokenValue).toBe('hello');
});
it('should resolve value from node injector if it is lower than embedded view injector', () => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
selector: 'wrapper',
providers: [{provide: token, useValue: 'hello from wrapper'}],
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class Wrapper {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<wrapper></wrapper>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Wrapper) wrapper!: Wrapper;
}
TestBed.configureTestingModule({declarations: [App, MenuTrigger, Menu, Wrapper]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(
Injector.create({providers: [{provide: token, useValue: 'hello from injector'}]}));
fixture.detectChanges();
fixture.componentInstance.wrapper.trigger.open(undefined);
fixture.detectChanges();
expect(fixture.componentInstance.wrapper.menu.tokenValue).toBe('hello from wrapper');
});
it('should be able to inject a value provided at the module level', () => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@NgModule({
declarations: [App, MenuTrigger, Menu],
exports: [App, MenuTrigger, Menu],
providers: [{provide: token, useValue: 'hello'}]
})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const injector = Injector.create({providers: []});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.tokenValue).toBe('hello');
});
it('should have value from custom injector take precedence over module injector', () => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@NgModule({
declarations: [App, MenuTrigger, Menu],
exports: [App, MenuTrigger, Menu],
providers: [{provide: token, useValue: 'hello from module'}]
})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const injector =
Injector.create({providers: [{provide: token, useValue: 'hello from injector'}]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.tokenValue).toBe('hello from injector');
});
it('should have value from custom injector take precedence over parent injector', () => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`,
providers: [{provide: token, useValue: 'hello from parent'}]
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@NgModule({
declarations: [App, MenuTrigger, Menu],
exports: [App, MenuTrigger, Menu],
})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const injector =
Injector.create({providers: [{provide: token, useValue: 'hello from injector'}]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.tokenValue).toBe('hello from injector');
});
it('should be able to inject built-in tokens when a custom injector is provided', () => {
@Directive({selector: 'menu'})
class Menu {
constructor(public elementRef: ElementRef, public changeDetectorRef: ChangeDetectorRef) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
TestBed.configureTestingModule({declarations: [App, MenuTrigger, Menu]});
const injector = Injector.create({providers: []});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.elementRef.nativeElement)
.toBe(fixture.nativeElement.querySelector('menu'));
expect(fixture.componentInstance.menu.changeDetectorRef).toBeTruthy();
});
it('should have value from parent component injector take precedence over module injector',
() => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`,
providers: [{provide: token, useValue: 'hello from parent'}]
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@NgModule({
declarations: [App, MenuTrigger, Menu],
exports: [App, MenuTrigger, Menu],
providers: [{provide: token, useValue: 'hello from module'}]
})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const injector = Injector.create({providers: []});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.tokenValue).toBe('hello from parent');
});
it('should be able to inject an injectable with dependencies', () => {
@Injectable()
class Greeter {
constructor(@Inject(token) private tokenValue: string) {}
greet() {
return `hello from ${this.tokenValue}`;
}
}
@Directive({selector: 'menu'})
class Menu {
constructor(public greeter: Greeter) {}
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@NgModule({
declarations: [App, MenuTrigger, Menu],
exports: [App, MenuTrigger, Menu],
providers: [{provide: token, useValue: 'module'}]
})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const injector = Injector.create({
providers: [
{provide: Greeter, useClass: Greeter},
{provide: token, useValue: 'injector'},
]
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.menu.greeter.greet()).toBe('hello from injector');
});
it('should be able to inject a value from a grandparent component when a custom injector is provided',
() => {
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) public tokenValue: string) {}
}
@Component({
selector: 'parent',
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`
})
class Parent {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@Component({
template: '<parent></parent>',
providers: [{provide: token, useValue: 'hello from grandparent'}]
})
class GrandParent {
@ViewChild(Parent) parent!: Parent;
}
TestBed.configureTestingModule({declarations: [GrandParent, Parent, MenuTrigger, Menu]});
const injector = Injector.create({providers: []});
const fixture = TestBed.createComponent(GrandParent);
fixture.detectChanges();
fixture.componentInstance.parent.trigger.open(injector);
fixture.detectChanges();
expect(fixture.componentInstance.parent.menu.tokenValue).toBe('hello from grandparent');
});
it('should be able to use a custom injector when created through TemplateRef', () => {
let injectedValue: string|undefined;
@Directive({selector: 'menu'})
class Menu {
constructor(@Inject(token) tokenValue: string) {
injectedValue = tokenValue;
}
}
@Component({
template: `
<ng-template>
<menu></menu>
</ng-template>
`
})
class App {
@ViewChild(TemplateRef) template!: TemplateRef<unknown>;
}
@NgModule({
declarations: [App, Menu],
exports: [App, Menu],
providers: [{provide: token, useValue: 'hello from module'}]
})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const injector =
Injector.create({providers: [{provide: token, useValue: 'hello from injector'}]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.template.createEmbeddedView({}, injector);
fixture.detectChanges();
expect(injectedValue).toBe('hello from injector');
});
it('should use a custom injector when the view is created outside of the declaration view',
() => {
const declarerToken = new InjectionToken<string>('declarerToken');
const creatorToken = new InjectionToken<string>('creatorToken');
@Directive({selector: 'menu'})
class Menu {
constructor(
@Inject(token) public tokenValue: string,
@Optional() @Inject(declarerToken) public declarerTokenValue: string,
@Optional() @Inject(creatorToken) public creatorTokenValue: string) {}
}
@Component({
selector: 'declarer',
template: '<ng-template><menu></menu></ng-template>',
providers: [{provide: declarerToken, useValue: 'hello from declarer'}]
})
class Declarer {
@ViewChild(Menu) menu!: Menu;
@ViewChild(TemplateRef) template!: TemplateRef<unknown>;
}
@Component({
selector: 'creator',
template: '<menu-trigger></menu-trigger>',
providers: [{provide: creatorToken, useValue: 'hello from creator'}]
})
class Creator {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
}
@Component({
template: `
<declarer></declarer>
<creator></creator>
`
})
class App {
@ViewChild(Declarer) declarer!: Declarer;
@ViewChild(Creator) creator!: Creator;
}
TestBed.configureTestingModule(
{declarations: [App, MenuTrigger, Menu, Declarer, Creator]});
const injector = Injector.create({providers: [{provide: token, useValue: 'hello'}]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const {declarer, creator} = fixture.componentInstance;
creator.trigger.menu = declarer.template;
creator.trigger.open(injector);
fixture.detectChanges();
expect(declarer.menu.tokenValue).toBe('hello');
expect(declarer.menu.declarerTokenValue).toBe('hello from declarer');
expect(declarer.menu.creatorTokenValue).toBeNull();
});
it('should 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: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<section>
<div provide-token>
<menu></menu>
</div>
</section>
</ng-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: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<div provide-token>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
</div>
`,
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: `<div><menu></menu></div>`})
class Wrapper {
@ViewChild(Menu) menu!: Menu;
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<ng-template #menuTemplate>
<section provide-token>
<wrapper></wrapper>
</section>
</ng-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: `<div><menu></menu></div>`})
class Wrapper {
@ViewChild(Menu) menu!: Menu;
}
@Component({
template: `
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
<div provide-token>
<ng-template #menuTemplate>
<wrapper></wrapper>
</ng-template>
</div>
`,
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: `
<div provide-token>
<menu-trigger [triggerFor]="menuTemplate"></menu-trigger>
</div>
<ng-template #menuTemplate>
<menu></menu>
</ng-template>
`,
providers: [{provide: token, useValue: 'hello from parent'}]
})
class App {
@ViewChild(MenuTrigger) trigger!: MenuTrigger;
@ViewChild(Menu) menu!: Menu;
}
@NgModule({
declarations: [App, MenuTrigger, Menu, 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');
});
});
});

View file

@ -923,6 +923,9 @@
{
"name": "getTNode"
},
{
"name": "getTNodeFromLView"
},
{
"name": "getTView"
},
@ -1088,6 +1091,9 @@
{
"name": "lookupTokenUsingModuleInjector"
},
{
"name": "lookupTokenUsingNodeInjector"
},
{
"name": "makeAnimationEvent"
},

View file

@ -206,6 +206,9 @@
{
"name": "getSimpleChangesStore"
},
{
"name": "getTNodeFromLView"
},
{
"name": "getTView"
},

View file

@ -992,6 +992,9 @@
{
"name": "getTNode"
},
{
"name": "getTNodeFromLView"
},
{
"name": "getTStylingRangeNext"
},
@ -1211,6 +1214,9 @@
{
"name": "lookupTokenUsingModuleInjector"
},
{
"name": "lookupTokenUsingNodeInjector"
},
{
"name": "makeParamDecorator"
},

View file

@ -956,6 +956,9 @@
{
"name": "getTNode"
},
{
"name": "getTNodeFromLView"
},
{
"name": "getTStylingRangeNext"
},
@ -1169,6 +1172,9 @@
{
"name": "lookupTokenUsingModuleInjector"
},
{
"name": "lookupTokenUsingNodeInjector"
},
{
"name": "makeParamDecorator"
},

View file

@ -161,6 +161,9 @@
{
"name": "getSimpleChangesStore"
},
{
"name": "getTNodeFromLView"
},
{
"name": "includeViewProviders"
},

View file

@ -1316,6 +1316,9 @@
{
"name": "getTNode"
},
{
"name": "getTNodeFromLView"
},
{
"name": "getTQuery"
},
@ -1526,6 +1529,9 @@
{
"name": "lookupTokenUsingModuleInjector"
},
{
"name": "lookupTokenUsingNodeInjector"
},
{
"name": "makeParamDecorator"
},

View file

@ -479,6 +479,9 @@
{
"name": "getTNode"
},
{
"name": "getTNodeFromLView"
},
{
"name": "getTStylingRangeNext"
},
@ -614,6 +617,9 @@
{
"name": "lookupTokenUsingModuleInjector"
},
{
"name": "lookupTokenUsingNodeInjector"
},
{
"name": "makeParamDecorator"
},

View file

@ -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);

View file

@ -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);

View file

@ -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",
)

View file

@ -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(

View file

@ -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(

View file

@ -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:
*
* <div>
* <div>
* <div>
* <div>
* <div [renderTemplate]="template"></div>
* <div>
* <ng-template #template>
* <div>
* <div>
* <div>
* <div>
* <div>
* <inner-comp></inner-comp>
* </div>
* </div>
* </div>
* </div>
* </div>
* </ng-template>
* </div>
* </div>
* </div>
* </div>
* </div>
*/
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;
};
}

View file

@ -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();

View file

@ -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<string>('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) {}
}

View file

@ -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:
*
*
* <div>
* <div>
* <div>
* <div>
* <div [renderTemplate]="template"></div>
* <div>
* <ng-template #template>
* <div>
* <div>
* <div>
* <div>
* <div>
* <injector-comp></injector-comp>
* </div>
* </div>
* </div>
* </div>
* </div>
* </ng-template>
* </div>
* </div>
* </div>
* </div>
* </div>
*/
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
});
};
}

View file

@ -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<any>) {
this.viewContainerRef.createEmbeddedView(template, undefined, {injector});
}
};
}

View file

@ -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(

View file

@ -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(

View file

@ -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)) {

View file

@ -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;
}

View file

@ -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(

View file

@ -292,7 +292,7 @@ export function renderTemplate<T>(
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<T>(
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;

View file

@ -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);
}
/**