Skip to content

Commit b7d3ecc

Browse files
atscottthePunderWoman
authored andcommitted
fix(router): routes should not get stale providers (#56798)
This fixes a bug with RouterOutlet and its context where it would reuse providers from a previously activated route. fixes #56774 PR Close #56798
1 parent 445dd96 commit b7d3ecc

File tree

8 files changed

+78
-32
lines changed

8 files changed

+78
-32
lines changed

goldens/public-api/router/index.api.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export class ChildActivationStart {
183183

184184
// @public
185185
export class ChildrenOutletContexts {
186-
constructor(parentInjector: EnvironmentInjector);
186+
constructor(rootInjector: EnvironmentInjector);
187187
// (undocumented)
188188
getContext(childName: string): OutletContext | null;
189189
// (undocumented)
@@ -530,13 +530,14 @@ export type OnSameUrlNavigation = 'reload' | 'ignore';
530530

531531
// @public
532532
export class OutletContext {
533-
constructor(injector: EnvironmentInjector);
533+
constructor(rootInjector: EnvironmentInjector);
534534
// (undocumented)
535535
attachRef: ComponentRef<any> | null;
536536
// (undocumented)
537537
children: ChildrenOutletContexts;
538538
// (undocumented)
539-
injector: EnvironmentInjector;
539+
get injector(): EnvironmentInjector;
540+
set injector(_: EnvironmentInjector);
540541
// (undocumented)
541542
outlet: RouterOutletContract | null;
542543
// (undocumented)

packages/router/src/components/empty_outlet.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
import {Component} from '@angular/core';
1010

1111
import {RouterOutlet} from '../directives/router_outlet';
12+
import {PRIMARY_OUTLET} from '../shared';
13+
import {Route} from '../models';
14+
export {ɵEmptyOutletComponent as EmptyOutletComponent};
1215

1316
/**
1417
* This component is used internally within the router to be a placeholder when an empty
@@ -26,4 +29,20 @@ import {RouterOutlet} from '../directives/router_outlet';
2629
})
2730
export class ɵEmptyOutletComponent {}
2831

29-
export {ɵEmptyOutletComponent as EmptyOutletComponent};
32+
/**
33+
* Makes a copy of the config and adds any default required properties.
34+
*/
35+
export function standardizeConfig(r: Route): Route {
36+
const children = r.children && r.children.map(standardizeConfig);
37+
const c = children ? {...r, children} : {...r};
38+
if (
39+
!c.component &&
40+
!c.loadComponent &&
41+
(children || c.loadChildren) &&
42+
c.outlet &&
43+
c.outlet !== PRIMARY_OUTLET
44+
) {
45+
c.component = ɵEmptyOutletComponent;
46+
}
47+
return c;
48+
}

packages/router/src/operators/activate_routes.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,8 @@ export class ActivateRoutes {
221221
advanceActivatedRoute(stored.route.value);
222222
this.activateChildRoutes(futureNode, null, context.children);
223223
} else {
224-
const injector = getClosestRouteInjector(future.snapshot);
225224
context.attachRef = null;
226225
context.route = future;
227-
context.injector = injector ?? context.injector;
228226
if (context.outlet) {
229227
// Activate the outlet when it has already been instantiated
230228
// Otherwise it will get activated from its `ngOnInit` when instantiated

packages/router/src/router.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ import {
5454
UrlSerializer,
5555
UrlTree,
5656
} from './url_tree';
57-
import {standardizeConfig, validateConfig} from './utils/config';
57+
import {validateConfig} from './utils/config';
5858
import {afterNextNavigation} from './utils/navigations';
59+
import {standardizeConfig} from './components/empty_outlet';
5960

6061
function defaultErrorHandler(error: any): never {
6162
throw error;

packages/router/src/router_config_loader.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {finalize, map, mergeMap, refCount, tap} from 'rxjs/operators';
2121

2222
import {DefaultExport, LoadedRouterConfig, Route, Routes} from './models';
2323
import {wrapIntoObservable} from './utils/collection';
24-
import {assertStandalone, standardizeConfig, validateConfig} from './utils/config';
24+
import {assertStandalone, validateConfig} from './utils/config';
25+
import {standardizeConfig} from './components/empty_outlet';
2526

2627
/**
2728
* The DI token for a router configuration.

packages/router/src/router_outlet_context.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {ComponentRef, EnvironmentInjector, Injectable} from '@angular/core';
1010

1111
import {RouterOutletContract} from './directives/router_outlet';
1212
import {ActivatedRoute} from './router_state';
13+
import {getClosestRouteInjector} from './utils/config';
1314

1415
/**
1516
* Store contextual information about a `RouterOutlet`
@@ -19,9 +20,15 @@ import {ActivatedRoute} from './router_state';
1920
export class OutletContext {
2021
outlet: RouterOutletContract | null = null;
2122
route: ActivatedRoute | null = null;
22-
children = new ChildrenOutletContexts(this.injector);
23+
children = new ChildrenOutletContexts(this.rootInjector);
2324
attachRef: ComponentRef<any> | null = null;
24-
constructor(public injector: EnvironmentInjector) {}
25+
get injector(): EnvironmentInjector {
26+
return getClosestRouteInjector(this.route?.snapshot) ?? this.rootInjector;
27+
}
28+
// TODO(atscott): Only here to avoid a "breaking" change in a patch/minor. Remove in v19.
29+
set injector(_: EnvironmentInjector) {}
30+
31+
constructor(private readonly rootInjector: EnvironmentInjector) {}
2532
}
2633

2734
/**
@@ -35,7 +42,7 @@ export class ChildrenOutletContexts {
3542
private contexts = new Map<string, OutletContext>();
3643

3744
/** @nodoc */
38-
constructor(private parentInjector: EnvironmentInjector) {}
45+
constructor(private rootInjector: EnvironmentInjector) {}
3946

4047
/** Called when a `RouterOutlet` directive is instantiated */
4148
onChildOutletCreated(childName: string, outlet: RouterOutletContract): void {
@@ -75,7 +82,7 @@ export class ChildrenOutletContexts {
7582
let context = this.getContext(childName);
7683

7784
if (!context) {
78-
context = new OutletContext(this.parentInjector);
85+
context = new OutletContext(this.rootInjector);
7986
this.contexts.set(childName, context);
8087
}
8188

packages/router/src/utils/config.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
ɵRuntimeError as RuntimeError,
1616
} from '@angular/core';
1717

18-
import {EmptyOutletComponent} from '../components/empty_outlet';
1918
import {RuntimeErrorCode} from '../errors';
2019
import {Route, Routes} from '../models';
2120
import {ActivatedRouteSnapshot} from '../router_state';
@@ -222,24 +221,6 @@ function getFullPath(parentPath: string, currentRoute: Route): string {
222221
}
223222
}
224223

225-
/**
226-
* Makes a copy of the config and adds any default required properties.
227-
*/
228-
export function standardizeConfig(r: Route): Route {
229-
const children = r.children && r.children.map(standardizeConfig);
230-
const c = children ? {...r, children} : {...r};
231-
if (
232-
!c.component &&
233-
!c.loadComponent &&
234-
(children || c.loadChildren) &&
235-
c.outlet &&
236-
c.outlet !== PRIMARY_OUTLET
237-
) {
238-
c.component = EmptyOutletComponent;
239-
}
240-
return c;
241-
}
242-
243224
/** Returns the `route.outlet` or PRIMARY_OUTLET if none exists. */
244225
export function getOutlet(route: Route): string {
245226
return route.outlet || PRIMARY_OUTLET;
@@ -268,7 +249,7 @@ export function sortByMatchingOutlets(routes: Routes, outletName: string): Route
268249
* also used for getting the correct injector to use for creating components.
269250
*/
270251
export function getClosestRouteInjector(
271-
snapshot: ActivatedRouteSnapshot,
252+
snapshot: ActivatedRouteSnapshot | undefined,
272253
): EnvironmentInjector | null {
273254
if (!snapshot) return null;
274255

packages/router/test/directives/router_outlet.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,44 @@ describe('injectors', () => {
427427
fixture.detectChanges();
428428
expect(childTokenValue).toEqual(null);
429429
});
430+
431+
it('should not get sibling providers', async () => {
432+
let childTokenValue: any = null;
433+
const TOKEN = new InjectionToken<any>('');
434+
@Component({
435+
template: '',
436+
standalone: true,
437+
})
438+
class Child {
439+
constructor() {
440+
childTokenValue = inject(TOKEN, {optional: true});
441+
}
442+
}
443+
444+
@Component({
445+
template: '<router-outlet/>',
446+
imports: [RouterOutlet],
447+
standalone: true,
448+
})
449+
class App {}
450+
451+
TestBed.configureTestingModule({
452+
providers: [
453+
provideRouter([
454+
{path: 'a', providers: [{provide: TOKEN, useValue: 'a value'}], component: Child},
455+
{path: 'b', component: Child},
456+
]),
457+
],
458+
});
459+
const fixture = TestBed.createComponent(App);
460+
fixture.detectChanges();
461+
await TestBed.inject(Router).navigateByUrl('/a');
462+
fixture.detectChanges();
463+
expect(childTokenValue).toEqual('a value');
464+
await TestBed.inject(Router).navigateByUrl('/b');
465+
fixture.detectChanges();
466+
expect(childTokenValue).toEqual(null);
467+
});
430468
});
431469

432470
function advance(fixture: ComponentFixture<unknown>, millis?: number): void {

0 commit comments

Comments
 (0)