Skip to content

Commit 822439f

Browse files
AndrewKushnirdylhunn
authored andcommitted
fix(core): ViewContainerRef.createComponent should consult module injector when custom one is provided (#44966)
Before Ivy, it was only possible to call the `ViewContainerRef.createComponent` function with the ComponentFactory as the first argument. An instance of a `ComponentFactory` resolved via `ComponentFactoryResolver` contained a reference to an `NgModule` where the component is declared. As a result, the component maintained a DI connection with the module injector tree (by retrieving an instance of `NgModuleRef` internally), even when the custom injector was provided (we try to find a token in a custom injector first and consult module injector after that). With Ivy, we expanded the `ViewContainerRef.createComponent` function API to support direct references to the Component classes without going through the factory resolution step. As a result, there was no connection to the NgModule that declares the component. Thus, if you provide a custom injector, this is the only injector that is taken into account. This commit updates the logic for the factory-less case to try retrieving an instance of an `NgModuleRef` using the DI tree which `ViewContainerRef` belongs to. The `NgModuleRef` instance is then used to get a hold of a module injector tree. This brings the factory-less and factory-based logic to more consistent state. Closes #44897. PR Close #44966
1 parent 1b91e10 commit 822439f

File tree

2 files changed

+75
-12
lines changed

2 files changed

+75
-12
lines changed

packages/core/src/linker/view_container_ref.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -337,11 +337,31 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
337337
componentFactoryOrType as ComponentFactory<C>:
338338
new R3ComponentFactory(getComponentDef(componentFactoryOrType)!);
339339
const contextInjector = injector || this.parentInjector;
340-
if (!ngModuleRef && (componentFactory as any).ngModule == null && contextInjector) {
341-
// DO NOT REFACTOR. The code here used to have a `value || undefined` expression
342-
// which seems to cause internal google apps to fail. This is documented in the
343-
// following internal bug issue: go/b/142967802
344-
const result = contextInjector.get(NgModuleRef, null);
340+
341+
// If an `NgModuleRef` is not provided explicitly, try retrieving it from the DI tree.
342+
if (!ngModuleRef && (componentFactory as any).ngModule == null) {
343+
// For the `ComponentFactory` case, entering this logic is very unlikely, since we expect that
344+
// an instance of a `ComponentFactory`, resolved via `ComponentFactoryResolver` would have an
345+
// `ngModule` field. This is possible in some test scenarios and potentially in some JIT-based
346+
// use-cases. For the `ComponentFactory` case we preserve backwards-compatibility and try
347+
// using a provided injector first, then fall back to the parent injector of this
348+
// `ViewContainerRef` instance.
349+
//
350+
// For the factory-less case, it's critical to establish a connection with the module
351+
// injector tree (by retrieving an instance of an `NgModuleRef` and accessing its injector),
352+
// so that a component can use DI tokens provided in MgModules. For this reason, we can not
353+
// rely on the provided injector, since it might be detached from the DI tree (for example, if
354+
// it was created via `Injector.create` without specifying a parent injector, or if an
355+
// injector is retrieved from an `NgModuleRef` created via `createNgModuleRef` using an
356+
// NgModule outside of a module tree). Instead, we always use `ViewContainerRef`'s parent
357+
// injector, which is normally connected to the DI tree, which includes module injector
358+
// subtree.
359+
const _injector = isComponentFactory ? contextInjector : this.parentInjector;
360+
361+
// DO NOT REFACTOR. The code here used to have a `injector.get(NgModuleRef, null) ||
362+
// undefined` expression which seems to cause internal google apps to fail. This is documented
363+
// in the following internal bug issue: go/b/142967802
364+
const result = _injector.get(NgModuleRef, null);
345365
if (result) {
346366
ngModuleRef = result;
347367
}

packages/core/test/acceptance/view_container_ref_spec.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
import {CommonModule, DOCUMENT} from '@angular/common';
1010
import {computeMsgId} from '@angular/compiler';
11-
import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, InjectionToken, Injector, Input, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, OnDestroy, OnInit, Pipe, PipeTransform, QueryList, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core';
11+
import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, InjectionToken, Injector, Input, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, OnDestroy, OnInit, Pipe, PipeTransform, QueryList, Renderer2, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core';
12+
import {isProceduralRenderer} from '@angular/core/src/render3/interfaces/renderer';
1213
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
1314
import {ComponentFixture, TestBed, TestComponentRenderer} from '@angular/core/testing';
1415
import {clearTranslations, loadTranslations} from '@angular/localize';
@@ -1411,7 +1412,7 @@ describe('ViewContainerRef', () => {
14111412
`,
14121413
})
14131414
class ChildB {
1414-
constructor(private injector: Injector) {}
1415+
constructor(private injector: Injector, public renderer: Renderer2) {}
14151416
get tokenA() {
14161417
return this.injector.get(TOKEN_A);
14171418
}
@@ -1424,19 +1425,19 @@ describe('ViewContainerRef', () => {
14241425
selector: 'app',
14251426
template: '',
14261427
providers: [
1427-
{provide: TOKEN_B, useValue: '[TokenValueB]'},
1428+
{provide: TOKEN_B, useValue: '[TokenB - Value]'},
14281429
]
14291430
})
14301431
class App {
14311432
constructor(
14321433
public viewContainerRef: ViewContainerRef, public ngModuleRef: NgModuleRef<unknown>,
1433-
public injector: Injector) {}
1434+
public injector: Injector, public cfr: ComponentFactoryResolver) {}
14341435
}
14351436

14361437
@NgModule({
14371438
declarations: [App, ChildA, ChildB],
14381439
providers: [
1439-
{provide: TOKEN_A, useValue: '[TokenValueA]'},
1440+
{provide: TOKEN_A, useValue: '[TokenA - Value]'},
14401441
]
14411442
})
14421443
class AppModule {
@@ -1454,6 +1455,48 @@ describe('ViewContainerRef', () => {
14541455
expect(fixture.nativeElement.parentNode.textContent).toContain('[Child Component A]');
14551456
});
14561457

1458+
it('should maintain connection with module injector when custom injector is provided', () => {
1459+
const comp = fixture.componentInstance;
1460+
const customInjector = Injector.create({
1461+
providers: [
1462+
{provide: TOKEN_B, useValue: '[TokenB - CustomValue]'},
1463+
]
1464+
});
1465+
1466+
// Use factory-less way of creating a component.
1467+
const factorylessChildB =
1468+
comp.viewContainerRef.createComponent(ChildB, {injector: customInjector});
1469+
fixture.detectChanges();
1470+
1471+
// Custom injector provides only `TOKEN_B`,
1472+
// so `TOKEN_A` should be retrieved from the module injector.
1473+
expect(getElementText(fixture.nativeElement.parentNode))
1474+
.toContain('[TokenA - Value] [TokenB - CustomValue]');
1475+
1476+
// Verify that the dynamically-created component uses correct instance of a renderer.
1477+
// Ivy runtime code switches over to Renderer3 (document) if an instance of the Renderer2 is
1478+
// not found, which can happen when the module injector subtree is not in the DI tree.
1479+
// See https://github.com/angular/angular/issues/44897.
1480+
expect(isProceduralRenderer(factorylessChildB.instance.renderer)).toBe(true);
1481+
1482+
// Use factory-based API to compare the output with the factory-less one.
1483+
const childBFactory = comp.cfr.resolveComponentFactory(ChildB);
1484+
const factoryBasedChildB = comp.viewContainerRef.createComponent(
1485+
childBFactory, undefined, customInjector /* injector */);
1486+
fixture.detectChanges();
1487+
1488+
// Custom injector provides only `TOKEN_B`,
1489+
// so `TOKEN_A` should be retrieved from the module injector
1490+
expect(getElementText(fixture.nativeElement.parentNode))
1491+
.toContain('[TokenA - Value] [TokenB - CustomValue]');
1492+
1493+
// Verify that the dynamically-created component uses correct instance of a renderer.
1494+
// Ivy runtime code switches over to Renderer3 (document) if an instance of the Renderer2 is
1495+
// not found, which can happen when the module injector subtree is not in the DI tree.
1496+
// See https://github.com/angular/angular/issues/44897.
1497+
expect(isProceduralRenderer(factoryBasedChildB.instance.renderer)).toBe(true);
1498+
});
1499+
14571500
it('should throw if class without @Component decorator is used as Component type', () => {
14581501
class MyClassWithoutComponentDecorator {}
14591502
const createComponent = () => {
@@ -1489,8 +1532,8 @@ describe('ViewContainerRef', () => {
14891532
.toContain(
14901533
'[Child Component B] ' +
14911534
'[Projectable Node] ' +
1492-
'[TokenValueA] ' +
1493-
'[TokenValueB] ' +
1535+
'[TokenA - Value] ' +
1536+
'[TokenB - Value] ' +
14941537
'[Child Component A]');
14951538
});
14961539
});

0 commit comments

Comments
 (0)