Skip to content

Commit d00b3fe

Browse files
JeanMechecrisbeto
authored andcommitted
feat(router): add a currentNavigation signal to the Router service. (#62971)
This new signal property is convenient to derive a `isNavigating` state. `isNavigating = computed(() => !!this.router.currentNavigation())` DEPRECATED: The Router.getCurrentNavigation method is deprecated. Use the Router.currentNavigation signal instead. fixes #62958 PR Close #62971
1 parent fb1fc82 commit d00b3fe

File tree

15 files changed

+421
-24
lines changed

15 files changed

+421
-24
lines changed

adev/src/content/guide/routing/data-resolvers.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,7 @@ import { map } from 'rxjs';
282282
})
283283
export class App {
284284
private router = inject(Router);
285-
isNavigating = toSignal(this.router.events.pipe(
286-
map(() => !!this.router.getCurrentNavigation())
287-
));
285+
isNavigating = computed(() => !!this.router.currentNavigation());
288286
}
289287
```
290288

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,8 +709,10 @@ export class Router {
709709
// (undocumented)
710710
config: Routes;
711711
createUrlTree(commands: readonly any[], navigationExtras?: UrlCreationOptions): UrlTree;
712+
readonly currentNavigation: i0.Signal<Navigation | null>;
712713
dispose(): void;
713714
get events(): Observable<Event_2>;
715+
// @deprecated
714716
getCurrentNavigation(): Navigation | null;
715717
initialNavigation(): void;
716718
// @deprecated
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"dist/browser/main-[hash].js": 221976,
2+
"dist/browser/main-[hash].js": 227093,
33
"dist/browser/polyfills-[hash].js": 34544,
44
"dist/browser/event-dispatch-contract.min.js": 476
55
}

packages/core/schematics/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ bundle_entrypoints = [
114114
"control-flow-migration",
115115
"packages/core/schematics/migrations/control-flow-migration/index.js",
116116
],
117+
[
118+
"router-current-navigation",
119+
"packages/core/schematics/migrations/router-current-navigation/index.js",
120+
],
117121
]
118122

119123
rollup.rollup(
@@ -128,6 +132,7 @@ rollup.rollup(
128132
"//packages/core/schematics/migrations/control-flow-migration",
129133
"//packages/core/schematics/migrations/document-core",
130134
"//packages/core/schematics/migrations/inject-flags",
135+
"//packages/core/schematics/migrations/router-current-navigation",
131136
"//packages/core/schematics/migrations/test-bed-get",
132137
"//packages/core/schematics/ng-generate/cleanup-unused-imports",
133138
"//packages/core/schematics/ng-generate/inject-migration",

packages/core/schematics/migrations.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
"version": "20.0.0",
2121
"description": "Moves imports of `DOCUMENT` from `@angular/common` to `@angular/core`",
2222
"factory": "./bundles/document-core.cjs#migrate"
23+
},
24+
"router-current-navigation": {
25+
"version": "20.2.0",
26+
"description": "Replaces usages of the deprecated Router.getCurrentNavigation method with the Router.currentNavigation signal",
27+
"factory": "./bundles/router-current-navigation.cjs#migrate",
28+
"optional": true
2329
}
2430
}
2531
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
load("//tools:defaults2.bzl", "ts_project")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/test:__pkg__",
7+
],
8+
)
9+
10+
ts_project(
11+
name = "router-current-navigation",
12+
srcs = glob(["**/*.ts"]),
13+
deps = [
14+
"//:node_modules/@angular-devkit/schematics",
15+
"//:node_modules/@types/node",
16+
"//:node_modules/typescript",
17+
"//packages/compiler-cli/private",
18+
"//packages/compiler-cli/src/ngtsc/file_system",
19+
"//packages/core/schematics/utils",
20+
"//packages/core/schematics/utils/tsurge",
21+
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
22+
],
23+
)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## Remove `Router.getCurrentNavihation` migration
2+
Replaces the usages of the deprecated `Router.getCurrentNavigation` method with the new `Router.currentNavigation()` signal:
3+
4+
### Before
5+
```typescript
6+
import { Router } from '@angular/router';
7+
8+
export class MyService {
9+
router = inject(Router);
10+
11+
someMethod() {
12+
const currentNavigation = this.router.getCurrentNavigation();
13+
}
14+
}
15+
```
16+
17+
### After
18+
```typescript
19+
import { Router } from '@angular/router';
20+
21+
export class MyService {
22+
router = inject(Router);
23+
24+
someMethod() {
25+
const currentNavigation = this.router.currentNavigation();
26+
}
27+
}
28+
```
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*!
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Rule} from '@angular-devkit/schematics';
10+
import {RouterCurrentNavigationMigration} from './router_current_navigation_migration';
11+
import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
12+
13+
export function migrate(): Rule {
14+
return async (tree) => {
15+
await runMigrationInDevkit({
16+
tree,
17+
getMigration: () => new RouterCurrentNavigationMigration(),
18+
});
19+
};
20+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import ts from 'typescript';
10+
import {
11+
confirmAsSerializable,
12+
ProgramInfo,
13+
ProjectFile,
14+
projectFile,
15+
Replacement,
16+
Serializable,
17+
TextUpdate,
18+
TsurgeFunnelMigration,
19+
} from '../../utils/tsurge';
20+
import {getImportSpecifier} from '../../utils/typescript/imports';
21+
import {isReferenceToImport} from '../../utils/typescript/symbol';
22+
23+
export interface CompilationUnitData {
24+
locations: Location[];
25+
}
26+
27+
/** Information about the `getCurrentNavigation` identifier in `Router.getCurrentNavigation`. */
28+
interface Location {
29+
/** File in which the expression is defined. */
30+
file: ProjectFile;
31+
32+
/** Start of the `getCurrentNavigation` identifier. */
33+
position: number;
34+
}
35+
36+
/** Name of the method being replaced. */
37+
const METHOD_NAME = 'getCurrentNavigation';
38+
39+
/** Migration that replaces `Router.getCurrentNavigation` usages with `Router.currentNavigation()`. */
40+
export class RouterCurrentNavigationMigration extends TsurgeFunnelMigration<
41+
CompilationUnitData,
42+
CompilationUnitData
43+
> {
44+
override async analyze(info: ProgramInfo): Promise<Serializable<CompilationUnitData>> {
45+
const locations: Location[] = [];
46+
47+
for (const sourceFile of info.sourceFiles) {
48+
const routerSpecifier = getImportSpecifier(sourceFile, '@angular/router', 'Router');
49+
50+
if (routerSpecifier === null) {
51+
continue;
52+
}
53+
54+
const typeChecker = info.program.getTypeChecker();
55+
sourceFile.forEachChild(function walk(node) {
56+
if (
57+
ts.isPropertyAccessExpression(node) &&
58+
node.name.text === METHOD_NAME &&
59+
isRouterType(typeChecker, node.expression, routerSpecifier)
60+
) {
61+
locations.push({file: projectFile(sourceFile, info), position: node.name.getStart()});
62+
} else {
63+
node.forEachChild(walk);
64+
}
65+
});
66+
}
67+
68+
return confirmAsSerializable({locations});
69+
}
70+
71+
override async migrate(globalData: CompilationUnitData) {
72+
const replacements = globalData.locations.map(({file, position}) => {
73+
return new Replacement(
74+
file,
75+
new TextUpdate({
76+
position: position,
77+
end: position + METHOD_NAME.length,
78+
toInsert: 'currentNavigation',
79+
}),
80+
);
81+
});
82+
83+
return confirmAsSerializable({replacements});
84+
}
85+
86+
override async combine(
87+
unitA: CompilationUnitData,
88+
unitB: CompilationUnitData,
89+
): Promise<Serializable<CompilationUnitData>> {
90+
const seen = new Set<string>();
91+
const locations: Location[] = [];
92+
const combined = [...unitA.locations, ...unitB.locations];
93+
94+
for (const location of combined) {
95+
const key = `${location.file.id}#${location.position}`;
96+
if (!seen.has(key)) {
97+
seen.add(key);
98+
locations.push(location);
99+
}
100+
}
101+
102+
return confirmAsSerializable({locations});
103+
}
104+
105+
override async globalMeta(
106+
combinedData: CompilationUnitData,
107+
): Promise<Serializable<CompilationUnitData>> {
108+
return confirmAsSerializable(combinedData);
109+
}
110+
111+
override async stats() {
112+
return confirmAsSerializable({});
113+
}
114+
}
115+
116+
/**
117+
* Checks if the given symbol represents a Router type.
118+
*/
119+
function isRouterType(
120+
typeChecker: ts.TypeChecker,
121+
expression: ts.Expression,
122+
routerSpecifier: ts.ImportSpecifier,
123+
): boolean {
124+
const expressionType = typeChecker.getTypeAtLocation(expression);
125+
const expressionSymbol = expressionType.getSymbol();
126+
if (!expressionSymbol) {
127+
return false;
128+
}
129+
130+
const declarations = expressionSymbol.getDeclarations() ?? [];
131+
132+
for (const declaration of declarations) {
133+
if (isReferenceToImport(typeChecker, declaration, routerSpecifier)) {
134+
return true;
135+
}
136+
}
137+
138+
return declarations.some((decl) => decl === routerSpecifier);
139+
}

0 commit comments

Comments
 (0)