Skip to content

Commit 708ba81

Browse files
AndrewKushnirdylhunn
authored andcommitted
fix(core): establish proper injector resolution order for @defer blocks (#55079)
This commit updates the `@defer` logic to establish proper injector resolution order. More specifically: - Makes node injectors to be inspected first, similar to how it happens when `@defer` block is not used. - Adds extra handling for the Router's `OutletInjector`, until we replace it with an `EnvironmentInjector`. Resolves #54864. Resolves #55028. Resolves #55036. PR Close #55079
1 parent fb5a288 commit 708ba81

File tree

3 files changed

+99
-8
lines changed

3 files changed

+99
-8
lines changed

packages/core/src/defer/instructions.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {populateDehydratedViewsInLContainer} from '../linker/view_container_ref'
1717
import {PendingTasks} from '../pending_tasks';
1818
import {assertLContainer, assertTNodeForLView} from '../render3/assert';
1919
import {bindingUpdated} from '../render3/bindings';
20+
import {ChainedInjector} from '../render3/component_ref';
2021
import {getComponentDef, getDirectiveDef, getPipeDef} from '../render3/definition';
2122
import {getTemplateLocationDetails} from '../render3/instructions/element_validation';
2223
import {markViewDirty} from '../render3/instructions/mark_view_dirty';
@@ -502,6 +503,15 @@ export function renderDeferBlockState(
502503
}
503504
}
504505

506+
/**
507+
* Detects whether an injector is an instance of a `ChainedInjector`,
508+
* created based on the `OutletInjector`.
509+
*/
510+
function isRouterOutletInjector(currentInjector: Injector): boolean {
511+
return (currentInjector instanceof ChainedInjector) &&
512+
((currentInjector.injector as any).__ngOutletInjector);
513+
}
514+
505515
/**
506516
* Applies changes to the DOM to reflect a given state.
507517
*/
@@ -534,16 +544,23 @@ function applyDeferBlockState(
534544
const providers = tDetails.providers;
535545
if (providers && providers.length > 0) {
536546
const parentInjector = hostLView[INJECTOR] as Injector;
537-
const parentEnvInjector = parentInjector.get(EnvironmentInjector);
538-
injector =
539-
parentEnvInjector.get(CachedInjectorService)
540-
.getOrCreateInjector(
541-
tDetails, parentEnvInjector, providers, ngDevMode ? 'DeferBlock Injector' : '');
547+
// Note: we have a special case for Router's `OutletInjector`,
548+
// since it's not an instance of the `EnvironmentInjector`, so
549+
// we can't inject it. Once the `OutletInjector` is replaced
550+
// with the `EnvironmentInjector` in Router's code, this special
551+
// handling can be removed.
552+
const parentEnvInjector = isRouterOutletInjector(parentInjector) ?
553+
parentInjector :
554+
parentInjector.get(EnvironmentInjector);
555+
injector = parentEnvInjector.get(CachedInjectorService)
556+
.getOrCreateInjector(
557+
tDetails, parentEnvInjector as EnvironmentInjector, providers,
558+
ngDevMode ? 'DeferBlock Injector' : '');
542559
}
543560
}
544561
const dehydratedView = findMatchingDehydratedView(lContainer, activeBlockTNode.tView!.ssrId);
545-
const embeddedLView = createAndRenderEmbeddedLView(
546-
hostLView, activeBlockTNode, null, {dehydratedView, embeddedViewInjector: injector});
562+
const embeddedLView =
563+
createAndRenderEmbeddedLView(hostLView, activeBlockTNode, null, {dehydratedView, injector});
547564
addLViewToLContainer(
548565
lContainer, embeddedLView, viewIndex, shouldAddViewToDom(activeBlockTNode, dehydratedView));
549566
markViewDirty(embeddedLView);

packages/core/test/acceptance/defer_spec.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
9+
import {CommonModule, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
1010
import {ApplicationRef, Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, createComponent, DebugElement, Directive, EnvironmentInjector, ErrorHandler, getDebugNode, inject, Injectable, InjectionToken, Input, NgModule, NgZone, Pipe, PipeTransform, PLATFORM_ID, QueryList, Type, ViewChildren, ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR} from '@angular/core';
1111
import {getComponentDef} from '@angular/core/src/render3/definition';
1212
import {ComponentFixture, DeferBlockBehavior, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
13+
import {ActivatedRoute, provideRouter, Router, RouterOutlet} from '@angular/router';
1314

1415
/**
1516
* Clears all associated directive defs from a given component class.
@@ -4151,4 +4152,69 @@ describe('@defer', () => {
41514152
.toContain(`<chart>${serviceFromNgModule}|${tokenFromRootComponent}</chart>`);
41524153
});
41534154
});
4155+
4156+
describe('Router', () => {
4157+
it('should inject correct `ActivatedRoutes` in components within defer blocks', async () => {
4158+
@Component({
4159+
standalone: true,
4160+
imports: [RouterOutlet],
4161+
template: '<router-outlet />',
4162+
})
4163+
class App {
4164+
}
4165+
4166+
@Component({
4167+
standalone: true,
4168+
selector: 'another-child',
4169+
imports: [CommonModule],
4170+
template: 'another child: {{route.snapshot.url[0]}}',
4171+
})
4172+
class AnotherChild {
4173+
route = inject(ActivatedRoute);
4174+
}
4175+
4176+
@Component({
4177+
standalone: true,
4178+
imports: [CommonModule, AnotherChild],
4179+
template: `
4180+
child: {{route.snapshot.url[0]}}
4181+
@defer (on immediate) {
4182+
<another-child />
4183+
}
4184+
`,
4185+
})
4186+
class Child {
4187+
route = inject(ActivatedRoute);
4188+
}
4189+
4190+
const deferDepsInterceptor = {
4191+
intercept() {
4192+
return () => {
4193+
return [dynamicImportOf(AnotherChild, 10)];
4194+
};
4195+
},
4196+
};
4197+
4198+
TestBed.configureTestingModule({
4199+
providers: [
4200+
{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
4201+
{provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor},
4202+
provideRouter([
4203+
{path: 'a', component: Child},
4204+
]),
4205+
],
4206+
});
4207+
clearDirectiveDefs(Child);
4208+
4209+
const app = TestBed.createComponent(App);
4210+
await TestBed.inject(Router).navigateByUrl('/a?x=1');
4211+
app.detectChanges();
4212+
4213+
await allPendingDynamicImports();
4214+
app.detectChanges();
4215+
4216+
expect(app.nativeElement.innerHTML).toContain('child: a');
4217+
expect(app.nativeElement.innerHTML).toContain('another child: a');
4218+
});
4219+
});
41544220
});

packages/router/src/directives/router_outlet.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,14 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
379379
}
380380

381381
class OutletInjector implements Injector {
382+
/**
383+
* A special flag that allows to identify the `OutletInjector` without
384+
* referring to the class itself. This is required as a temporary solution,
385+
* to have a special handling for this injector in core. Eventually, this
386+
* injector should just become an `EnvironmentInjector` without special logic.
387+
*/
388+
private __ngOutletInjector = true;
389+
382390
constructor(
383391
private route: ActivatedRoute,
384392
private childContexts: ChildrenOutletContexts,

0 commit comments

Comments
 (0)