From 86a359b399456e62335a0bcfe7c7fb48b7c2b781 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Wed, 27 Mar 2024 17:04:41 -0700 Subject: [PATCH] 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 --- packages/core/src/defer/instructions.ts | 31 +++++++-- packages/core/test/acceptance/defer_spec.ts | 68 ++++++++++++++++++- .../router/src/directives/router_outlet.ts | 8 +++ 3 files changed, 99 insertions(+), 8 deletions(-) diff --git a/packages/core/src/defer/instructions.ts b/packages/core/src/defer/instructions.ts index 72cbead18ab5b..4b658129d8f94 100644 --- a/packages/core/src/defer/instructions.ts +++ b/packages/core/src/defer/instructions.ts @@ -17,6 +17,7 @@ import {populateDehydratedViewsInLContainer} from '../linker/view_container_ref' import {PendingTasks} from '../pending_tasks'; import {assertLContainer, assertTNodeForLView} from '../render3/assert'; import {bindingUpdated} from '../render3/bindings'; +import {ChainedInjector} from '../render3/component_ref'; import {getComponentDef, getDirectiveDef, getPipeDef} from '../render3/definition'; import {getTemplateLocationDetails} from '../render3/instructions/element_validation'; import {markViewDirty} from '../render3/instructions/mark_view_dirty'; @@ -500,6 +501,15 @@ export function renderDeferBlockState( } } +/** + * Detects whether an injector is an instance of a `ChainedInjector`, + * created based on the `OutletInjector`. + */ +function isRouterOutletInjector(currentInjector: Injector): boolean { + return (currentInjector instanceof ChainedInjector) && + ((currentInjector.injector as any).__ngOutletInjector); +} + /** * Applies changes to the DOM to reflect a given state. */ @@ -532,16 +542,23 @@ function applyDeferBlockState( const providers = tDetails.providers; if (providers && providers.length > 0) { const parentInjector = hostLView[INJECTOR] as Injector; - const parentEnvInjector = parentInjector.get(EnvironmentInjector); - injector = - parentEnvInjector.get(CachedInjectorService) - .getOrCreateInjector( - tDetails, parentEnvInjector, providers, ngDevMode ? 'DeferBlock Injector' : ''); + // Note: we have a special case for Router's `OutletInjector`, + // since it's not an instance of the `EnvironmentInjector`, so + // we can't inject it. Once the `OutletInjector` is replaced + // with the `EnvironmentInjector` in Router's code, this special + // handling can be removed. + const parentEnvInjector = isRouterOutletInjector(parentInjector) ? + parentInjector : + parentInjector.get(EnvironmentInjector); + injector = parentEnvInjector.get(CachedInjectorService) + .getOrCreateInjector( + tDetails, parentEnvInjector as EnvironmentInjector, providers, + ngDevMode ? 'DeferBlock Injector' : ''); } } const dehydratedView = findMatchingDehydratedView(lContainer, activeBlockTNode.tView!.ssrId); - const embeddedLView = createAndRenderEmbeddedLView( - hostLView, activeBlockTNode, null, {dehydratedView, embeddedViewInjector: injector}); + const embeddedLView = + createAndRenderEmbeddedLView(hostLView, activeBlockTNode, null, {dehydratedView, injector}); addLViewToLContainer( lContainer, embeddedLView, viewIndex, shouldAddViewToDom(activeBlockTNode, dehydratedView)); markViewDirty(embeddedLView); diff --git a/packages/core/test/acceptance/defer_spec.ts b/packages/core/test/acceptance/defer_spec.ts index e023a40546ce9..b856bdb73532e 100644 --- a/packages/core/test/acceptance/defer_spec.ts +++ b/packages/core/test/acceptance/defer_spec.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; +import {CommonModule, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; 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'; import {getComponentDef} from '@angular/core/src/render3/definition'; import {ComponentFixture, DeferBlockBehavior, fakeAsync, flush, TestBed, tick} from '@angular/core/testing'; +import {ActivatedRoute, provideRouter, Router, RouterOutlet} from '@angular/router'; /** * Clears all associated directive defs from a given component class. @@ -4151,4 +4152,69 @@ describe('@defer', () => { .toContain(`${serviceFromNgModule}|${tokenFromRootComponent}`); }); }); + + describe('Router', () => { + it('should inject correct `ActivatedRoutes` in components within defer blocks', async () => { + @Component({ + standalone: true, + imports: [RouterOutlet], + template: '', + }) + class App { + } + + @Component({ + standalone: true, + selector: 'another-child', + imports: [CommonModule], + template: 'another child: {{route.snapshot.url[0]}}', + }) + class AnotherChild { + route = inject(ActivatedRoute); + } + + @Component({ + standalone: true, + imports: [CommonModule, AnotherChild], + template: ` + child: {{route.snapshot.url[0]}} + @defer (on immediate) { + + } + `, + }) + class Child { + route = inject(ActivatedRoute); + } + + const deferDepsInterceptor = { + intercept() { + return () => { + return [dynamicImportOf(AnotherChild, 10)]; + }; + }, + }; + + TestBed.configureTestingModule({ + providers: [ + {provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID}, + {provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor}, + provideRouter([ + {path: 'a', component: Child}, + ]), + ], + }); + clearDirectiveDefs(Child); + + const app = TestBed.createComponent(App); + await TestBed.inject(Router).navigateByUrl('/a?x=1'); + app.detectChanges(); + + await allPendingDynamicImports(); + app.detectChanges(); + + expect(app.nativeElement.innerHTML).toContain('child: a'); + expect(app.nativeElement.innerHTML).toContain('another child: a'); + }); + }); }); diff --git a/packages/router/src/directives/router_outlet.ts b/packages/router/src/directives/router_outlet.ts index 5717f0bd5e476..9e914cca33e7f 100644 --- a/packages/router/src/directives/router_outlet.ts +++ b/packages/router/src/directives/router_outlet.ts @@ -379,6 +379,14 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract { } class OutletInjector implements Injector { + /** + * A special flag that allows to identify the `OutletInjector` without + * referring to the class itself. This is required as a temporary solution, + * to have a special handling for this injector in core. Eventually, this + * injector should just become an `EnvironmentInjector` without special logic. + */ + private __ngOutletInjector = true; + constructor( private route: ActivatedRoute, private childContexts: ChildrenOutletContexts,