Skip to content

Commit 4909844

Browse files
AndrewKushnirthePunderWoman
authored andcommitted
fix(core): establish proper defer injector hierarchy for components attached to ApplicationRef (#56763)
This commit updates the logic that create an injector for defer blocks (when it's needed) to account for a situation when a component is instantiated without a connection to the current component tree. This can happen if a component is created using its factory function or via `createComponent()` call. Resolves #56372. PR Close #56763
1 parent 26da308 commit 4909844

3 files changed

Lines changed: 168 additions & 43 deletions

File tree

packages/core/src/defer/instructions.ts

Lines changed: 67 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {setActiveConsumer} from '@angular/core/primitives/signals';
1010

1111
import {CachedInjectorService} from '../cached_injector_service';
1212
import {NotificationSource} from '../change_detection/scheduling/zoneless_scheduling';
13-
import {EnvironmentInjector, InjectionToken, Injector} from '../di';
13+
import {EnvironmentInjector, InjectionToken, Injector, Provider} from '../di';
1414
import {internalImportProvidersFrom} from '../di/provider_collection';
1515
import {RuntimeError, RuntimeErrorCode} from '../errors';
1616
import {findMatchingDehydratedView} from '../hydration/views';
@@ -90,7 +90,6 @@ import {
9090
setLDeferBlockDetails,
9191
setTDeferBlockDetails,
9292
} from './utils';
93-
import {isRouterOutletInjector} from '../render3/util/injector_utils';
9493

9594
/**
9695
* **INTERNAL**, avoid referencing it in application code.
@@ -626,20 +625,73 @@ export function renderDeferBlockState(
626625
}
627626

628627
/**
629-
* Creates an instance of the `OutletInjector` using a private factory
630-
* function available on the `OutletInjector` class.
631-
*
632-
* @param parentOutletInjector Parent OutletInjector, which should be used
633-
* to produce a new instance.
634-
* @param parentInjector An Injector, which should be used as a parent one
635-
* for a newly created `OutletInjector` instance.
628+
* Checks whether there is a cached injector associated with a given defer block
629+
* declaration and returns if it exists. If there is no cached injector present -
630+
* creates a new injector and stores in the cache.
636631
*/
637-
function createRouterOutletInjector(
638-
parentOutletInjector: ChainedInjector,
632+
function getOrCreateEnvironmentInjector(
639633
parentInjector: Injector,
634+
tDetails: TDeferBlockDetails,
635+
providers: Provider[],
640636
) {
641-
const outletInjector = parentOutletInjector.injector as any;
642-
return outletInjector.__ngOutletInjector(parentInjector);
637+
return parentInjector
638+
.get(CachedInjectorService)
639+
.getOrCreateInjector(
640+
tDetails,
641+
parentInjector as EnvironmentInjector,
642+
providers,
643+
ngDevMode ? 'DeferBlock Injector' : '',
644+
);
645+
}
646+
647+
/**
648+
* Creates a new injector, which contains providers collected from dependencies (NgModules) of
649+
* defer-loaded components. This function detects different types of parent injectors and creates
650+
* a new injector based on that.
651+
*/
652+
function createDeferBlockInjector(
653+
parentInjector: Injector,
654+
tDetails: TDeferBlockDetails,
655+
providers: Provider[],
656+
) {
657+
// Check if the parent injector is an instance of a `ChainedInjector`.
658+
//
659+
// In this case, we retain the shape of the injector and use a newly created
660+
// `EnvironmentInjector` as a parent in the `ChainedInjector`. That is needed to
661+
// make sure that the primary injector gets consulted first (since it's typically
662+
// a NodeInjector) and `EnvironmentInjector` tree is consulted after that.
663+
if (parentInjector instanceof ChainedInjector) {
664+
const origInjector = parentInjector.injector;
665+
// Guaranteed to be an environment injector
666+
const parentEnvInjector = parentInjector.parentInjector;
667+
668+
const envInjector = getOrCreateEnvironmentInjector(parentEnvInjector, tDetails, providers);
669+
return new ChainedInjector(origInjector, envInjector);
670+
}
671+
672+
const parentEnvInjector = parentInjector.get(EnvironmentInjector);
673+
674+
// If the `parentInjector` is *not* an `EnvironmentInjector` - we need to create
675+
// a new `ChainedInjector` with the following setup:
676+
//
677+
// - the provided `parentInjector` becomes a primary injector
678+
// - an existing (real) `EnvironmentInjector` becomes a parent injector for
679+
// a newly-created one, which contains extra providers
680+
//
681+
// So the final order in which injectors would be consulted in this case would look like this:
682+
//
683+
// 1. Provided `parentInjector`
684+
// 2. Newly-created `EnvironmentInjector` with extra providers
685+
// 3. `EnvironmentInjector` from the `parentInjector`
686+
if (parentEnvInjector !== parentInjector) {
687+
const envInjector = getOrCreateEnvironmentInjector(parentEnvInjector, tDetails, providers);
688+
return new ChainedInjector(parentInjector, envInjector);
689+
}
690+
691+
// The `parentInjector` is an instance of an `EnvironmentInjector`.
692+
// No need for special handling, we can use `parentInjector` as a
693+
// parent injector directly.
694+
return getOrCreateEnvironmentInjector(parentInjector, tDetails, providers);
643695
}
644696

645697
/**
@@ -672,40 +724,12 @@ function applyDeferBlockState(
672724
// newly loaded standalone components used within the block, which may
673725
// import NgModules with providers. In order to make those providers
674726
// available for components declared in that NgModule, we create an instance
675-
// of environment injector to host those providers and pass this injector
727+
// of an environment injector to host those providers and pass this injector
676728
// to the logic that creates a view.
677729
const tDetails = getTDeferBlockDetails(hostTView, tNode);
678730
const providers = tDetails.providers;
679731
if (providers && providers.length > 0) {
680-
const parentInjector = hostLView[INJECTOR] as Injector;
681-
682-
// Note: we have a special case for Router's `OutletInjector`,
683-
// since it's not an instance of the `EnvironmentInjector`, so
684-
// we can't inject it. Once the `OutletInjector` is replaced
685-
// with the `EnvironmentInjector` in Router's code, this special
686-
// handling can be removed.
687-
const isParentOutletInjector = isRouterOutletInjector(parentInjector);
688-
const parentEnvInjector = isParentOutletInjector
689-
? parentInjector
690-
: parentInjector.get(EnvironmentInjector);
691-
692-
injector = parentEnvInjector
693-
.get(CachedInjectorService)
694-
.getOrCreateInjector(
695-
tDetails,
696-
parentEnvInjector as EnvironmentInjector,
697-
providers,
698-
ngDevMode ? 'DeferBlock Injector' : '',
699-
);
700-
701-
// Note: this is a continuation of the special case for Router's `OutletInjector`.
702-
// Since the `OutletInjector` handles `ActivatedRoute` and `ChildrenOutletContexts`
703-
// dynamically (i.e. their values are not really stored statically in an injector),
704-
// we need to "wrap" a defer injector into another `OutletInjector`, so we retain
705-
// the dynamic resolution of the mentioned tokens.
706-
if (isParentOutletInjector) {
707-
injector = createRouterOutletInjector(parentInjector as ChainedInjector, injector);
708-
}
732+
injector = createDeferBlockInjector(hostLView[INJECTOR]!, tDetails, providers);
709733
}
710734
}
711735
const dehydratedView = findMatchingDehydratedView(lContainer, activeBlockTNode.tView!.ssrId);

packages/core/test/acceptance/defer_spec.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import {
3131
ViewChildren,
3232
ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
3333
ɵRuntimeError as RuntimeError,
34+
Injector,
35+
ElementRef,
36+
ViewChild,
3437
} from '@angular/core';
3538
import {getComponentDef} from '@angular/core/src/render3/definition';
3639
import {
@@ -4113,6 +4116,101 @@ describe('@defer', () => {
41134116
`<child-cmp>Token A: ${tokenA} | Token B: ${tokenB}</child-cmp>`,
41144117
);
41154118
});
4119+
4120+
it(
4121+
'should provide access to tokens from a parent component ' +
4122+
'for components instantiated via `createComponent` call (when a corresponding NodeInjector is used in the call), ' +
4123+
'but attached to the ApplicationRef',
4124+
async () => {
4125+
const TokenA = new InjectionToken('A');
4126+
const TokenB = new InjectionToken('B');
4127+
4128+
@NgModule({
4129+
providers: [{provide: TokenB, useValue: 'TokenB value'}],
4130+
})
4131+
class MyModule {}
4132+
4133+
@Component({
4134+
selector: 'lazy',
4135+
standalone: true,
4136+
imports: [MyModule],
4137+
template: `
4138+
Lazy Component! Token: {{ token }}
4139+
`,
4140+
})
4141+
class Lazy {
4142+
token = inject(TokenA);
4143+
}
4144+
4145+
@Component({
4146+
standalone: true,
4147+
imports: [Lazy],
4148+
template: `
4149+
@defer {
4150+
<lazy />
4151+
}
4152+
`,
4153+
})
4154+
class Dialog {}
4155+
4156+
@Component({
4157+
standalone: true,
4158+
selector: 'app-root',
4159+
providers: [{provide: TokenA, useValue: 'TokenA from RootCmp'}],
4160+
template: `
4161+
<div #container></div>
4162+
`,
4163+
})
4164+
class RootCmp {
4165+
injector = inject(Injector);
4166+
appRef = inject(ApplicationRef);
4167+
envInjector = inject(EnvironmentInjector);
4168+
@ViewChild('container', {read: ElementRef}) container!: ElementRef;
4169+
4170+
openModal() {
4171+
const hostElement = this.container.nativeElement;
4172+
const componentRef = createComponent(Dialog, {
4173+
hostElement,
4174+
elementInjector: this.injector,
4175+
environmentInjector: this.envInjector,
4176+
});
4177+
this.appRef.attachView(componentRef.hostView);
4178+
componentRef.changeDetectorRef.detectChanges();
4179+
}
4180+
}
4181+
4182+
const deferDepsInterceptor = {
4183+
intercept() {
4184+
return () => {
4185+
return [dynamicImportOf(Lazy)];
4186+
};
4187+
},
4188+
};
4189+
4190+
TestBed.configureTestingModule({
4191+
providers: [
4192+
{provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor},
4193+
],
4194+
deferBlockBehavior: DeferBlockBehavior.Playthrough,
4195+
});
4196+
4197+
const fixture = TestBed.createComponent(RootCmp);
4198+
fixture.detectChanges();
4199+
4200+
fixture.componentInstance.openModal();
4201+
4202+
// The call above instantiates a component that uses a `@defer` block,
4203+
// so we need to wait for dynamic imports to complete.
4204+
await allPendingDynamicImports();
4205+
fixture.detectChanges();
4206+
4207+
// Verify that tokens from parent components are available for injection
4208+
// inside a component within a `@defer` block.
4209+
expect(fixture.nativeElement.innerHTML).toContain(
4210+
`<lazy> Lazy Component! Token: TokenA from RootCmp </lazy>`,
4211+
);
4212+
},
4213+
);
41164214
});
41174215

41184216
describe('NgModules', () => {

packages/core/test/bundling/defer/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,9 @@
932932
{
933933
"name": "getOrCreateComponentTView"
934934
},
935+
{
936+
"name": "getOrCreateEnvironmentInjector"
937+
},
935938
{
936939
"name": "getOrCreateInjectable"
937940
},

0 commit comments

Comments
 (0)