├── .editorconfig
├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
└── tasks.json
├── README.md
├── angular.json
├── package-lock.json
├── package.json
├── projects
├── file-routing
│ ├── public
│ │ └── favicon.ico
│ ├── src
│ │ ├── app
│ │ │ ├── app.component.ts
│ │ │ └── app.config.ts
│ │ ├── index.html
│ │ ├── main.ts
│ │ ├── routeTree.gen.ts
│ │ ├── routes
│ │ │ ├── __root.ts
│ │ │ ├── about.ts
│ │ │ ├── index.ts
│ │ │ ├── lazy-foo.lazy.ts
│ │ │ └── lazy-foo.ts
│ │ └── styles.css
│ ├── tsconfig.app.json
│ └── tsconfig.spec.json
├── router-devtools
│ ├── README.md
│ ├── ng-package.json
│ ├── package.json
│ ├── src
│ │ ├── lib
│ │ │ └── router-devtools.ts
│ │ └── public-api.ts
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ └── tsconfig.spec.json
└── router
│ ├── README.md
│ ├── ng-package.json
│ ├── package.json
│ ├── src
│ ├── lib
│ │ ├── can-go-back.ts
│ │ ├── default-error.ts
│ │ ├── default-not-found.ts
│ │ ├── distinct-until-ref-changed.ts
│ │ ├── file-route.ts
│ │ ├── index.ts
│ │ ├── is-dev-mode.ts
│ │ ├── key.ts
│ │ ├── link.ts
│ │ ├── loader-data.ts
│ │ ├── loader-deps.ts
│ │ ├── location.ts
│ │ ├── match-route.ts
│ │ ├── match.ts
│ │ ├── matches.ts
│ │ ├── outlet.ts
│ │ ├── params.ts
│ │ ├── route-context.ts
│ │ ├── route.ts
│ │ ├── router-devtools.ts
│ │ ├── router-root.ts
│ │ ├── router-state.ts
│ │ ├── router.ts
│ │ ├── search.ts
│ │ └── transitioner.ts
│ └── public-api.ts
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ └── tsconfig.spec.json
├── public
└── favicon.ico
├── src
├── app
│ ├── about
│ │ ├── about.route.ts
│ │ └── about.ts
│ ├── app.config.ts
│ ├── app.spec.ts
│ ├── app.ts
│ ├── auth-state.ts
│ ├── child.ts
│ ├── home.ts
│ ├── login.ts
│ ├── parent.ts
│ ├── protected.ts
│ ├── root.route.ts
│ ├── router.ts
│ ├── spinner.ts
│ └── todos-client.ts
├── index.html
├── main.ts
└── styles.css
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tsr.config.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 | ij_typescript_use_double_quotes = false
14 |
15 | [*.md]
16 | max_line_length = off
17 | trim_trailing_whitespace = false
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Miscellaneous
32 | /.angular/cache
33 | .sass-cache/
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | testem.log
38 | /typings
39 |
40 | # System files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3 | "recommendations": ["angular.ng-template"]
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "ng serve",
7 | "type": "chrome",
8 | "request": "launch",
9 | "preLaunchTask": "npm: start",
10 | "url": "http://localhost:4200/"
11 | },
12 | {
13 | "name": "ng test",
14 | "type": "chrome",
15 | "request": "launch",
16 | "preLaunchTask": "npm: test",
17 | "url": "http://localhost:9876/debug.html"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
3 | "version": "2.0.0",
4 | "tasks": [
5 | {
6 | "type": "npm",
7 | "script": "start",
8 | "isBackground": true,
9 | "problemMatcher": {
10 | "owner": "typescript",
11 | "pattern": "$tsc",
12 | "background": {
13 | "activeOnStart": true,
14 | "beginsPattern": {
15 | "regexp": "(.*?)"
16 | },
17 | "endsPattern": {
18 | "regexp": "bundle generation complete"
19 | }
20 | }
21 | }
22 | },
23 | {
24 | "type": "npm",
25 | "script": "test",
26 | "isBackground": true,
27 | "problemMatcher": {
28 | "owner": "typescript",
29 | "pattern": "$tsc",
30 | "background": {
31 | "activeOnStart": true,
32 | "beginsPattern": {
33 | "regexp": "(.*?)"
34 | },
35 | "endsPattern": {
36 | "regexp": "bundle generation complete"
37 | }
38 | }
39 | }
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tanstack Router for Angular
2 |
3 | This is a prototype of the TanStack Router for Angular
4 |
5 | ## Setup
6 |
7 | ```sh
8 | npm install
9 | ```
10 |
11 | ## Development
12 |
13 | To start a local development server, run:
14 |
15 | ```bash
16 | ng serve
17 | ```
18 |
19 | Navigate to http://localhost:4200
20 |
21 | ## Support
22 |
23 | Sponsor me on [GitHub](https://github.com/sponsors/brandonroberts)
24 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "tanstack-router-angular": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "inlineTemplate": true,
11 | "inlineStyle": true
12 | }
13 | },
14 | "root": "",
15 | "sourceRoot": "src",
16 | "prefix": "app",
17 | "architect": {
18 | "build": {
19 | "builder": "@angular-devkit/build-angular:application",
20 | "options": {
21 | "outputPath": "dist/tanstack-router-angular",
22 | "index": "src/index.html",
23 | "browser": "src/main.ts",
24 | "polyfills": ["zone.js"],
25 | "tsConfig": "tsconfig.app.json",
26 | "assets": [
27 | {
28 | "glob": "**/*",
29 | "input": "public"
30 | }
31 | ],
32 | "styles": ["src/styles.css"],
33 | "scripts": []
34 | },
35 | "configurations": {
36 | "production": {
37 | "budgets": [
38 | {
39 | "type": "initial",
40 | "maximumWarning": "500kB",
41 | "maximumError": "1MB"
42 | },
43 | {
44 | "type": "anyComponentStyle",
45 | "maximumWarning": "4kB",
46 | "maximumError": "8kB"
47 | }
48 | ],
49 | "outputHashing": "all"
50 | },
51 | "development": {
52 | "optimization": false,
53 | "extractLicenses": false,
54 | "sourceMap": true
55 | }
56 | },
57 | "defaultConfiguration": "production"
58 | },
59 | "serve": {
60 | "builder": "@angular-devkit/build-angular:dev-server",
61 | "configurations": {
62 | "production": {
63 | "buildTarget": "tanstack-router-angular:build:production"
64 | },
65 | "development": {
66 | "buildTarget": "tanstack-router-angular:build:development"
67 | }
68 | },
69 | "defaultConfiguration": "development"
70 | },
71 | "extract-i18n": {
72 | "builder": "@angular-devkit/build-angular:extract-i18n"
73 | },
74 | "test": {
75 | "builder": "@angular-devkit/build-angular:karma",
76 | "options": {
77 | "polyfills": ["zone.js", "zone.js/testing"],
78 | "tsConfig": "tsconfig.spec.json",
79 | "assets": [
80 | {
81 | "glob": "**/*",
82 | "input": "public"
83 | }
84 | ],
85 | "styles": ["src/styles.css"],
86 | "scripts": []
87 | }
88 | }
89 | }
90 | },
91 | "router": {
92 | "projectType": "library",
93 | "root": "projects/router",
94 | "sourceRoot": "projects/router/src",
95 | "prefix": "lib",
96 | "architect": {
97 | "build": {
98 | "builder": "@angular-devkit/build-angular:ng-packagr",
99 | "options": {
100 | "project": "projects/router/ng-package.json"
101 | },
102 | "configurations": {
103 | "production": {
104 | "tsConfig": "projects/router/tsconfig.lib.prod.json"
105 | },
106 | "development": {
107 | "tsConfig": "projects/router/tsconfig.lib.json"
108 | }
109 | },
110 | "defaultConfiguration": "production"
111 | },
112 | "test": {
113 | "builder": "@angular-devkit/build-angular:karma",
114 | "options": {
115 | "tsConfig": "projects/router/tsconfig.spec.json",
116 | "polyfills": ["zone.js", "zone.js/testing"]
117 | }
118 | }
119 | }
120 | },
121 | "router-devtools": {
122 | "projectType": "library",
123 | "root": "projects/router-devtools",
124 | "sourceRoot": "projects/router-devtools/src",
125 | "prefix": "lib",
126 | "architect": {
127 | "build": {
128 | "builder": "@angular-devkit/build-angular:ng-packagr",
129 | "options": {
130 | "project": "projects/router-devtools/ng-package.json"
131 | },
132 | "configurations": {
133 | "production": {
134 | "tsConfig": "projects/router-devtools/tsconfig.lib.prod.json"
135 | },
136 | "development": {
137 | "tsConfig": "projects/router-devtools/tsconfig.lib.json"
138 | }
139 | },
140 | "defaultConfiguration": "production"
141 | }
142 | }
143 | },
144 | "file-routing": {
145 | "projectType": "application",
146 | "schematics": {},
147 | "root": "projects/file-routing",
148 | "sourceRoot": "projects/file-routing/src",
149 | "prefix": "app",
150 | "architect": {
151 | "build": {
152 | "builder": "@angular-devkit/build-angular:application",
153 | "options": {
154 | "outputPath": "dist/file-routing",
155 | "index": "projects/file-routing/src/index.html",
156 | "browser": "projects/file-routing/src/main.ts",
157 | "polyfills": ["zone.js"],
158 | "tsConfig": "projects/file-routing/tsconfig.app.json",
159 | "assets": [
160 | {
161 | "glob": "**/*",
162 | "input": "projects/file-routing/public"
163 | }
164 | ],
165 | "styles": ["projects/file-routing/src/styles.css"],
166 | "scripts": []
167 | },
168 | "configurations": {
169 | "production": {
170 | "budgets": [
171 | {
172 | "type": "initial",
173 | "maximumWarning": "500kB",
174 | "maximumError": "1MB"
175 | },
176 | {
177 | "type": "anyComponentStyle",
178 | "maximumWarning": "4kB",
179 | "maximumError": "8kB"
180 | }
181 | ],
182 | "outputHashing": "all"
183 | },
184 | "development": {
185 | "optimization": false,
186 | "extractLicenses": false,
187 | "sourceMap": true
188 | }
189 | },
190 | "defaultConfiguration": "production"
191 | },
192 | "serve": {
193 | "builder": "@angular-devkit/build-angular:dev-server",
194 | "configurations": {
195 | "production": {
196 | "buildTarget": "file-routing:build:production"
197 | },
198 | "development": {
199 | "buildTarget": "file-routing:build:development"
200 | }
201 | },
202 | "defaultConfiguration": "development"
203 | },
204 | "extract-i18n": {
205 | "builder": "@angular-devkit/build-angular:extract-i18n"
206 | },
207 | "test": {
208 | "builder": "@angular-devkit/build-angular:karma",
209 | "options": {
210 | "polyfills": ["zone.js", "zone.js/testing"],
211 | "tsConfig": "projects/file-routing/tsconfig.spec.json",
212 | "assets": [
213 | {
214 | "glob": "**/*",
215 | "input": "projects/file-routing/public"
216 | }
217 | ],
218 | "styles": ["projects/file-routing/src/styles.css"],
219 | "scripts": []
220 | }
221 | }
222 | }
223 | }
224 | },
225 | "cli": {
226 | "analytics": false
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tanstack-router-angular",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "watch": "ng build --watch --configuration development",
9 | "test": "ng test",
10 | "prettify": "prettier --write ."
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/common": "^19.2.0",
15 | "@angular/compiler": "^19.2.0",
16 | "@angular/core": "^19.2.0",
17 | "@angular/forms": "^19.2.0",
18 | "@angular/platform-browser": "^19.2.0",
19 | "@angular/platform-browser-dynamic": "^19.2.0",
20 | "@angular/router": "^19.2.0",
21 | "@tanstack/angular-store": "^0.7.0",
22 | "@tanstack/history": "*",
23 | "@tanstack/router-core": "*",
24 | "@tanstack/router-devtools-core": "*",
25 | "jsesc": "^3.0.2",
26 | "rxjs": "~7.8.0",
27 | "solid-js": "^1.9.5",
28 | "tanstack-angular-router-experimental": "^0.0.1",
29 | "tiny-invariant": "^1.3.3",
30 | "tiny-warning": "^1.0.3",
31 | "tslib": "^2.3.0",
32 | "zone.js": "~0.15.0"
33 | },
34 | "devDependencies": {
35 | "@angular-devkit/build-angular": "^19.2.4",
36 | "@angular/cli": "^19.2.4",
37 | "@angular/compiler-cli": "^19.2.0",
38 | "@tanstack/router-cli": "^1.114.27",
39 | "@types/jasmine": "~5.1.0",
40 | "jasmine-core": "~5.6.0",
41 | "karma": "~6.4.0",
42 | "karma-chrome-launcher": "~3.2.0",
43 | "karma-coverage": "~2.2.0",
44 | "karma-jasmine": "~5.1.0",
45 | "karma-jasmine-html-reporter": "~2.1.0",
46 | "ng-packagr": "^19.2.0",
47 | "prettier": "^3.5.3",
48 | "prettier-plugin-organize-imports": "^4.1.0",
49 | "typescript": "~5.7.2"
50 | },
51 | "prettier": {
52 | "trailingComma": "es5",
53 | "tabWidth": 2,
54 | "semi": true,
55 | "singleQuote": true,
56 | "htmlWhitespaceSensitivity": "ignore",
57 | "plugins": [
58 | "prettier-plugin-organize-imports"
59 | ]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/projects/file-routing/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brandonroberts/tanstack-angular-router/a89ddd27d8116392c6fbc1205c17f488fd19f660/projects/file-routing/public/favicon.ico
--------------------------------------------------------------------------------
/projects/file-routing/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 | import { Link, Outlet } from 'tanstack-angular-router-experimental';
3 |
4 | @Component({
5 | selector: 'app-root',
6 | template: `
7 |
File Routing
8 |
9 |
20 |
21 |
22 |
23 |
24 | `,
25 | changeDetection: ChangeDetectionStrategy.OnPush,
26 | imports: [Outlet, Link],
27 | })
28 | export class AppComponent {
29 | title = 'file-routing';
30 | }
31 |
--------------------------------------------------------------------------------
/projects/file-routing/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
2 | import { provideRouter } from 'tanstack-angular-router-experimental';
3 | import { routeTree } from '../routeTree.gen';
4 |
5 | export const appConfig: ApplicationConfig = {
6 | providers: [
7 | provideZoneChangeDetection({ eventCoalescing: true }),
8 | provideRouter({ routeTree }),
9 | ],
10 | };
11 |
--------------------------------------------------------------------------------
/projects/file-routing/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FileRouting
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/projects/file-routing/src/main.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser';
2 | import { AppComponent } from './app/app.component';
3 | import { appConfig } from './app/app.config';
4 |
5 | bootstrapApplication(AppComponent, appConfig).catch((err) =>
6 | console.error(err)
7 | );
8 |
--------------------------------------------------------------------------------
/projects/file-routing/src/routeTree.gen.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | // @ts-nocheck
4 |
5 | // noinspection JSUnusedGlobalSymbols
6 |
7 | // This file was automatically generated by TanStack Router.
8 | // You should NOT make any changes in this file as it will be overwritten.
9 | // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10 |
11 | // Import Routes
12 |
13 | import { Route as rootRoute } from './routes/__root';
14 | import { Route as AboutImport } from './routes/about';
15 | import { Route as IndexImport } from './routes/index';
16 | import { Route as LazyFooImport } from './routes/lazy-foo';
17 |
18 | // Create/Update Routes
19 |
20 | const LazyFooRoute = LazyFooImport.update({
21 | id: '/lazy-foo',
22 | path: '/lazy-foo',
23 | getParentRoute: () => rootRoute,
24 | } as any).lazy(() => import('./routes/lazy-foo.lazy').then((d) => d.Route));
25 |
26 | const AboutRoute = AboutImport.update({
27 | id: '/about',
28 | path: '/about',
29 | getParentRoute: () => rootRoute,
30 | } as any);
31 |
32 | const IndexRoute = IndexImport.update({
33 | id: '/',
34 | path: '/',
35 | getParentRoute: () => rootRoute,
36 | } as any);
37 |
38 | // Populate the FileRoutesByPath interface
39 |
40 | declare module 'tanstack-angular-router-experimental' {
41 | interface FileRoutesByPath {
42 | '/': {
43 | id: '/';
44 | path: '/';
45 | fullPath: '/';
46 | preLoaderRoute: typeof IndexImport;
47 | parentRoute: typeof rootRoute;
48 | };
49 | '/about': {
50 | id: '/about';
51 | path: '/about';
52 | fullPath: '/about';
53 | preLoaderRoute: typeof AboutImport;
54 | parentRoute: typeof rootRoute;
55 | };
56 | '/lazy-foo': {
57 | id: '/lazy-foo';
58 | path: '/lazy-foo';
59 | fullPath: '/lazy-foo';
60 | preLoaderRoute: typeof LazyFooImport;
61 | parentRoute: typeof rootRoute;
62 | };
63 | }
64 | }
65 |
66 | // Create and export the route tree
67 |
68 | export interface FileRoutesByFullPath {
69 | '/': typeof IndexRoute;
70 | '/about': typeof AboutRoute;
71 | '/lazy-foo': typeof LazyFooRoute;
72 | }
73 |
74 | export interface FileRoutesByTo {
75 | '/': typeof IndexRoute;
76 | '/about': typeof AboutRoute;
77 | '/lazy-foo': typeof LazyFooRoute;
78 | }
79 |
80 | export interface FileRoutesById {
81 | __root__: typeof rootRoute;
82 | '/': typeof IndexRoute;
83 | '/about': typeof AboutRoute;
84 | '/lazy-foo': typeof LazyFooRoute;
85 | }
86 |
87 | export interface FileRouteTypes {
88 | fileRoutesByFullPath: FileRoutesByFullPath;
89 | fullPaths: '/' | '/about' | '/lazy-foo';
90 | fileRoutesByTo: FileRoutesByTo;
91 | to: '/' | '/about' | '/lazy-foo';
92 | id: '__root__' | '/' | '/about' | '/lazy-foo';
93 | fileRoutesById: FileRoutesById;
94 | }
95 |
96 | export interface RootRouteChildren {
97 | IndexRoute: typeof IndexRoute;
98 | AboutRoute: typeof AboutRoute;
99 | LazyFooRoute: typeof LazyFooRoute;
100 | }
101 |
102 | const rootRouteChildren: RootRouteChildren = {
103 | IndexRoute: IndexRoute,
104 | AboutRoute: AboutRoute,
105 | LazyFooRoute: LazyFooRoute,
106 | };
107 |
108 | export const routeTree = rootRoute
109 | ._addFileChildren(rootRouteChildren)
110 | ._addFileTypes();
111 |
112 | /* ROUTE_MANIFEST_START
113 | {
114 | "routes": {
115 | "__root__": {
116 | "filePath": "__root.ts",
117 | "children": [
118 | "/",
119 | "/about",
120 | "/lazy-foo"
121 | ]
122 | },
123 | "/": {
124 | "filePath": "index.ts"
125 | },
126 | "/about": {
127 | "filePath": "about.ts"
128 | },
129 | "/lazy-foo": {
130 | "filePath": "lazy-foo.ts"
131 | }
132 | }
133 | }
134 | ROUTE_MANIFEST_END */
135 |
--------------------------------------------------------------------------------
/projects/file-routing/src/routes/__root.ts:
--------------------------------------------------------------------------------
1 | import { createRootRoute } from 'tanstack-angular-router-experimental';
2 | import { AppComponent } from '../app/app.component';
3 |
4 | export const Route = createRootRoute({
5 | component: () => AppComponent,
6 | });
7 |
--------------------------------------------------------------------------------
/projects/file-routing/src/routes/about.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 | import { createFileRoute } from 'tanstack-angular-router-experimental';
3 |
4 | export const Route = createFileRoute('/about')({
5 | component: () => AboutPage,
6 | });
7 |
8 | @Component({
9 | selector: 'app-about',
10 | template: `
11 | About
12 | `,
13 | changeDetection: ChangeDetectionStrategy.OnPush,
14 | })
15 | export class AboutPage {}
16 |
--------------------------------------------------------------------------------
/projects/file-routing/src/routes/index.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 | import { createFileRoute } from 'tanstack-angular-router-experimental';
3 |
4 | export const Route = createFileRoute('/')({
5 | component: () => HomePage,
6 | });
7 |
8 | @Component({
9 | selector: 'app-home',
10 | template: `
11 | Home
12 | `,
13 | changeDetection: ChangeDetectionStrategy.OnPush,
14 | })
15 | export class HomePage {}
16 |
--------------------------------------------------------------------------------
/projects/file-routing/src/routes/lazy-foo.lazy.ts:
--------------------------------------------------------------------------------
1 | import { JsonPipe } from '@angular/common';
2 | import { ChangeDetectionStrategy, Component } from '@angular/core';
3 | import { createLazyFileRoute } from 'tanstack-angular-router-experimental';
4 |
5 | export const Route = createLazyFileRoute('/lazy-foo')({
6 | component: () => LazyPage,
7 | });
8 |
9 | @Component({
10 | selector: 'app-lazy',
11 | template: `
12 | Lazy page foo
13 |
14 | {{ loaderData() | json }}
15 | `,
16 | changeDetection: ChangeDetectionStrategy.OnPush,
17 | imports: [JsonPipe],
18 | })
19 | export class LazyPage {
20 | loaderData = Route.loaderData();
21 | }
22 |
--------------------------------------------------------------------------------
/projects/file-routing/src/routes/lazy-foo.ts:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from 'tanstack-angular-router-experimental';
2 |
3 | export const Route = createFileRoute('/lazy-foo')({
4 | loader: async () => {
5 | const json = await fetch(
6 | 'https://jsonplaceholder.typicode.com/todos/1'
7 | ).then((response) => response.json());
8 | return json as { id: number; title: string; completed: boolean };
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/projects/file-routing/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/projects/file-routing/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "../../tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "../../out-tsc/app",
7 | "types": []
8 | },
9 | "files": ["src/main.ts"],
10 | "include": ["src/**/*.d.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/projects/file-routing/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "../../tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "../../out-tsc/spec",
7 | "types": ["jasmine"]
8 | },
9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/projects/router-devtools/README.md:
--------------------------------------------------------------------------------
1 | # Tanstack Router for Angular
2 |
3 | This is an **experimental** release of the TanStack Router for Angular.
4 |
5 | ## Setup
6 |
7 | ```sh
8 | npm install tanstack-angular-router-experimental
9 | ```
10 |
--------------------------------------------------------------------------------
/projects/router-devtools/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/router-devtools",
4 | "lib": {
5 | "entryFile": "src/public-api.ts"
6 | },
7 | "allowedNonPeerDependencies": [
8 | "@tanstack/router-devtools-core",
9 | "solid-js"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/projects/router-devtools/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tanstack-angular-router-devtools-experimental",
3 | "version": "0.0.40",
4 | "dependencies": {
5 | "@tanstack/router-devtools-core": "^1.114.29",
6 | "solid-js": "^1.9.5",
7 | "tslib": "^2.3.0"
8 | },
9 | "peerDependencies": {
10 | "@angular/core": "^19.0.0",
11 | "@angular/router": "^19.0.0",
12 | "tanstack-angular-router-experimental": "^0.0.39"
13 | },
14 | "sideEffects": false
15 | }
16 |
--------------------------------------------------------------------------------
/projects/router-devtools/src/lib/router-devtools.ts:
--------------------------------------------------------------------------------
1 | import {
2 | afterNextRender,
3 | booleanAttribute,
4 | computed,
5 | Directive,
6 | effect,
7 | ElementRef,
8 | inject,
9 | Injector,
10 | input,
11 | NgZone,
12 | signal,
13 | untracked,
14 | } from '@angular/core';
15 | import { injectStore } from '@tanstack/angular-store';
16 | import { TanStackRouterDevtoolsCore } from '@tanstack/router-devtools-core';
17 | import { injectRouter } from 'tanstack-angular-router-experimental';
18 |
19 | @Directive({
20 | selector: 'router-devtools,RouterDevtools',
21 | host: { style: 'display: block;' },
22 | })
23 | export class RouterDevtools {
24 | private injectedRouter = injectRouter();
25 | private host = inject>(ElementRef);
26 | private ngZone = inject(NgZone);
27 | private injector = inject(Injector);
28 |
29 | router = input(this.injectedRouter);
30 | initialIsOpen = input(undefined, { transform: booleanAttribute });
31 | panelOptions = input>({});
32 | closeButtonOptions = input>({});
33 | toggleButtonOptions = input>({});
34 | shadowDOMTarget = input();
35 | containerElement = input();
36 | position = input<'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'>();
37 |
38 | private activeRouterState = computed(
39 | injectStore(this.router().__store, (s) => s, { injector: this.injector })
40 | );
41 |
42 | private devtools = signal(null);
43 |
44 | constructor() {
45 | afterNextRender(() => {
46 | const [
47 | router,
48 | initialIsOpen,
49 | panelOptions,
50 | closeButtonOptions,
51 | toggleButtonOptions,
52 | shadowDOMTarget,
53 | containerElement,
54 | position,
55 | activeRouterState,
56 | ] = [
57 | untracked(this.router),
58 | untracked(this.initialIsOpen),
59 | untracked(this.panelOptions),
60 | untracked(this.closeButtonOptions),
61 | untracked(this.toggleButtonOptions),
62 | untracked(this.shadowDOMTarget),
63 | untracked(this.containerElement),
64 | untracked(this.position),
65 | untracked(this.activeRouterState),
66 | ];
67 |
68 | // initial devTools
69 | this.devtools.set(
70 | new TanStackRouterDevtoolsCore({
71 | router,
72 | routerState: activeRouterState,
73 | initialIsOpen,
74 | position,
75 | panelProps: panelOptions,
76 | closeButtonProps: closeButtonOptions,
77 | toggleButtonProps: toggleButtonOptions,
78 | shadowDOMTarget,
79 | containerElement,
80 | })
81 | );
82 | });
83 |
84 | effect(() => {
85 | const devtools = this.devtools();
86 | if (!devtools) return;
87 | this.ngZone.runOutsideAngular(() => devtools.setRouter(this.router()));
88 | });
89 |
90 | effect(() => {
91 | const devtools = this.devtools();
92 | if (!devtools) return;
93 | this.ngZone.runOutsideAngular(() =>
94 | devtools.setRouterState(this.activeRouterState())
95 | );
96 | });
97 |
98 | effect(() => {
99 | const devtools = this.devtools();
100 | if (!devtools) return;
101 |
102 | this.ngZone.runOutsideAngular(() => {
103 | devtools.setOptions({
104 | initialIsOpen: this.initialIsOpen(),
105 | panelProps: this.panelOptions(),
106 | closeButtonProps: this.closeButtonOptions(),
107 | toggleButtonProps: this.toggleButtonOptions(),
108 | position: this.position(),
109 | containerElement: this.containerElement(),
110 | shadowDOMTarget: this.shadowDOMTarget(),
111 | });
112 | });
113 | });
114 |
115 | effect((onCleanup) => {
116 | const devtools = this.devtools();
117 | if (!devtools) return;
118 | this.ngZone.runOutsideAngular(() =>
119 | devtools.mount(this.host.nativeElement)
120 | );
121 | onCleanup(() => {
122 | this.ngZone.runOutsideAngular(() => devtools.unmount());
123 | });
124 | });
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/projects/router-devtools/src/public-api.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/router-devtools';
2 |
--------------------------------------------------------------------------------
/projects/router-devtools/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "../../tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "../../out-tsc/lib",
7 | "declaration": true,
8 | "declarationMap": true,
9 | "inlineSources": true,
10 | "types": []
11 | },
12 | "exclude": ["**/*.spec.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/projects/router-devtools/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "./tsconfig.lib.json",
5 | "compilerOptions": {
6 | "declarationMap": false
7 | },
8 | "angularCompilerOptions": {
9 | "compilationMode": "partial"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/projects/router-devtools/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "../../tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "../../out-tsc/spec",
7 | "types": ["jasmine"]
8 | },
9 | "include": ["**/*.spec.ts", "**/*.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/projects/router/README.md:
--------------------------------------------------------------------------------
1 | # Tanstack Router for Angular
2 |
3 | This is an **experimental** release of the TanStack Router for Angular.
4 |
5 | ## Setup
6 |
7 | ```sh
8 | npm install tanstack-angular-router-experimental
9 | ```
10 |
--------------------------------------------------------------------------------
/projects/router/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/router",
4 | "lib": {
5 | "entryFile": "src/public-api.ts"
6 | },
7 | "allowedNonPeerDependencies": [
8 | "@tanstack/angular-store",
9 | "@tanstack/history",
10 | "@tanstack/router-core",
11 | "@tanstack/router-devtools-core",
12 | "solid-js",
13 | "tiny-invariant",
14 | "tiny-warning"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/projects/router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tanstack-angular-router-experimental",
3 | "version": "0.0.61",
4 | "dependencies": {
5 | "@tanstack/angular-store": "^0.7.0",
6 | "@tanstack/history": "^1.114.29",
7 | "@tanstack/router-core": "^1.114.29",
8 | "@tanstack/router-devtools-core": "^1.114.29",
9 | "solid-js": "^1.9.5",
10 | "tiny-invariant": "^1.3.3",
11 | "tiny-warning": "^1.0.3",
12 | "tslib": "^2.3.0"
13 | },
14 | "peerDependencies": {
15 | "@angular/core": "^19.0.0",
16 | "@angular/router": "^19.0.0",
17 | "rxjs": "^7.0.0"
18 | },
19 | "sideEffects": false
20 | }
21 |
--------------------------------------------------------------------------------
/projects/router/src/lib/can-go-back.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertInInjectionContext,
3 | inject,
4 | Injector,
5 | runInInjectionContext,
6 | } from '@angular/core';
7 | import { toSignal } from '@angular/core/rxjs-interop';
8 | import { routerState$ } from './router-state';
9 |
10 | export function canGoBack$({ injector }: { injector?: Injector } = {}) {
11 | !injector && assertInInjectionContext(canGoBack$);
12 |
13 | if (!injector) {
14 | injector = inject(Injector);
15 | }
16 |
17 | return runInInjectionContext(injector, () => {
18 | return routerState$({
19 | select: (s) => s.location.state.__TSR_index !== 0,
20 | injector,
21 | });
22 | });
23 | }
24 |
25 | export function canGoBack({ injector }: { injector?: Injector } = {}) {
26 | !injector && assertInInjectionContext(canGoBack);
27 |
28 | if (!injector) {
29 | injector = inject(Injector);
30 | }
31 |
32 | return runInInjectionContext(injector, () => {
33 | return toSignal(canGoBack$({ injector }));
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/projects/router/src/lib/default-error.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | inject,
5 | signal,
6 | } from '@angular/core';
7 | import { ERROR_COMPONENT_CONTEXT } from './route';
8 |
9 | @Component({
10 | selector: 'default-error,DefaultError',
11 | template: `
12 |
13 | Something went wrong!
14 |
20 |
21 |
22 | @if (show()) {
23 |
24 |
27 | @if (context.error.message; as message) {
28 | {{ message }}
29 | }
30 |
31 |
32 | }
33 | `,
34 | styles: `
35 | :host {
36 | display: block;
37 | padding: 0.5rem;
38 | max-width: 100%;
39 | }
40 | `,
41 | changeDetection: ChangeDetectionStrategy.OnPush,
42 | })
43 | export class DefaultError {
44 | protected context = inject(ERROR_COMPONENT_CONTEXT);
45 | protected show = signal(false);
46 | }
47 |
--------------------------------------------------------------------------------
/projects/router/src/lib/default-not-found.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'default-not-found,DefaultNotFound',
5 | template: `
6 | Page not found
7 | `,
8 | changeDetection: ChangeDetectionStrategy.OnPush,
9 | host: { style: 'display: contents;' },
10 | })
11 | export class DefaultNotFound {}
12 |
--------------------------------------------------------------------------------
/projects/router/src/lib/distinct-until-ref-changed.ts:
--------------------------------------------------------------------------------
1 | import { distinctUntilChanged } from 'rxjs';
2 |
3 | export function distinctUntilRefChanged() {
4 | return distinctUntilChanged(Object.is);
5 | }
6 |
--------------------------------------------------------------------------------
/projects/router/src/lib/file-route.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AnyContext,
3 | AnyRoute,
4 | AnyRouter,
5 | Constrain,
6 | ConstrainLiteral,
7 | FileBaseRouteOptions,
8 | FileRoutesByPath,
9 | LazyRouteOptions,
10 | RegisteredRouter,
11 | ResolveParams,
12 | RouteById,
13 | RouteConstraints,
14 | RouteIds,
15 | RouteLoaderFn,
16 | UpdatableRouteOptions,
17 | } from '@tanstack/router-core';
18 | import warning from 'tiny-warning';
19 | import { loaderData, loaderData$, LoaderDataRoute } from './loader-data';
20 | import { loaderDeps, loaderDeps$, LoaderDepsRoute } from './loader-deps';
21 | import { match, match$, MatchRoute } from './match';
22 | import { params, params$, ParamsRoute } from './params';
23 | import { createRoute, Route } from './route';
24 | import {
25 | routeContext,
26 | routeContext$,
27 | RouteContextRoute,
28 | } from './route-context';
29 | import { search, search$, SearchRoute } from './search';
30 |
31 | export function createFileRoute<
32 | TFilePath extends keyof FileRoutesByPath,
33 | TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'],
34 | TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'],
35 | TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'],
36 | TFullPath extends
37 | RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'],
38 | >(
39 | path: TFilePath
40 | ): FileRoute['createRoute'] {
41 | return new FileRoute(path, {
42 | silent: true,
43 | }).createRoute;
44 | }
45 |
46 | /**
47 | @deprecated It's no longer recommended to use the `FileRoute` class directly.
48 | Instead, use `createFileRoute('/path/to/file')(options)` to create a file route.
49 | */
50 | export class FileRoute<
51 | TFilePath extends keyof FileRoutesByPath,
52 | TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'],
53 | TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'],
54 | TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'],
55 | TFullPath extends
56 | RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'],
57 | > {
58 | silent?: boolean;
59 |
60 | constructor(
61 | public path: TFilePath,
62 | _opts?: { silent: boolean }
63 | ) {
64 | this.silent = _opts?.silent;
65 | }
66 |
67 | createRoute = <
68 | TSearchValidator = undefined,
69 | TParams = ResolveParams,
70 | TRouteContextFn = AnyContext,
71 | TBeforeLoadFn = AnyContext,
72 | TLoaderDeps extends Record = {},
73 | TLoaderFn = undefined,
74 | TChildren = unknown,
75 | >(
76 | options?: FileBaseRouteOptions<
77 | TParentRoute,
78 | TId,
79 | TPath,
80 | TSearchValidator,
81 | TParams,
82 | TLoaderDeps,
83 | TLoaderFn,
84 | AnyContext,
85 | TRouteContextFn,
86 | TBeforeLoadFn
87 | > &
88 | UpdatableRouteOptions<
89 | TParentRoute,
90 | TId,
91 | TFullPath,
92 | TParams,
93 | TSearchValidator,
94 | TLoaderFn,
95 | TLoaderDeps,
96 | AnyContext,
97 | TRouteContextFn,
98 | TBeforeLoadFn
99 | >
100 | ): Route<
101 | TParentRoute,
102 | TPath,
103 | TFullPath,
104 | TFilePath,
105 | TId,
106 | TSearchValidator,
107 | TParams,
108 | AnyContext,
109 | TRouteContextFn,
110 | TBeforeLoadFn,
111 | TLoaderDeps,
112 | TLoaderFn,
113 | TChildren,
114 | unknown
115 | > => {
116 | warning(
117 | this.silent,
118 | 'FileRoute is deprecated and will be removed in the next major version. Use the createFileRoute(path)(options) function instead.'
119 | );
120 | const route = createRoute(options as any);
121 | (route as any).isRoot = false;
122 | return route as any;
123 | };
124 | }
125 |
126 | /**
127 | @deprecated It's recommended not to split loaders into separate files.
128 | Instead, place the loader function in the the main route file, inside the
129 | `createFileRoute('/path/to/file)(options)` options.
130 | */
131 | export function FileRouteLoader<
132 | TFilePath extends keyof FileRoutesByPath,
133 | TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'],
134 | >(
135 | _path: TFilePath
136 | ): (
137 | loaderFn: Constrain<
138 | TLoaderFn,
139 | RouteLoaderFn<
140 | TRoute['parentRoute'],
141 | TRoute['types']['id'],
142 | TRoute['types']['params'],
143 | TRoute['types']['loaderDeps'],
144 | TRoute['types']['routerContext'],
145 | TRoute['types']['routeContextFn'],
146 | TRoute['types']['beforeLoadFn']
147 | >
148 | >
149 | ) => TLoaderFn {
150 | warning(
151 | false,
152 | `FileRouteLoader is deprecated and will be removed in the next major version. Please place the loader function in the the main route file, inside the \`createFileRoute('/path/to/file')(options)\` options`
153 | );
154 | return (loaderFn) => loaderFn as any;
155 | }
156 |
157 | export class LazyRoute {
158 | options: {
159 | id: string;
160 | } & LazyRouteOptions;
161 |
162 | constructor(
163 | opts: {
164 | id: string;
165 | } & LazyRouteOptions
166 | ) {
167 | this.options = opts;
168 | }
169 |
170 | match$: MatchRoute = (opts) =>
171 | match$({ ...opts, from: this.options.id } as any) as any;
172 | match: MatchRoute = (opts) =>
173 | match({ ...opts, from: this.options.id } as any) as any;
174 |
175 | routeContext$: RouteContextRoute = (opts) =>
176 | routeContext$({ ...opts, from: this.options.id } as any);
177 | routeContext: RouteContextRoute = (opts) =>
178 | routeContext({ ...opts, from: this.options.id } as any);
179 |
180 | search$: SearchRoute = (opts) =>
181 | search$({ ...opts, from: this.options.id } as any) as any;
182 | search: SearchRoute = (opts) =>
183 | search({ ...opts, from: this.options.id } as any) as any;
184 |
185 | params$: ParamsRoute = (opts) =>
186 | params$({ ...opts, from: this.options.id } as any) as any;
187 | params: ParamsRoute = (opts) =>
188 | params({ ...opts, from: this.options.id } as any) as any;
189 |
190 | loaderDeps$: LoaderDepsRoute = (opts) =>
191 | loaderDeps$({ ...opts, from: this.options.id } as any);
192 | loaderDeps: LoaderDepsRoute = (opts) =>
193 | loaderDeps({ ...opts, from: this.options.id } as any);
194 |
195 | loaderData$: LoaderDataRoute = (opts) =>
196 | loaderData$({ ...opts, from: this.options.id } as any);
197 | loaderData: LoaderDataRoute = (opts) =>
198 | loaderData({ ...opts, from: this.options.id } as any);
199 | }
200 |
201 | export function createLazyRoute<
202 | TRouter extends AnyRouter = RegisteredRouter,
203 | TId extends string = string,
204 | TRoute extends AnyRoute = RouteById,
205 | >(id: ConstrainLiteral>) {
206 | return (opts: LazyRouteOptions) => {
207 | return new LazyRoute({
208 | id: id,
209 | ...opts,
210 | });
211 | };
212 | }
213 | export function createLazyFileRoute<
214 | TFilePath extends keyof FileRoutesByPath,
215 | TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'],
216 | >(id: TFilePath) {
217 | return (opts: LazyRouteOptions) => new LazyRoute({ id, ...opts });
218 | }
219 |
--------------------------------------------------------------------------------
/projects/router/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { default as invariant } from 'tiny-invariant';
2 | export { default as warning } from 'tiny-warning';
3 |
4 | export {
5 | TSR_DEFERRED_PROMISE,
6 | cleanPath,
7 | createControlledPromise,
8 | decode,
9 | deepEqual,
10 | defaultParseSearch,
11 | defaultSerializeError,
12 | defaultStringifySearch,
13 | defer,
14 | encode,
15 | escapeJSON,
16 | functionalUpdate,
17 | interpolatePath,
18 | isMatch,
19 | isPlainArray,
20 | isPlainObject,
21 | joinPaths,
22 | matchByPath,
23 | matchPathname,
24 | parsePathname,
25 | parseSearchWith, // SSR
26 | pick,
27 | removeBasepath,
28 | replaceEqualDeep,
29 | resolvePath,
30 | retainSearchParams,
31 | rootRouteId,
32 | shallow,
33 | stringifySearchWith,
34 | stripSearchParams,
35 | trimPath,
36 | trimPathLeft,
37 | trimPathRight,
38 | } from '@tanstack/router-core';
39 |
40 | export type {
41 | AbsoluteToPath,
42 | ActiveOptions,
43 | AllContext,
44 | AllLoaderData,
45 | AllParams,
46 | AnyContext,
47 | AnyPathParams,
48 | AnyRedirect,
49 | AnyRoute,
50 | AnyRouteMatch,
51 | AnyRouteWithContext,
52 | AnyRouter,
53 | AnyRouterWithContext,
54 | AnySchema,
55 | AnyValidator,
56 | AnyValidatorAdapter,
57 | AnyValidatorFn,
58 | AnyValidatorObj,
59 | Assign,
60 | BaseRouteOptions,
61 | BeforeLoadContextOptions,
62 | BeforeLoadContextParameter,
63 | BeforeLoadFn,
64 | BuildLocationFn,
65 | BuildNextOptions,
66 | CommitLocationOptions,
67 | Constrain,
68 | ContextAsyncReturnType,
69 | ContextOptions,
70 | ContextReturnType,
71 | ControllablePromise,
72 | ControlledPromise,
73 | DefaultValidator,
74 | DeferredPromise,
75 | DeferredPromiseState,
76 | ErrorComponentProps,
77 | ErrorRouteProps,
78 | Expand,
79 | ExtractedEntry,
80 | ExtractedPromise,
81 | ExtractedStream,
82 | FileBaseRouteOptions,
83 | FileRouteTypes,
84 | FileRoutesByPath,
85 | FullSearchSchema,
86 | FullSearchSchemaInput,
87 | FullSearchSchemaOption,
88 | InferAllContext,
89 | InferAllParams,
90 | InferDescendantToPaths,
91 | InferFullSearchSchema,
92 | InferFullSearchSchemaInput,
93 | InjectedHtmlEntry,
94 | IntersectAssign,
95 | LazyRouteOptions,
96 | LinkOptions,
97 | ListenerFn,
98 | LoaderFnContext,
99 | LooseAsyncReturnType,
100 | LooseReturnType,
101 | MakeOptionalPathParams,
102 | MakeRemountDepsOptionsUnion,
103 | MakeRouteMatch,
104 | MakeRouteMatchUnion,
105 | Manifest,
106 | MatchLocation,
107 | MergeAll,
108 | MetaDescriptor,
109 | NavigateFn,
110 | NavigateOptions,
111 | NotFoundRouteProps,
112 | ParamsOptions,
113 | ParseParamsFn,
114 | ParsePathParams,
115 | ParseRoute,
116 | ParseSplatParams,
117 | ParsedLocation,
118 | PathParamOptions,
119 | PreloadableObj,
120 | Redirect,
121 | Register,
122 | RegisteredRouter,
123 | RelativeToCurrentPath,
124 | RelativeToParentPath,
125 | RelativeToPath,
126 | RelativeToPathAutoComplete,
127 | RemountDepsOptions,
128 | RemoveLeadingSlashes,
129 | RemoveTrailingSlashes,
130 | ResolveAllContext,
131 | ResolveAllParamsFromParent,
132 | ResolveFullPath,
133 | ResolveFullSearchSchema,
134 | ResolveFullSearchSchemaInput,
135 | ResolveId,
136 | ResolveLoaderData,
137 | ResolveParams,
138 | ResolveRelativePath,
139 | ResolveRoute,
140 | ResolveRouteContext,
141 | ResolveSearchValidatorInput,
142 | ResolveSearchValidatorInputFn,
143 | ResolveValidatorInput,
144 | ResolveValidatorInputFn,
145 | ResolveValidatorOutput,
146 | ResolveValidatorOutputFn,
147 | ResolvedRedirect,
148 | RootRouteId,
149 | RootRouteOptions,
150 | RouteById,
151 | RouteByPath,
152 | RouteConstraints,
153 | RouteContext,
154 | RouteContextFn,
155 | RouteContextOptions,
156 | RouteContextParameter,
157 | RouteIds,
158 | RouteLinkEntry,
159 | RouteLoaderFn,
160 | RouteMask,
161 | RouteOptions,
162 | RoutePathOptions,
163 | RoutePathOptionsIntersection,
164 | RoutePaths,
165 | RouterConstructorOptions,
166 | RouterContextOptions,
167 | RouterErrorSerializer,
168 | RouterEvent,
169 | RouterEvents,
170 | RouterListener,
171 | RouterManagedTag,
172 | RouterOptions,
173 | RouterState,
174 | RoutesById,
175 | RoutesByPath,
176 | SearchFilter,
177 | SearchParamOptions,
178 | SearchParser,
179 | SearchSchemaInput,
180 | SearchSerializer,
181 | Segment,
182 | Serializable,
183 | SerializerParse,
184 | SerializerParseBy,
185 | SerializerStringify,
186 | SerializerStringifyBy,
187 | SplatParams,
188 | StartSerializer,
189 | StaticDataRouteOption,
190 | StreamState,
191 | StringifyParamsFn,
192 | MatchRouteOptions as TanstackMatchRouteOptions,
193 | RouteMatch as TanstackRouteMatch,
194 | ToMaskOptions,
195 | ToOptions,
196 | ToPathOption,
197 | ToSubOptions,
198 | TrailingSlashOption,
199 | TrimPath,
200 | TrimPathLeft,
201 | TrimPathRight,
202 | UpdatableRouteOptions,
203 | UpdatableStaticRouteOption,
204 | UseNavigateResult,
205 | Validator,
206 | ValidatorAdapter,
207 | ValidatorFn,
208 | ValidatorObj,
209 | } from '@tanstack/router-core';
210 |
211 | export {
212 | createBrowserHistory,
213 | createHashHistory,
214 | createHistory,
215 | createMemoryHistory,
216 | } from '@tanstack/history';
217 |
218 | export type {
219 | BlockerFn,
220 | HistoryLocation,
221 | HistoryState,
222 | ParsedPath,
223 | RouterHistory,
224 | } from '@tanstack/history';
225 |
226 | export { Link, linkOptions } from './link';
227 | export { MatchRoute, matchRoute, matchRoute$ } from './match-route';
228 | export type { MakeMatchRouteOptions, MatchRouteOptions } from './match-route';
229 | export {
230 | Matches,
231 | childMatches,
232 | childMatches$,
233 | matches,
234 | matches$,
235 | parentMatches,
236 | parentMatches$,
237 | } from './matches';
238 |
239 | export { OnRendered, Outlet, RouteMatch } from './outlet';
240 |
241 | export { loaderData, loaderData$ } from './loader-data';
242 | export { loaderDeps, loaderDeps$ } from './loader-deps';
243 | export { match, match$ } from './match';
244 |
245 | export { isRedirect, redirect } from '@tanstack/router-core';
246 |
247 | export {
248 | LazyRoute,
249 | createFileRoute,
250 | createLazyFileRoute,
251 | createLazyRoute,
252 | } from './file-route';
253 | export {
254 | NotFoundRoute,
255 | RootRoute,
256 | Route,
257 | RouteApi,
258 | createRootRoute,
259 | createRootRouteWithContext,
260 | createRoute,
261 | routeApi,
262 | } from './route';
263 |
264 | export type { AnyRootRoute, RouteComponent } from './route';
265 |
266 | export {
267 | NgRouter,
268 | ROUTER,
269 | ROUTER_STATE,
270 | createRouter,
271 | injectRouter,
272 | injectRouterState,
273 | provideRouter,
274 | } from './router';
275 | export * from './router-root';
276 |
277 | export {
278 | PathParamError,
279 | SearchParamError,
280 | componentTypes,
281 | getInitialRouterState,
282 | lazyFn,
283 | } from '@tanstack/router-core';
284 |
285 | export { params, params$ } from './params';
286 | export { search, search$ } from './search';
287 |
288 | export { canGoBack, canGoBack$ } from './can-go-back';
289 | export { location, location$ } from './location';
290 | export { routeContext, routeContext$ } from './route-context';
291 | export { routerState, routerState$ } from './router-state';
292 |
293 | export { isNotFound, notFound } from '@tanstack/router-core';
294 | export type { NotFoundError } from '@tanstack/router-core';
295 | export { DefaultNotFound } from './default-not-found';
296 |
297 | export type { ValidateLinkOptions, ValidateLinkOptionsArray } from './link';
298 |
299 | export type {
300 | InferFrom,
301 | InferMaskFrom,
302 | InferMaskTo,
303 | InferSelected,
304 | InferShouldThrow,
305 | InferStrict,
306 | InferTo,
307 | ValidateFromPath,
308 | ValidateId,
309 | ValidateNavigateOptions,
310 | ValidateNavigateOptionsArray,
311 | ValidateParams,
312 | ValidateRedirectOptions,
313 | ValidateRedirectOptionsArray,
314 | ValidateSearch,
315 | ValidateToPath,
316 | ValidateUseParamsResult,
317 | ValidateUseSearchResult,
318 | } from '@tanstack/router-core';
319 |
320 | export * from './router-devtools';
321 | export * from './transitioner';
322 |
--------------------------------------------------------------------------------
/projects/router/src/lib/is-dev-mode.ts:
--------------------------------------------------------------------------------
1 | import { DOCUMENT } from '@angular/common';
2 | import {
3 | assertInInjectionContext,
4 | inject,
5 | Injector,
6 | runInInjectionContext,
7 | } from '@angular/core';
8 |
9 | export function isDevMode({ injector }: { injector?: Injector } = {}) {
10 | !injector && assertInInjectionContext(isDevMode);
11 |
12 | if (!injector) {
13 | injector = inject(Injector);
14 | }
15 |
16 | return runInInjectionContext(injector, () => {
17 | const document = inject(DOCUMENT);
18 | const window = document.defaultView;
19 | return !!window && 'ng' in window;
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/projects/router/src/lib/key.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DestroyRef,
3 | Directive,
4 | effect,
5 | inject,
6 | input,
7 | linkedSignal,
8 | TemplateRef,
9 | ViewContainerRef,
10 | } from '@angular/core';
11 |
12 | @Directive({ selector: 'ng-template[key]' })
13 | export class Key {
14 | key = input.required();
15 |
16 | private vcr = inject(ViewContainerRef);
17 | private templateRef = inject(TemplateRef);
18 |
19 | private previousKey = linkedSignal({
20 | source: this.key,
21 | computation: (_, prev) => prev?.source ?? undefined,
22 | });
23 |
24 | constructor() {
25 | effect(() => {
26 | const [key, previousKey] = [this.key(), this.previousKey()];
27 | if (key === previousKey) return;
28 |
29 | this.vcr.clear();
30 | const ref = this.vcr.createEmbeddedView(this.templateRef, {
31 | key,
32 | previousKey,
33 | });
34 | ref.markForCheck();
35 | });
36 |
37 | inject(DestroyRef).onDestroy(() => {
38 | this.vcr.clear();
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/projects/router/src/lib/link.ts:
--------------------------------------------------------------------------------
1 | import {
2 | computed,
3 | Directive,
4 | effect,
5 | ElementRef,
6 | inject,
7 | input,
8 | signal,
9 | untracked,
10 | } from '@angular/core';
11 | import { toObservable, toSignal } from '@angular/core/rxjs-interop';
12 | import {
13 | AnyRouter,
14 | Constrain,
15 | deepEqual,
16 | exactPathTest,
17 | InferFrom,
18 | InferMaskFrom,
19 | InferMaskTo,
20 | InferTo,
21 | LinkOptions,
22 | preloadWarning,
23 | RegisteredRouter,
24 | removeTrailingSlash,
25 | } from '@tanstack/router-core';
26 | import { combineLatest, map } from 'rxjs';
27 | import { distinctUntilRefChanged } from './distinct-until-ref-changed';
28 | import { matches$ } from './matches';
29 | import { injectRouter } from './router';
30 | import { routerState, routerState$ } from './router-state';
31 |
32 | @Directive({
33 | selector: 'a[link]',
34 | exportAs: 'link',
35 | host: {
36 | '(click)': 'handleClick($event)',
37 | '(focus)': 'handleFocus()',
38 | '(touchstart)': 'handleClick($event)',
39 | '(mouseenter)': 'handleMouseEnter($event)',
40 | '(mouseleave)': 'handleMouseLeave()',
41 | '[class]': '[isActive() ? activeClass() : ""]',
42 | '[attr.data-active]': 'isActive()',
43 | '[attr.data-type]': 'type()',
44 | '[attr.data-transitioning]':
45 | 'transitioning() ? "transitioning" : undefined',
46 | '[attr.href]': 'hostHref()',
47 | '[attr.role]': 'disabled() ? "link" : undefined',
48 | '[attr.aria-disabled]': 'disabled()',
49 | '[attr.aria-current]': 'isActive() ? "page" : undefined',
50 | },
51 | })
52 | export class Link {
53 | linkOptions = input.required({
54 | alias: 'link',
55 | transform: (
56 | value:
57 | | (Omit & {
58 | activeOptions?: LinkOptions['activeOptions'] & { class?: string };
59 | })
60 | | NonNullable
61 | ) => {
62 | return (typeof value === 'object' ? value : { to: value }) as Omit<
63 | LinkOptions,
64 | 'activeOptions'
65 | > & { activeOptions?: LinkOptions['activeOptions'] & { class?: string } };
66 | },
67 | });
68 | linkActiveOptions = input(
69 | { class: 'active' },
70 | {
71 | alias: 'linkActive',
72 | transform: (
73 | value: (LinkOptions['activeOptions'] & { class?: string }) | string
74 | ) => {
75 | if (typeof value === 'string') return { class: value };
76 |
77 | if (!value.class) value.class = 'active';
78 | return value;
79 | },
80 | }
81 | );
82 |
83 | private router = injectRouter();
84 | hostElement = inject>(ElementRef);
85 |
86 | private currentSearch = routerState({ select: (s) => s.location.searchStr });
87 |
88 | protected disabled = computed(() => this.linkOptions()['disabled']);
89 | private to = computed(() => this.linkOptions()['to']);
90 | private userFrom = computed(() => this.linkOptions()['from']);
91 | private userReloadDocument = computed(
92 | () => this.linkOptions()['reloadDocument']
93 | );
94 | private userPreload = computed(() => this.linkOptions()['preload']);
95 | private userPreloadDelay = computed(() => this.linkOptions()['preloadDelay']);
96 |
97 | private activeOptions = computed(
98 | () => this.linkOptions().activeOptions || this.linkActiveOptions() || {}
99 | );
100 | private exactActiveOptions = computed(() => this.activeOptions().exact);
101 | private includeHashActiveOptions = computed(
102 | () => this.activeOptions().includeHash
103 | );
104 | private includeSearchActiveOptions = computed(
105 | () => this.activeOptions().includeSearch
106 | );
107 | private explicitUndefinedActiveOptions = computed(
108 | () => this.activeOptions().explicitUndefined
109 | );
110 | protected activeClass = computed(
111 | () => this.activeOptions().class || 'active'
112 | );
113 |
114 | protected type = computed(() => {
115 | const to = this.to();
116 | try {
117 | new URL(`${to}`);
118 | return 'external';
119 | } catch {
120 | return 'internal';
121 | }
122 | });
123 |
124 | // when `from` is not supplied, use the leaf route of the current matches as the `from` location
125 | // so relative routing works as expected
126 | private from = toSignal(
127 | combineLatest([
128 | toObservable(this.userFrom),
129 | matches$({ select: (matches) => matches[matches.length - 1]?.fullPath }),
130 | ]).pipe(
131 | map(([userFrom, from]) => userFrom ?? from),
132 | distinctUntilRefChanged()
133 | )
134 | );
135 |
136 | private navigateOptions = computed(() => {
137 | return { ...this.linkOptions(), from: this.from() };
138 | });
139 |
140 | private next = computed(() => {
141 | const [options] = [this.navigateOptions(), this.currentSearch()];
142 | try {
143 | return this.router.buildLocation(options as any);
144 | } catch (err) {
145 | return null;
146 | }
147 | });
148 |
149 | private preload = computed(() => {
150 | const userReloadDocument = this.userReloadDocument();
151 | if (userReloadDocument) return false;
152 | const userPreload = this.userPreload();
153 | if (userPreload) return userPreload;
154 | return this.router.options.defaultPreload;
155 | });
156 |
157 | private preloadDelay = computed(() => {
158 | const userPreloadDelay = this.userPreloadDelay();
159 | if (userPreloadDelay) return userPreloadDelay;
160 | return this.router.options.defaultPreloadDelay;
161 | });
162 |
163 | protected hostHref = computed(() => {
164 | const [type, to] = [this.type(), this.to()];
165 | if (type === 'external') return to;
166 |
167 | const disabled = this.disabled();
168 | if (disabled) return undefined;
169 |
170 | const next = this.next();
171 | if (!next) return undefined;
172 |
173 | return next.maskedLocation
174 | ? this.router.history.createHref(next.maskedLocation.href)
175 | : this.router.history.createHref(next.href);
176 | });
177 |
178 | transitioning = signal(false);
179 |
180 | isActive = toSignal(
181 | combineLatest([
182 | toObservable(this.next),
183 | toObservable(this.exactActiveOptions),
184 | toObservable(this.includeSearchActiveOptions),
185 | toObservable(this.includeHashActiveOptions),
186 | routerState$({ select: (s) => s.location }),
187 | ]).pipe(
188 | map(
189 | ([next, exact, includeSearchOptions, includeHashOptions, location]) => {
190 | if (!next) return false;
191 | if (exact) {
192 | const testExact = exactPathTest(
193 | location.pathname,
194 | next.pathname,
195 | this.router.basepath
196 | );
197 | if (!testExact) return false;
198 | } else {
199 | const currentPathSplit = removeTrailingSlash(
200 | location.pathname,
201 | this.router.basepath
202 | ).split('/');
203 | const nextPathSplit = removeTrailingSlash(
204 | next.pathname,
205 | this.router.basepath
206 | ).split('/');
207 | const pathIsFuzzyEqual = nextPathSplit.every(
208 | (d, i) => d === currentPathSplit[i]
209 | );
210 | if (!pathIsFuzzyEqual) {
211 | return false;
212 | }
213 | }
214 |
215 | const includeSearch = includeSearchOptions ?? true;
216 | if (includeSearch) {
217 | const searchTest = deepEqual(location.search, next.search, {
218 | partial: !exact,
219 | ignoreUndefined: !(includeSearchOptions ?? true),
220 | });
221 | if (!searchTest) {
222 | return false;
223 | }
224 | }
225 |
226 | const includeHash = includeHashOptions ?? true;
227 | if (includeHash) {
228 | return location.hash === next.hash;
229 | }
230 |
231 | return true;
232 | }
233 | )
234 | )
235 | );
236 |
237 | constructor() {
238 | effect(() => {
239 | const [disabled, preload] = [
240 | untracked(this.disabled),
241 | untracked(this.preload),
242 | ];
243 | if (!disabled && preload === 'render') {
244 | this.doPreload();
245 | }
246 | });
247 |
248 | effect((onCleanup) => {
249 | const unsub = this.router.subscribe('onResolved', () => {
250 | this.transitioning.set(false);
251 | });
252 | onCleanup(() => unsub());
253 | });
254 | }
255 |
256 | protected handleClick(event: MouseEvent) {
257 | if (this.type() === 'external') return;
258 |
259 | const [disabled, target] = [
260 | this.disabled(),
261 | this.hostElement.nativeElement.target,
262 | ];
263 |
264 | if (
265 | disabled ||
266 | this.isCtrlEvent(event) ||
267 | event.defaultPrevented ||
268 | (target && target !== '_self') ||
269 | event.button !== 0
270 | ) {
271 | return;
272 | }
273 |
274 | event.preventDefault();
275 | this.transitioning.set(true);
276 |
277 | this.router.navigate(this.navigateOptions() as any);
278 | }
279 |
280 | protected handleFocus() {
281 | if (this.disabled() || this.type() === 'external') return;
282 | if (this.preload()) {
283 | this.doPreload();
284 | }
285 | }
286 |
287 | private preloadTimeout: ReturnType | null = null;
288 | protected handleMouseEnter(event: MouseEvent) {
289 | if (
290 | this.disabled() ||
291 | !this.preload() ||
292 | this.isActive() ||
293 | this.type() === 'external'
294 | )
295 | return;
296 |
297 | this.preloadTimeout = setTimeout(() => {
298 | this.preloadTimeout = null;
299 | this.doPreload();
300 | }, this.preloadDelay());
301 | }
302 |
303 | protected handleMouseLeave() {
304 | if (this.disabled() || this.type() === 'external') return;
305 | if (this.preloadTimeout) {
306 | clearTimeout(this.preloadTimeout);
307 | this.preloadTimeout = null;
308 | }
309 | }
310 |
311 | private doPreload() {
312 | this.router.preloadRoute(this.navigateOptions() as any).catch((err) => {
313 | console.warn(err);
314 | console.warn(preloadWarning);
315 | });
316 | }
317 |
318 | private isCtrlEvent(e: MouseEvent) {
319 | return e.metaKey || e.altKey || e.ctrlKey || e.shiftKey;
320 | }
321 | }
322 |
323 | export type ValidateLinkOptions<
324 | TRouter extends AnyRouter = RegisteredRouter,
325 | TOptions = unknown,
326 | TDefaultFrom extends string = string,
327 | > = Constrain<
328 | TOptions,
329 | Omit<
330 | LinkOptions<
331 | TRouter,
332 | InferFrom,
333 | InferTo,
334 | InferMaskFrom,
335 | InferMaskTo
336 | >,
337 | 'activeOptions'
338 | > &
339 | Partial> & {
340 | label?: string | (() => string);
341 | activeOptions?: LinkOptions<
342 | TRouter,
343 | InferFrom,
344 | InferTo,
345 | InferMaskFrom,
346 | InferMaskTo
347 | >['activeOptions'] & { class?: string };
348 | }
349 | >;
350 |
351 | export type ValidateLinkOptionsArray<
352 | TRouter extends AnyRouter = RegisteredRouter,
353 | TOptions extends ReadonlyArray = ReadonlyArray,
354 | TDefaultFrom extends string = string,
355 | > = {
356 | [K in keyof TOptions]: ValidateLinkOptions<
357 | TRouter,
358 | TOptions[K],
359 | TDefaultFrom
360 | >;
361 | };
362 |
363 | export type LinkOptionsFnOptions<
364 | TOptions,
365 | TRouter extends AnyRouter = RegisteredRouter,
366 | > =
367 | TOptions extends ReadonlyArray
368 | ? ValidateLinkOptionsArray
369 | : ValidateLinkOptions;
370 |
371 | export type LinkOptionsFn = <
372 | const TOptions,
373 | TRouter extends AnyRouter = RegisteredRouter,
374 | >(
375 | options: LinkOptionsFnOptions
376 | ) => TOptions;
377 |
378 | export const linkOptions: LinkOptionsFn = (options) => {
379 | return options as any;
380 | };
381 |
--------------------------------------------------------------------------------
/projects/router/src/lib/loader-data.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertInInjectionContext,
3 | inject,
4 | Injector,
5 | runInInjectionContext,
6 | Signal,
7 | } from '@angular/core';
8 | import {
9 | AnyRouter,
10 | RegisteredRouter,
11 | ResolveUseLoaderData,
12 | StrictOrFrom,
13 | UseLoaderDataResult,
14 | } from '@tanstack/router-core';
15 |
16 | import { toSignal } from '@angular/core/rxjs-interop';
17 | import { Observable } from 'rxjs';
18 | import { match$ } from './match';
19 |
20 | export interface LoaderDataBaseOptions<
21 | TRouter extends AnyRouter,
22 | TFrom,
23 | TStrict extends boolean,
24 | TSelected,
25 | > {
26 | select?: (match: ResolveUseLoaderData) => TSelected;
27 | injector?: Injector;
28 | }
29 |
30 | export type LoaderDataOptions<
31 | TRouter extends AnyRouter,
32 | TFrom extends string | undefined,
33 | TStrict extends boolean,
34 | TSelected,
35 | > = StrictOrFrom &
36 | LoaderDataBaseOptions;
37 |
38 | export type LoaderDataRoute = <
39 | TRouter extends AnyRouter = RegisteredRouter,
40 | TSelected = unknown,
41 | >(
42 | opts?: LoaderDataBaseOptions
43 | ) => TObservable extends true
44 | ? Observable>
45 | : Signal>;
46 |
47 | export function loaderData$<
48 | TRouter extends AnyRouter = RegisteredRouter,
49 | const TFrom extends string | undefined = undefined,
50 | TStrict extends boolean = true,
51 | TSelected = unknown,
52 | >({
53 | injector,
54 | ...opts
55 | }: LoaderDataOptions): Observable<
56 | UseLoaderDataResult
57 | > {
58 | !injector && assertInInjectionContext(loaderData$);
59 |
60 | if (!injector) {
61 | injector = inject(Injector);
62 | }
63 |
64 | return runInInjectionContext(injector, () => {
65 | return match$({
66 | injector,
67 | from: opts.from,
68 | strict: opts.strict,
69 | select: (s) => (opts.select ? opts.select(s.loaderData) : s.loaderData),
70 | }) as any;
71 | });
72 | }
73 |
74 | export function loaderData<
75 | TRouter extends AnyRouter = RegisteredRouter,
76 | const TFrom extends string | undefined = undefined,
77 | TStrict extends boolean = true,
78 | TSelected = unknown,
79 | >({
80 | injector,
81 | ...opts
82 | }: LoaderDataOptions): Signal<
83 | UseLoaderDataResult
84 | > {
85 | !injector && assertInInjectionContext(loaderData);
86 |
87 | if (!injector) {
88 | injector = inject(Injector);
89 | }
90 |
91 | return runInInjectionContext(injector, () => {
92 | return toSignal(loaderData$({ injector, ...opts } as any), { injector });
93 | }) as any;
94 | }
95 |
--------------------------------------------------------------------------------
/projects/router/src/lib/loader-deps.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertInInjectionContext,
3 | inject,
4 | Injector,
5 | runInInjectionContext,
6 | Signal,
7 | } from '@angular/core';
8 | import { toSignal } from '@angular/core/rxjs-interop';
9 | import {
10 | AnyRouter,
11 | RegisteredRouter,
12 | ResolveUseLoaderDeps,
13 | StrictOrFrom,
14 | UseLoaderDepsResult,
15 | } from '@tanstack/router-core';
16 | import { Observable } from 'rxjs';
17 | import { match$ } from './match';
18 |
19 | export interface LoaderDepsBaseOptions<
20 | TRouter extends AnyRouter,
21 | TFrom,
22 | TSelected,
23 | > {
24 | select?: (deps: ResolveUseLoaderDeps) => TSelected;
25 | injector?: Injector;
26 | }
27 |
28 | export type LoaderDepsOptions<
29 | TRouter extends AnyRouter,
30 | TFrom extends string | undefined,
31 | TSelected,
32 | > = StrictOrFrom &
33 | LoaderDepsBaseOptions;
34 |
35 | export type LoaderDepsRoute = <
36 | TRouter extends AnyRouter = RegisteredRouter,
37 | TSelected = unknown,
38 | >(
39 | opts?: LoaderDepsBaseOptions
40 | ) => TObservable extends true
41 | ? Observable>
42 | : Signal>;
43 |
44 | export function loaderDeps$<
45 | TRouter extends AnyRouter = RegisteredRouter,
46 | const TFrom extends string | undefined = undefined,
47 | TSelected = unknown,
48 | >({
49 | injector,
50 | ...opts
51 | }: LoaderDepsOptions): Observable<
52 | UseLoaderDepsResult
53 | > {
54 | !injector && assertInInjectionContext(loaderDeps$);
55 |
56 | if (!injector) {
57 | injector = inject(Injector);
58 | }
59 |
60 | return runInInjectionContext(injector, () => {
61 | const { select, ...rest } = opts;
62 | return match$({
63 | ...rest,
64 | select: (s) => {
65 | return select ? select(s.loaderDeps) : s.loaderDeps;
66 | },
67 | }) as Observable>;
68 | });
69 | }
70 |
71 | export function loaderDeps<
72 | TRouter extends AnyRouter = RegisteredRouter,
73 | const TFrom extends string | undefined = undefined,
74 | TSelected = unknown,
75 | >({
76 | injector,
77 | ...opts
78 | }: LoaderDepsOptions): Signal<
79 | UseLoaderDepsResult
80 | > {
81 | !injector && assertInInjectionContext(loaderDeps);
82 |
83 | if (!injector) {
84 | injector = inject(Injector);
85 | }
86 |
87 | return runInInjectionContext(injector, () => {
88 | return toSignal(loaderDeps$({ injector, ...opts } as any), { injector });
89 | }) as any;
90 | }
91 |
--------------------------------------------------------------------------------
/projects/router/src/lib/location.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertInInjectionContext,
3 | inject,
4 | Injector,
5 | runInInjectionContext,
6 | Signal,
7 | } from '@angular/core';
8 | import { toSignal } from '@angular/core/rxjs-interop';
9 | import {
10 | AnyRouter,
11 | RegisteredRouter,
12 | RouterState,
13 | } from '@tanstack/router-core';
14 | import { Observable } from 'rxjs';
15 | import { routerState$ } from './router-state';
16 |
17 | export interface LocationBaseOptions {
18 | select?: (state: RouterState['location']) => TSelected;
19 | injector?: Injector;
20 | }
21 |
22 | export type LocationResult<
23 | TRouter extends AnyRouter,
24 | TSelected,
25 | > = unknown extends TSelected
26 | ? RouterState['location']
27 | : TSelected;
28 |
29 | export function location$<
30 | TRouter extends AnyRouter = RegisteredRouter,
31 | TSelected = unknown,
32 | >({
33 | injector,
34 | select,
35 | }: LocationBaseOptions = {}): Observable<
36 | LocationResult
37 | > {
38 | !injector && assertInInjectionContext(location$);
39 |
40 | if (!injector) {
41 | injector = inject(Injector);
42 | }
43 |
44 | return runInInjectionContext(injector, () => {
45 | return routerState$({
46 | injector,
47 | select: (state) => (select ? select(state.location) : state.location),
48 | }) as Observable>;
49 | });
50 | }
51 |
52 | export function location<
53 | TRouter extends AnyRouter = RegisteredRouter,
54 | TSelected = unknown,
55 | >({ injector, select }: LocationBaseOptions = {}): Signal<
56 | LocationResult
57 | > {
58 | !injector && assertInInjectionContext(location);
59 |
60 | if (!injector) {
61 | injector = inject(Injector);
62 | }
63 |
64 | return runInInjectionContext(injector, () => {
65 | return toSignal(location$({ injector, select })) as Signal<
66 | LocationResult
67 | >;
68 | });
69 | }
70 |
--------------------------------------------------------------------------------
/projects/router/src/lib/match-route.ts:
--------------------------------------------------------------------------------
1 | import {
2 | afterNextRender,
3 | assertInInjectionContext,
4 | DestroyRef,
5 | Directive,
6 | EmbeddedViewRef,
7 | inject,
8 | Injector,
9 | input,
10 | runInInjectionContext,
11 | TemplateRef,
12 | ViewContainerRef,
13 | } from '@angular/core';
14 | import { toObservable, toSignal } from '@angular/core/rxjs-interop';
15 | import {
16 | AnyRouter,
17 | DeepPartial,
18 | MakeOptionalPathParams,
19 | MakeOptionalSearchParams,
20 | MaskOptions,
21 | RegisteredRouter,
22 | MatchRouteOptions as TanstackMatchRouteOptions,
23 | ToSubOptionsProps,
24 | } from '@tanstack/router-core';
25 | import { combineLatest, map, Subscription, switchMap } from 'rxjs';
26 | import { injectRouter } from './router';
27 | import { routerState$ } from './router-state';
28 |
29 | export type MatchRouteOptions<
30 | TRouter extends AnyRouter = RegisteredRouter,
31 | TFrom extends string = string,
32 | TTo extends string | undefined = undefined,
33 | TMaskFrom extends string = TFrom,
34 | TMaskTo extends string = '',
35 | > = ToSubOptionsProps &
36 | DeepPartial> &
37 | DeepPartial> &
38 | MaskOptions &
39 | TanstackMatchRouteOptions & { injector?: Injector };
40 |
41 | export function matchRoute$({
42 | injector,
43 | }: { injector?: Injector } = {}) {
44 | !injector && assertInInjectionContext(matchRoute$);
45 |
46 | if (!injector) {
47 | injector = inject(Injector);
48 | }
49 |
50 | return runInInjectionContext(injector, () => {
51 | const router = injectRouter();
52 | const status$ = routerState$({ select: (s) => s.status });
53 |
54 | return <
55 | const TFrom extends string = string,
56 | const TTo extends string | undefined = undefined,
57 | const TMaskFrom extends string = TFrom,
58 | const TMaskTo extends string = '',
59 | >(
60 | opts: MatchRouteOptions
61 | ) => {
62 | const { pending, caseSensitive, fuzzy, includeSearch, ...rest } = opts;
63 | return status$.pipe(
64 | map(() =>
65 | router.matchRoute(rest as any, {
66 | pending,
67 | caseSensitive,
68 | fuzzy,
69 | includeSearch,
70 | })
71 | )
72 | );
73 | };
74 | });
75 | }
76 |
77 | export function matchRoute({
78 | injector,
79 | }: { injector?: Injector } = {}) {
80 | !injector && assertInInjectionContext(matchRoute);
81 |
82 | if (!injector) {
83 | injector = inject(Injector);
84 | }
85 |
86 | return runInInjectionContext(injector, () => {
87 | const matchRoute$Return = matchRoute$({ injector });
88 | return <
89 | const TFrom extends string = string,
90 | const TTo extends string | undefined = undefined,
91 | const TMaskFrom extends string = TFrom,
92 | const TMaskTo extends string = '',
93 | >(
94 | opts: MatchRouteOptions
95 | ) => {
96 | return toSignal(matchRoute$Return(opts as any), { injector });
97 | };
98 | });
99 | }
100 |
101 | export type MakeMatchRouteOptions<
102 | TRouter extends AnyRouter = RegisteredRouter,
103 | TFrom extends string = string,
104 | TTo extends string | undefined = undefined,
105 | TMaskFrom extends string = TFrom,
106 | TMaskTo extends string = '',
107 | > = MatchRouteOptions;
108 |
109 | @Directive({ selector: 'ng-template[matchRoute]' })
110 | export class MatchRoute<
111 | TRouter extends AnyRouter = RegisteredRouter,
112 | const TFrom extends string = string,
113 | const TTo extends string | undefined = undefined,
114 | const TMaskFrom extends string = TFrom,
115 | const TMaskTo extends string = '',
116 | > {
117 | matchRoute =
118 | input.required<
119 | MakeMatchRouteOptions
120 | >();
121 |
122 | private status$ = routerState$({ select: (s) => s.status });
123 | private matchRouteFn = matchRoute$();
124 | private params$ = toObservable(this.matchRoute).pipe(
125 | switchMap((matchRoute) => this.matchRouteFn(matchRoute as any))
126 | );
127 |
128 | private vcr = inject(ViewContainerRef);
129 | private templateRef = inject(TemplateRef);
130 |
131 | private ref?: EmbeddedViewRef;
132 |
133 | constructor() {
134 | let subscription: Subscription;
135 | afterNextRender(() => {
136 | subscription = combineLatest([this.params$, this.status$]).subscribe(
137 | ([params]) => {
138 | if (this.ref) {
139 | this.ref.destroy();
140 | }
141 |
142 | this.ref = this.vcr.createEmbeddedView(this.templateRef, {
143 | match: params,
144 | });
145 | this.ref.markForCheck();
146 | }
147 | );
148 | });
149 |
150 | inject(DestroyRef).onDestroy(() => {
151 | subscription?.unsubscribe();
152 | this.vcr.clear();
153 | this.ref?.destroy();
154 | });
155 | }
156 |
157 | static ngTemplateContextGuard<
158 | TRouter extends AnyRouter = RegisteredRouter,
159 | const TFrom extends string = string,
160 | const TTo extends string | undefined = undefined,
161 | const TMaskFrom extends string = TFrom,
162 | const TMaskTo extends string = '',
163 | >(
164 | _: MatchRoute,
165 | ctx: unknown
166 | ): ctx is { match: boolean } {
167 | return true;
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/projects/router/src/lib/match.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertInInjectionContext,
3 | computed,
4 | inject,
5 | Injector,
6 | runInInjectionContext,
7 | Signal,
8 | } from '@angular/core';
9 | import { toObservable, toSignal } from '@angular/core/rxjs-interop';
10 | import {
11 | AnyRouter,
12 | MakeRouteMatch,
13 | MakeRouteMatchUnion,
14 | RegisteredRouter,
15 | StrictOrFrom,
16 | ThrowConstraint,
17 | ThrowOrOptional,
18 | } from '@tanstack/router-core';
19 | import { combineLatest, map, Observable } from 'rxjs';
20 | import invariant from 'tiny-invariant';
21 | import { MATCH_ID } from './outlet';
22 | import { routerState$ } from './router-state';
23 |
24 | export interface MatchBaseOptions<
25 | TRouter extends AnyRouter,
26 | TFrom,
27 | TStrict extends boolean,
28 | TThrow extends boolean,
29 | TSelected,
30 | > {
31 | select?: (
32 | match: MakeRouteMatch
33 | ) => TSelected;
34 | shouldThrow?: TThrow;
35 | injector?: Injector;
36 | }
37 |
38 | export type MatchRoute = <
39 | TRouter extends AnyRouter = RegisteredRouter,
40 | TSelected = unknown,
41 | >(
42 | opts?: MatchBaseOptions
43 | ) => TObservable extends true
44 | ? Observable>
45 | : Signal>;
46 |
47 | export type MatchOptions<
48 | TRouter extends AnyRouter,
49 | TFrom extends string | undefined,
50 | TStrict extends boolean,
51 | TThrow extends boolean,
52 | TSelected,
53 | > = StrictOrFrom &
54 | MatchBaseOptions;
55 |
56 | export type MatchResult<
57 | TRouter extends AnyRouter,
58 | TFrom,
59 | TStrict extends boolean,
60 | TSelected,
61 | > = unknown extends TSelected
62 | ? TStrict extends true
63 | ? MakeRouteMatch
64 | : MakeRouteMatchUnion
65 | : TSelected;
66 |
67 | export function match$<
68 | TRouter extends AnyRouter = RegisteredRouter,
69 | const TFrom extends string | undefined = undefined,
70 | TStrict extends boolean = true,
71 | TThrow extends boolean = true,
72 | TSelected = unknown,
73 | >({
74 | injector,
75 | ...opts
76 | }: MatchOptions<
77 | TRouter,
78 | TFrom,
79 | TStrict,
80 | ThrowConstraint,
81 | TSelected
82 | >): Observable<
83 | ThrowOrOptional, TThrow>
84 | > {
85 | !injector && assertInInjectionContext(match$);
86 |
87 | if (!injector) {
88 | injector = inject(Injector);
89 | }
90 |
91 | return runInInjectionContext(injector, () => {
92 | const closestMatchId = inject(MATCH_ID, { optional: true });
93 | const nearestMatchId = computed(() => {
94 | if (opts.from) return null;
95 | return closestMatchId;
96 | });
97 |
98 | return combineLatest([
99 | routerState$({ select: (s) => s.matches, injector }),
100 | toObservable(nearestMatchId),
101 | ]).pipe(
102 | map(([matches, matchId]) => {
103 | const match = matches.find((d) => {
104 | return opts.from ? opts.from === d.routeId : d.id === matchId;
105 | });
106 | invariant(
107 | !((opts.shouldThrow ?? true) && !match),
108 | `Could not find ${opts.from ? `an active match from "${opts.from}"` : 'a nearest match!'}`
109 | );
110 | if (match === undefined) {
111 | return undefined;
112 | }
113 |
114 | return opts.select ? opts.select(match) : match;
115 | })
116 | ) as any;
117 | });
118 | }
119 |
120 | export function match<
121 | TRouter extends AnyRouter = RegisteredRouter,
122 | const TFrom extends string | undefined = undefined,
123 | TStrict extends boolean = true,
124 | TThrow extends boolean = true,
125 | TSelected = unknown,
126 | >({
127 | injector,
128 | ...opts
129 | }: MatchOptions<
130 | TRouter,
131 | TFrom,
132 | TStrict,
133 | ThrowConstraint,
134 | TSelected
135 | >): Signal<
136 | ThrowOrOptional, TThrow>
137 | > {
138 | !injector && assertInInjectionContext(match);
139 |
140 | if (!injector) {
141 | injector = inject(Injector);
142 | }
143 |
144 | return runInInjectionContext(injector, () => {
145 | return toSignal(match$({ injector, ...opts } as any), { injector });
146 | }) as any;
147 | }
148 |
--------------------------------------------------------------------------------
/projects/router/src/lib/matches.ts:
--------------------------------------------------------------------------------
1 | import {
2 | afterNextRender,
3 | assertInInjectionContext,
4 | ComponentRef,
5 | DestroyRef,
6 | Directive,
7 | inject,
8 | Injector,
9 | runInInjectionContext,
10 | Signal,
11 | ViewContainerRef,
12 | } from '@angular/core';
13 | import { toSignal } from '@angular/core/rxjs-interop';
14 | import {
15 | AnyRouter,
16 | MakeRouteMatchUnion,
17 | RegisteredRouter,
18 | RouterState,
19 | } from '@tanstack/router-core';
20 | import {
21 | combineLatest,
22 | map,
23 | Observable,
24 | of,
25 | Subscription,
26 | switchMap,
27 | } from 'rxjs';
28 | import { DefaultError } from './default-error';
29 | import { distinctUntilRefChanged } from './distinct-until-ref-changed';
30 | import { MATCH_ID, RouteMatch } from './outlet';
31 | import { ERROR_COMPONENT_CONTEXT } from './route';
32 | import { injectRouter } from './router';
33 | import { routerState$ } from './router-state';
34 | import { Transitioner } from './transitioner';
35 |
36 | @Directive({ hostDirectives: [Transitioner] })
37 | export class Matches {
38 | private router = injectRouter();
39 | private injector = inject(Injector);
40 | private vcr = inject(ViewContainerRef);
41 |
42 | private defaultPendingComponent =
43 | this.router.options.defaultPendingComponent?.();
44 |
45 | private resetKey$ = routerState$({ select: (s) => s.loadedAt.toString() });
46 | private rootMatchId$ = routerState$({ select: (s) => s.matches[0]?.id });
47 |
48 | private matchLoad$ = this.rootMatchId$.pipe(
49 | switchMap((rootMatchId) => {
50 | if (!rootMatchId) return of({ pending: false });
51 | const loadPromise = this.router.getMatch(rootMatchId)?.loadPromise;
52 | if (!loadPromise) return of({ pending: false });
53 | return of({ pending: true }).pipe(
54 | switchMap(() => loadPromise.then(() => ({ pending: false })))
55 | );
56 | })
57 | );
58 |
59 | private cmpRef?: ComponentRef;
60 |
61 | private run$ = this.matchLoad$.pipe(
62 | switchMap(({ pending }) => {
63 | if (pending) {
64 | if (this.defaultPendingComponent) {
65 | return of({
66 | component: this.defaultPendingComponent,
67 | clearView: true,
68 | matchId: null,
69 | } as const);
70 | }
71 | return of(null);
72 | }
73 |
74 | return combineLatest([this.rootMatchId$, this.resetKey$]).pipe(
75 | map(([matchId]) => {
76 | if (!matchId) return null;
77 | if (this.cmpRef) return { clearView: false } as const;
78 | return {
79 | component: RouteMatch,
80 | matchId,
81 | clearView: true,
82 | } as const;
83 | })
84 | );
85 | })
86 | );
87 |
88 | constructor() {
89 | let subscription: Subscription;
90 | afterNextRender(() => {
91 | subscription = this.run$.subscribe({
92 | next: (runData) => {
93 | if (!runData) return;
94 | if (!runData.clearView) {
95 | this.cmpRef?.changeDetectorRef.markForCheck();
96 | return;
97 | }
98 | const { component, matchId } = runData;
99 | this.vcr.clear();
100 | this.cmpRef = this.vcr.createComponent(component);
101 | if (matchId) {
102 | this.cmpRef.setInput('matchId', matchId);
103 | }
104 | this.cmpRef.changeDetectorRef.markForCheck();
105 | },
106 | error: (error) => {
107 | console.error(error);
108 | const injector = Injector.create({
109 | providers: [
110 | {
111 | provide: ERROR_COMPONENT_CONTEXT,
112 | useValue: {
113 | error: error,
114 | info: { componentStack: '' },
115 | reset: () => void this.router.invalidate(),
116 | },
117 | },
118 | ],
119 | parent: this.injector,
120 | });
121 | this.vcr.clear();
122 | const ref = this.vcr.createComponent(DefaultError, { injector });
123 | ref.changeDetectorRef.markForCheck();
124 | this.cmpRef = undefined;
125 | },
126 | });
127 | });
128 |
129 | inject(DestroyRef).onDestroy(() => {
130 | subscription?.unsubscribe();
131 | this.vcr.clear();
132 | this.cmpRef = undefined;
133 | });
134 | }
135 | }
136 |
137 | export interface MatchesBaseOptions {
138 | select?: (matches: Array>) => TSelected;
139 | injector?: Injector;
140 | }
141 |
142 | export type MatchesResult<
143 | TRouter extends AnyRouter,
144 | TSelected,
145 | > = unknown extends TSelected ? Array> : TSelected;
146 |
147 | export function matches$<
148 | TRouter extends AnyRouter = RegisteredRouter,
149 | TSelected = unknown,
150 | >({
151 | injector,
152 | ...opts
153 | }: MatchesBaseOptions = {}): Observable<
154 | MatchesResult
155 | > {
156 | !injector && assertInInjectionContext(matches$);
157 |
158 | if (!injector) {
159 | injector = inject(Injector);
160 | }
161 |
162 | return runInInjectionContext(injector, () => {
163 | return routerState$({
164 | injector,
165 | select: (state: RouterState) => {
166 | const matches = state.matches;
167 | return opts.select
168 | ? opts.select(matches as Array>)
169 | : matches;
170 | },
171 | }) as Observable>;
172 | });
173 | }
174 |
175 | export function matches<
176 | TRouter extends AnyRouter = RegisteredRouter,
177 | TSelected = unknown,
178 | >({ injector, ...opts }: MatchesBaseOptions = {}): Signal<
179 | MatchesResult
180 | > {
181 | !injector && assertInInjectionContext(matches);
182 |
183 | if (!injector) {
184 | injector = inject(Injector);
185 | }
186 |
187 | return runInInjectionContext(injector, () => {
188 | return toSignal(matches$({ injector, ...opts })) as Signal<
189 | MatchesResult
190 | >;
191 | });
192 | }
193 |
194 | export function parentMatches$<
195 | TRouter extends AnyRouter = RegisteredRouter,
196 | TSelected = unknown,
197 | >({
198 | injector,
199 | ...opts
200 | }: MatchesBaseOptions = {}): Observable<
201 | MatchesResult
202 | > {
203 | !injector && assertInInjectionContext(parentMatches$);
204 |
205 | if (!injector) {
206 | injector = inject(Injector);
207 | }
208 |
209 | return runInInjectionContext(injector, () => {
210 | const closestMatch = inject(MATCH_ID);
211 | return routerState$({ injector, select: (s) => s.matches }).pipe(
212 | map((matches) => {
213 | const sliced = matches.slice(
214 | 0,
215 | matches.findIndex((d) => d.id === closestMatch)
216 | );
217 | return opts.select
218 | ? opts.select(sliced as Array>)
219 | : sliced;
220 | }),
221 | distinctUntilRefChanged() as any
222 | ) as Observable>;
223 | });
224 | }
225 |
226 | export function parentMatches<
227 | TRouter extends AnyRouter = RegisteredRouter,
228 | TSelected = unknown,
229 | >({ injector, ...opts }: MatchesBaseOptions = {}): Signal<
230 | MatchesResult
231 | > {
232 | !injector && assertInInjectionContext(parentMatches);
233 |
234 | if (!injector) {
235 | injector = inject(Injector);
236 | }
237 |
238 | return runInInjectionContext(injector, () => {
239 | return toSignal(parentMatches$({ injector, ...opts })) as Signal<
240 | MatchesResult
241 | >;
242 | });
243 | }
244 | export function childMatches$<
245 | TRouter extends AnyRouter = RegisteredRouter,
246 | TSelected = unknown,
247 | >({
248 | injector,
249 | ...opts
250 | }: MatchesBaseOptions = {}): Observable<
251 | MatchesResult
252 | > {
253 | !injector && assertInInjectionContext(childMatches$);
254 |
255 | if (!injector) {
256 | injector = inject(Injector);
257 | }
258 |
259 | return runInInjectionContext(injector, () => {
260 | const closestMatch = inject(MATCH_ID);
261 | return routerState$({ injector, select: (s) => s.matches }).pipe(
262 | map((matches) => {
263 | const sliced = matches.slice(
264 | matches.findIndex((d) => d.id === closestMatch) + 1
265 | );
266 | return opts.select
267 | ? opts.select(sliced as Array>)
268 | : sliced;
269 | }),
270 | distinctUntilRefChanged() as any
271 | ) as Observable>;
272 | });
273 | }
274 |
275 | export function childMatches<
276 | TRouter extends AnyRouter = RegisteredRouter,
277 | TSelected = unknown,
278 | >({ injector, ...opts }: MatchesBaseOptions = {}): Signal<
279 | MatchesResult
280 | > {
281 | !injector && assertInInjectionContext(childMatches);
282 |
283 | if (!injector) {
284 | injector = inject(Injector);
285 | }
286 |
287 | return runInInjectionContext(injector, () => {
288 | return toSignal(childMatches$({ injector, ...opts })) as Signal<
289 | MatchesResult
290 | >;
291 | });
292 | }
293 |
--------------------------------------------------------------------------------
/projects/router/src/lib/outlet.ts:
--------------------------------------------------------------------------------
1 | import {
2 | afterNextRender,
3 | ChangeDetectionStrategy,
4 | Component,
5 | ComponentRef,
6 | DestroyRef,
7 | Directive,
8 | EnvironmentInjector,
9 | inject,
10 | InjectionToken,
11 | Injector,
12 | input,
13 | Type,
14 | ViewContainerRef,
15 | } from '@angular/core';
16 | import { toObservable } from '@angular/core/rxjs-interop';
17 | import {
18 | createControlledPromise,
19 | getLocationChangeInfo,
20 | isNotFound,
21 | isRedirect,
22 | pick,
23 | rootRouteId,
24 | } from '@tanstack/router-core';
25 | import {
26 | catchError,
27 | combineLatest,
28 | distinctUntilChanged,
29 | filter,
30 | map,
31 | of,
32 | Subscription,
33 | switchMap,
34 | throwError,
35 | withLatestFrom,
36 | } from 'rxjs';
37 | import invariant from 'tiny-invariant';
38 | import warning from 'tiny-warning';
39 | import { DefaultError } from './default-error';
40 | import { DefaultNotFound } from './default-not-found';
41 | import { distinctUntilRefChanged } from './distinct-until-ref-changed';
42 | import { isDevMode } from './is-dev-mode';
43 | import { ERROR_COMPONENT_CONTEXT, NOT_FOUND_COMPONENT_CONTEXT } from './route';
44 | import { injectRouter } from './router';
45 | import { routerState$ } from './router-state';
46 |
47 | @Directive()
48 | export class OnRendered {
49 | private match = inject(RouteMatch);
50 | private router = injectRouter();
51 |
52 | private parentRouteId$ = combineLatest([
53 | this.match.matchId$,
54 | routerState$({ select: (s) => s.matches }),
55 | ]).pipe(
56 | map(
57 | ([matchId, matches]) =>
58 | matches.find((d) => d.id === matchId)?.routeId as string
59 | ),
60 | distinctUntilRefChanged()
61 | );
62 | private location$ = routerState$({
63 | select: (s) => s.resolvedLocation?.state.key,
64 | });
65 |
66 | constructor() {
67 | let subscription: Subscription;
68 | afterNextRender(() => {
69 | subscription = combineLatest([
70 | this.parentRouteId$,
71 | this.location$,
72 | ]).subscribe(([parentRouteId]) => {
73 | if (!parentRouteId || parentRouteId !== rootRouteId) return;
74 | this.router.emit({
75 | type: 'onRendered',
76 | ...getLocationChangeInfo(this.router.state),
77 | });
78 | });
79 | });
80 |
81 | inject(DestroyRef).onDestroy(() => {
82 | subscription?.unsubscribe();
83 | });
84 | }
85 | }
86 |
87 | export const MATCH_ID = new InjectionToken('MATCH_ID');
88 | export const MATCH_IDS = new InjectionToken('MATCH_IDS');
89 |
90 | @Component({
91 | selector: 'route-match,RouteMatch',
92 | template: ``,
93 | hostDirectives: [OnRendered],
94 | changeDetection: ChangeDetectionStrategy.OnPush,
95 | host: {
96 | '[attr.data-matchId]': 'matchId()',
97 | },
98 | })
99 | export class RouteMatch {
100 | matchId = input.required();
101 |
102 | private isDevMode = isDevMode();
103 | private router = injectRouter();
104 | private vcr = inject(ViewContainerRef);
105 | private injector = inject(Injector);
106 | private environmentInjector = inject(EnvironmentInjector);
107 |
108 | matchId$ = toObservable(this.matchId);
109 | private resetKey$ = routerState$({ select: (s) => s.loadedAt.toString() });
110 | private matches$ = routerState$({ select: (s) => s.matches });
111 | private routeId$ = combineLatest([this.matchId$, this.matches$]).pipe(
112 | map(
113 | ([matchId, matches]) =>
114 | matches.find((d) => d.id === matchId)?.routeId as string
115 | ),
116 | distinctUntilRefChanged()
117 | );
118 |
119 | private route$ = this.routeId$.pipe(
120 | map((routeId) => this.router.routesById[routeId]),
121 | distinctUntilRefChanged()
122 | );
123 | private pendingComponent$ = this.route$.pipe(
124 | map(
125 | (route) =>
126 | route.options.pendingComponent ||
127 | this.router.options.defaultPendingComponent
128 | ),
129 | distinctUntilRefChanged()
130 | );
131 | private errorComponent$ = this.route$.pipe(
132 | map(
133 | (route) =>
134 | route.options.errorComponent ||
135 | this.router.options.defaultErrorComponent
136 | ),
137 | distinctUntilRefChanged()
138 | );
139 | private onCatch$ = this.route$.pipe(
140 | map((route) => route.options.onCatch || this.router.options.defaultOnCatch),
141 | distinctUntilRefChanged()
142 | );
143 |
144 | private matchIndex$ = combineLatest([this.matchId$, this.matches$]).pipe(
145 | map(([matchId, matches]) => matches.findIndex((d) => d.id === matchId)),
146 | distinctUntilRefChanged()
147 | );
148 | private matchState$ = combineLatest([this.matchIndex$, this.matches$]).pipe(
149 | map(([matchIndex, matches]) => matches[matchIndex]),
150 | filter((match) => !!match),
151 | map((match) => ({
152 | routeId: match.routeId as string,
153 | match: pick(match, ['id', 'status', 'error']),
154 | }))
155 | );
156 |
157 | private matchRoute$ = this.matchState$.pipe(
158 | map(({ routeId }) => this.router.routesById[routeId]),
159 | distinctUntilRefChanged()
160 | );
161 | private match$ = this.matchState$.pipe(
162 | map(({ match }) => match),
163 | distinctUntilChanged(
164 | (a, b) => !!a && !!b && a.id === b.id && a.status === b.status
165 | )
166 | );
167 | private matchLoad$ = this.match$.pipe(
168 | withLatestFrom(this.matchRoute$),
169 | switchMap(([match, matchRoute]) => {
170 | const loadPromise = this.router.getMatch(match.id)?.loadPromise;
171 | if (!loadPromise) return Promise.resolve() as any;
172 |
173 | if (match.status === 'pending') {
174 | const pendingMinMs =
175 | matchRoute.options.pendingMinMs ??
176 | this.router.options.defaultPendingMinMs;
177 | let minPendingPromise = this.router.getMatch(
178 | match.id
179 | )?.minPendingPromise;
180 |
181 | if (pendingMinMs && !minPendingPromise) {
182 | // Create a promise that will resolve after the minPendingMs
183 | if (!this.router.isServer) {
184 | minPendingPromise = createControlledPromise();
185 | Promise.resolve().then(() => {
186 | this.router.updateMatch(match.id, (prev) => ({
187 | ...prev,
188 | minPendingPromise,
189 | }));
190 | });
191 |
192 | setTimeout(() => {
193 | minPendingPromise?.resolve();
194 | // We've handled the minPendingPromise, so we can delete it
195 | this.router.updateMatch(match.id, (prev) => ({
196 | ...prev,
197 | minPendingPromise: undefined,
198 | }));
199 | }, pendingMinMs);
200 | }
201 | }
202 |
203 | return minPendingPromise?.then(() => loadPromise) || loadPromise;
204 | }
205 |
206 | return loadPromise;
207 | }),
208 | distinctUntilRefChanged()
209 | );
210 |
211 | private run$ = this.routeId$.pipe(
212 | switchMap((routeId) => {
213 | invariant(
214 | routeId,
215 | `Could not find routeId for matchId "${this.matchId()}". Please file an issue!`
216 | );
217 | return combineLatest([
218 | this.match$,
219 | this.matchRoute$,
220 | this.resetKey$,
221 | ]).pipe(
222 | switchMap(([match, route]) => {
223 | if (match.status === 'notFound') {
224 | invariant(isNotFound(match.error), 'Expected a notFound error');
225 | let notFoundCmp: Type | undefined;
226 | if (!route.options.notFoundComponent) {
227 | notFoundCmp = this.router.options.defaultNotFoundComponent?.();
228 | if (!notFoundCmp) {
229 | if (this.isDevMode) {
230 | warning(
231 | route.options.notFoundComponent,
232 | `A notFoundError was encountered on the route with ID "${route.id}", but a notFoundComponent option was not configured, nor was a router level defaultNotFoundComponent configured. Consider configuring at least one of these to avoid TanStack Router's overly generic defaultNotFoundComponent (Page not found
)`
233 | );
234 | }
235 | notFoundCmp = DefaultNotFound;
236 | }
237 | } else {
238 | notFoundCmp = route.options.notFoundComponent?.();
239 | }
240 |
241 | if (!notFoundCmp) return of(null);
242 |
243 | const injector = this.router.getRouteInjector(
244 | route.id + '-not-found',
245 | this.injector,
246 | [{ provide: NOT_FOUND_COMPONENT_CONTEXT, useValue: {} }]
247 | );
248 | return of({
249 | component: notFoundCmp,
250 | injector,
251 | environmentInjector: null,
252 | clearView: true,
253 | } as const);
254 | }
255 |
256 | if (match.status === 'redirected' || match.status === 'pending') {
257 | if (match.status === 'redirected') {
258 | invariant(isRedirect(match.error), 'Expected a redirect error');
259 | }
260 |
261 | return this.matchLoad$.pipe(
262 | withLatestFrom(this.pendingComponent$),
263 | switchMap(([, pendingComponent]) => {
264 | const pendingCmp = pendingComponent?.();
265 | if (!pendingCmp) return of(null);
266 | return of({
267 | component: pendingCmp,
268 | injector: null,
269 | environmentInjector: null,
270 | clearView: true,
271 | } as const);
272 | })
273 | );
274 | }
275 |
276 | if (match.status === 'error') {
277 | return of(null).pipe(
278 | withLatestFrom(this.errorComponent$),
279 | switchMap(([, errorComponent]) => {
280 | const errorCmp = errorComponent?.() || DefaultError;
281 | const injector = this.router.getRouteInjector(
282 | route.id + '-error',
283 | this.injector,
284 | [
285 | {
286 | provide: ERROR_COMPONENT_CONTEXT,
287 | useValue: {
288 | error: match.error,
289 | info: { componentStack: '' },
290 | reset: () => void this.router.invalidate(),
291 | },
292 | },
293 | ]
294 | );
295 | return of({
296 | component: errorCmp,
297 | injector,
298 | environmentInjector: null,
299 | clearView: true,
300 | } as const);
301 | })
302 | );
303 | }
304 |
305 | if (match.status === 'success') {
306 | const successComponent = route.options.component?.() || Outlet;
307 |
308 | if (this.cmp === successComponent) {
309 | this.cmpRef?.changeDetectorRef.markForCheck();
310 | return of({ clearView: false } as const);
311 | }
312 |
313 | this.cmpRef = undefined;
314 | this.cmp = successComponent;
315 | const injector = this.router.getRouteInjector(
316 | route.id,
317 | this.injector
318 | );
319 | const environmentInjector = this.router.getRouteEnvInjector(
320 | route.id,
321 | this.environmentInjector,
322 | route.options.providers || [],
323 | this.router
324 | );
325 |
326 | return of({
327 | component: successComponent,
328 | injector: Injector.create({
329 | providers: [
330 | { provide: MATCH_ID, useValue: match.id },
331 | {
332 | provide: MATCH_IDS,
333 | useValue: [match.id, this.matchId(), route.id],
334 | },
335 | ],
336 | parent: injector,
337 | }),
338 | environmentInjector,
339 | clearView: true,
340 | } as const);
341 | }
342 |
343 | return of(null);
344 | })
345 | );
346 | }),
347 | catchError((error) =>
348 | of(null).pipe(
349 | withLatestFrom(this.onCatch$),
350 | switchMap(([, onCatch]) => throwError(() => [error, onCatch]))
351 | )
352 | )
353 | );
354 |
355 | private cmp?: Type;
356 | private cmpRef?: ComponentRef;
357 |
358 | constructor() {
359 | let subscription: Subscription;
360 |
361 | afterNextRender(() => {
362 | subscription = this.run$.subscribe({
363 | next: (runData) => {
364 | if (!runData) return;
365 | if (!runData.clearView) {
366 | this.cmpRef?.changeDetectorRef.markForCheck();
367 | return;
368 | }
369 | const { component, injector, environmentInjector } = runData;
370 | this.vcr.clear();
371 |
372 | this.cmpRef = this.vcr.createComponent(component, {
373 | injector: injector || undefined,
374 | environmentInjector: environmentInjector || undefined,
375 | });
376 | this.cmpRef.changeDetectorRef.markForCheck();
377 | },
378 | error: (error) => {
379 | if (Array.isArray(error)) {
380 | const [errorToThrow, onCatch] = error;
381 | if (onCatch) onCatch(errorToThrow);
382 | console.error(errorToThrow);
383 | return;
384 | }
385 | console.error(error);
386 | },
387 | });
388 | });
389 |
390 | inject(DestroyRef).onDestroy(() => {
391 | subscription?.unsubscribe();
392 | this.vcr.clear();
393 | this.cmp = undefined;
394 | this.cmpRef = undefined;
395 | });
396 | }
397 | }
398 |
399 | @Component({
400 | selector: 'outlet,Outlet',
401 | template: ``,
402 | changeDetection: ChangeDetectionStrategy.OnPush,
403 | })
404 | export class Outlet {
405 | private matchId = inject(MATCH_ID);
406 | private matchIds = inject(MATCH_IDS);
407 | private router = injectRouter();
408 | private vcr = inject(ViewContainerRef);
409 | private isDevMode = isDevMode();
410 |
411 | protected readonly defaultPendingComponent =
412 | this.router.options.defaultPendingComponent?.();
413 |
414 | private matches$ = routerState$({ select: (s) => s.matches });
415 | private routeId$ = this.matches$.pipe(
416 | map(
417 | (matches) => matches.find((d) => d.id === this.matchId)?.routeId as string
418 | ),
419 | distinctUntilRefChanged()
420 | );
421 | private route$ = this.routeId$.pipe(
422 | map((routeId) => this.router.routesById[routeId]),
423 | distinctUntilRefChanged()
424 | );
425 | private parentGlobalNotFound$ = this.matches$.pipe(
426 | map((matches) => {
427 | console.log(this.vcr.element.nativeElement, { matchIds: this.matchIds });
428 | const parentMatch = matches.find((d) => d.id === this.matchId);
429 | if (!parentMatch) {
430 | warning(
431 | false,
432 | `Could not find parent match for matchId "${this.matchId}". Please file an issue!`
433 | );
434 | return false;
435 | }
436 | return parentMatch.globalNotFound;
437 | })
438 | );
439 |
440 | private childMatchId$ = this.matches$.pipe(
441 | map((matches) => {
442 | const index = matches.findIndex((d) => d.id === this.matchId);
443 | if (index === -1) return null;
444 | return matches[index + 1]?.id;
445 | }),
446 | distinctUntilRefChanged()
447 | );
448 | private matchLoad$ = this.childMatchId$.pipe(
449 | switchMap((childMatchId) => {
450 | if (!childMatchId) return Promise.resolve() as any;
451 | const loadPromise = this.router.getMatch(childMatchId)?.loadPromise;
452 | if (!loadPromise) return Promise.resolve() as any;
453 | return loadPromise;
454 | })
455 | );
456 |
457 | private renderedId?: string;
458 | private cmpRef?: ComponentRef;
459 |
460 | private run$ = combineLatest([
461 | this.parentGlobalNotFound$,
462 | this.childMatchId$,
463 | ]).pipe(
464 | switchMap(([parentGlobalNotFound, childMatchId]) => {
465 | if (parentGlobalNotFound) {
466 | return this.route$.pipe(
467 | map((route) => {
468 | let notFoundCmp: Type | undefined = undefined;
469 |
470 | if (!route.options.notFoundComponent) {
471 | notFoundCmp = this.router.options.defaultNotFoundComponent?.();
472 | if (!notFoundCmp) {
473 | if (this.isDevMode) {
474 | warning(
475 | route.options.notFoundComponent,
476 | `A notFoundError was encountered on the route with ID "${route.id}", but a notFoundComponent option was not configured, nor was a router level defaultNotFoundComponent configured. Consider configuring at least one of these to avoid TanStack Router's overly generic defaultNotFoundComponent (Page not found
)`
477 | );
478 | }
479 | notFoundCmp = DefaultNotFound;
480 | }
481 | } else {
482 | notFoundCmp = route.options.notFoundComponent?.();
483 | }
484 |
485 | if (!notFoundCmp) return null;
486 |
487 | const injector = this.router.getRouteInjector(
488 | route.id + '-not-found',
489 | this.vcr.injector,
490 | [{ provide: NOT_FOUND_COMPONENT_CONTEXT, useValue: {} }]
491 | );
492 | return {
493 | component: notFoundCmp,
494 | injector,
495 | clearView: true,
496 | childMatchId: null,
497 | } as const;
498 | })
499 | );
500 | }
501 |
502 | if (!childMatchId) return of(null);
503 |
504 | if (this.renderedId === childMatchId) {
505 | return of({ clearView: false } as const);
506 | }
507 |
508 | this.cmpRef = undefined;
509 |
510 | if (childMatchId === rootRouteId) {
511 | return this.matchLoad$.pipe(
512 | map(() => {
513 | return {
514 | component: this.defaultPendingComponent,
515 | injector: null,
516 | clearView: true,
517 | childMatchId: null,
518 | } as const;
519 | })
520 | );
521 | }
522 |
523 | this.renderedId = childMatchId;
524 | return of({
525 | component: RouteMatch,
526 | injector: null,
527 | clearView: true,
528 | childMatchId,
529 | } as const);
530 | }),
531 | catchError((error) => throwError(() => error))
532 | );
533 |
534 | constructor() {
535 | let subscription: Subscription;
536 | afterNextRender(() => {
537 | subscription = this.run$.subscribe({
538 | next: (runData) => {
539 | if (!runData) return;
540 | if (!runData.clearView) {
541 | this.cmpRef?.changeDetectorRef.markForCheck();
542 | return;
543 | }
544 | const { component, injector, childMatchId } = runData;
545 | this.vcr.clear();
546 | if (!component) return;
547 | this.cmpRef = this.vcr.createComponent(component, {
548 | injector: injector || undefined,
549 | });
550 | if (childMatchId) {
551 | this.cmpRef.setInput('matchId', childMatchId);
552 | }
553 | this.cmpRef.changeDetectorRef.markForCheck();
554 | },
555 | error: (error) => {
556 | console.error(error);
557 | },
558 | });
559 | });
560 |
561 | inject(DestroyRef).onDestroy(() => {
562 | subscription?.unsubscribe();
563 | this.vcr.clear();
564 | this.cmpRef = undefined;
565 | this.renderedId = undefined;
566 | });
567 | }
568 | }
569 |
--------------------------------------------------------------------------------
/projects/router/src/lib/params.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertInInjectionContext,
3 | inject,
4 | Injector,
5 | runInInjectionContext,
6 | Signal,
7 | } from '@angular/core';
8 | import { toSignal } from '@angular/core/rxjs-interop';
9 | import {
10 | AnyRouter,
11 | RegisteredRouter,
12 | ResolveUseParams,
13 | StrictOrFrom,
14 | ThrowConstraint,
15 | ThrowOrOptional,
16 | UseParamsResult,
17 | } from '@tanstack/router-core';
18 | import { Observable } from 'rxjs';
19 | import { match$ } from './match';
20 |
21 | export interface ParamsBaseOptions<
22 | TRouter extends AnyRouter,
23 | TFrom,
24 | TStrict extends boolean,
25 | TThrow extends boolean,
26 | TSelected,
27 | > {
28 | select?: (params: ResolveUseParams) => TSelected;
29 | shouldThrow?: TThrow;
30 | injector?: Injector;
31 | }
32 |
33 | export type ParamsOptions<
34 | TRouter extends AnyRouter,
35 | TFrom extends string | undefined,
36 | TStrict extends boolean,
37 | TThrow extends boolean,
38 | TSelected,
39 | > = StrictOrFrom &
40 | ParamsBaseOptions;
41 |
42 | export type ParamsRoute = <
43 | TRouter extends AnyRouter = RegisteredRouter,
44 | TSelected = unknown,
45 | >(
46 | opts?: ParamsBaseOptions<
47 | TRouter,
48 | TFrom,
49 | /* TStrict */ true,
50 | /* TThrow */ true,
51 | TSelected
52 | >
53 | ) => TObservable extends true
54 | ? Observable>
55 | : Signal>;
56 |
57 | export function params$<
58 | TRouter extends AnyRouter = RegisteredRouter,
59 | const TFrom extends string | undefined = undefined,
60 | TStrict extends boolean = true,
61 | TThrow extends boolean = true,
62 | TSelected = unknown,
63 | >({
64 | injector,
65 | ...opts
66 | }: ParamsOptions<
67 | TRouter,
68 | TFrom,
69 | TStrict,
70 | ThrowConstraint,
71 | TSelected
72 | >): Observable<
73 | ThrowOrOptional, TThrow>
74 | > {
75 | !injector && assertInInjectionContext(params);
76 |
77 | if (!injector) {
78 | injector = inject(Injector);
79 | }
80 |
81 | return runInInjectionContext(injector, () => {
82 | return match$({
83 | from: opts.from!,
84 | strict: opts.strict,
85 | shouldThrow: opts.shouldThrow,
86 | select: (match) => {
87 | return opts.select ? opts.select(match.params) : match.params;
88 | },
89 | }) as any;
90 | });
91 | }
92 |
93 | export function params<
94 | TRouter extends AnyRouter = RegisteredRouter,
95 | const TFrom extends string | undefined = undefined,
96 | TStrict extends boolean = true,
97 | TThrow extends boolean = true,
98 | TSelected = unknown,
99 | >({
100 | injector,
101 | ...opts
102 | }: ParamsOptions<
103 | TRouter,
104 | TFrom,
105 | TStrict,
106 | ThrowConstraint,
107 | TSelected
108 | >): Signal<
109 | ThrowOrOptional, TThrow>
110 | > {
111 | !injector && assertInInjectionContext(params);
112 |
113 | if (!injector) {
114 | injector = inject(Injector);
115 | }
116 |
117 | return runInInjectionContext(injector, () => {
118 | return toSignal(params$({ injector, ...opts } as any)) as any;
119 | });
120 | }
121 |
--------------------------------------------------------------------------------
/projects/router/src/lib/route-context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertInInjectionContext,
3 | inject,
4 | Injector,
5 | runInInjectionContext,
6 | Signal,
7 | } from '@angular/core';
8 | import { toSignal } from '@angular/core/rxjs-interop';
9 | import {
10 | AnyRouter,
11 | RegisteredRouter,
12 | UseRouteContextBaseOptions,
13 | UseRouteContextOptions,
14 | UseRouteContextResult,
15 | } from '@tanstack/router-core';
16 | import { Observable } from 'rxjs';
17 | import { match$ } from './match';
18 |
19 | export type RouteContextRoute = <
20 | TRouter extends AnyRouter = RegisteredRouter,
21 | TSelected = unknown,
22 | >(
23 | opts?: UseRouteContextBaseOptions & {
24 | injector?: Injector;
25 | }
26 | ) => TObservable extends true
27 | ? Observable>
28 | : Signal>;
29 |
30 | export function routeContext$<
31 | TRouter extends AnyRouter = RegisteredRouter,
32 | const TFrom extends string | undefined = undefined,
33 | TStrict extends boolean = true,
34 | TSelected = unknown,
35 | >({
36 | injector,
37 | ...opts
38 | }: UseRouteContextOptions & {
39 | injector?: Injector;
40 | }): Observable> {
41 | !injector && assertInInjectionContext(routeContext);
42 |
43 | if (!injector) {
44 | injector = inject(Injector);
45 | }
46 |
47 | return runInInjectionContext(injector, () => {
48 | return match$({
49 | ...(opts as any),
50 | select: (match) => {
51 | return opts.select ? opts.select(match.context) : match.context;
52 | },
53 | }) as any;
54 | });
55 | }
56 |
57 | export function routeContext<
58 | TRouter extends AnyRouter = RegisteredRouter,
59 | const TFrom extends string | undefined = undefined,
60 | TStrict extends boolean = true,
61 | TSelected = unknown,
62 | >({
63 | injector,
64 | ...opts
65 | }: UseRouteContextOptions & {
66 | injector?: Injector;
67 | }): Signal> {
68 | !injector && assertInInjectionContext(routeContext);
69 |
70 | if (!injector) {
71 | injector = inject(Injector);
72 | }
73 |
74 | return runInInjectionContext(injector, () => {
75 | return toSignal(routeContext$({ injector, ...opts } as any)) as any;
76 | });
77 | }
78 |
--------------------------------------------------------------------------------
/projects/router/src/lib/route.ts:
--------------------------------------------------------------------------------
1 | import {
2 | InjectionToken,
3 | Provider,
4 | runInInjectionContext,
5 | type Type,
6 | } from '@angular/core';
7 | import {
8 | AnyContext,
9 | AnyRoute,
10 | AnyRouter,
11 | BaseRootRoute,
12 | BaseRoute,
13 | BaseRouteApi,
14 | ConstrainLiteral,
15 | ErrorComponentProps,
16 | NotFoundRouteProps,
17 | RegisteredRouter,
18 | ResolveFullPath,
19 | ResolveId,
20 | ResolveParams,
21 | RootRouteId,
22 | RootRouteOptions,
23 | RouteConstraints,
24 | RouteIds,
25 | RouteOptions,
26 | } from '@tanstack/router-core';
27 | import { loaderData, loaderData$, LoaderDataRoute } from './loader-data';
28 | import { loaderDeps, loaderDeps$, LoaderDepsRoute } from './loader-deps';
29 | import { match, match$, MatchRoute } from './match';
30 | import { params, params$, ParamsRoute } from './params';
31 | import {
32 | routeContext,
33 | routeContext$,
34 | RouteContextRoute,
35 | } from './route-context';
36 | import { search, search$, SearchRoute } from './search';
37 |
38 | declare module '@tanstack/router-core' {
39 | export interface UpdatableRouteOptionsExtensions {
40 | component?: () => RouteComponent;
41 | errorComponent?: false | null | (() => RouteComponent);
42 | notFoundComponent?: () => RouteComponent;
43 | pendingComponent?: () => RouteComponent;
44 | providers?: Provider[];
45 | }
46 |
47 | export interface RouteExtensions<
48 | TId extends string,
49 | TFullPath extends string,
50 | > {
51 | match$: MatchRoute;
52 | match: MatchRoute;
53 | routeContext$: RouteContextRoute;
54 | routeContext: RouteContextRoute;
55 | search$: SearchRoute;
56 | search: SearchRoute;
57 | params$: ParamsRoute;
58 | params: ParamsRoute;
59 | loaderDeps$: LoaderDepsRoute;
60 | loaderDeps: LoaderDepsRoute;
61 | loaderData$: LoaderDataRoute;
62 | loaderData: LoaderDataRoute;
63 | }
64 | }
65 |
66 | export const ERROR_COMPONENT_CONTEXT = new InjectionToken(
67 | 'ERROR_COMPONENT_CONTEXT'
68 | );
69 | export const NOT_FOUND_COMPONENT_CONTEXT =
70 | new InjectionToken('NOT_FOUND_COMPONENT_CONTEXT');
71 |
72 | export type RouteComponent =
73 | Type;
74 |
75 | export function routeApi<
76 | const TId,
77 | TRouter extends AnyRouter = RegisteredRouter,
78 | >(id: ConstrainLiteral>) {
79 | return new RouteApi({ id });
80 | }
81 |
82 | export class RouteApi<
83 | TId,
84 | TRouter extends AnyRouter = RegisteredRouter,
85 | > extends BaseRouteApi {
86 | /**
87 | * @deprecated Use the `getRouteApi` function instead.
88 | */
89 | constructor({ id }: { id: TId }) {
90 | super({ id });
91 | }
92 |
93 | match$: MatchRoute = (opts) =>
94 | match$({ ...opts, from: this.id } as any) as any;
95 | match: MatchRoute = (opts) =>
96 | match({ ...opts, from: this.id } as any) as any;
97 |
98 | routeContext$: RouteContextRoute = (opts) =>
99 | routeContext$({ ...opts, from: this.id } as any);
100 | routeContext: RouteContextRoute = (opts) =>
101 | routeContext({ ...opts, from: this.id } as any);
102 |
103 | search$: SearchRoute = (opts) =>
104 | search$({ ...opts, from: this.id } as any) as any;
105 | search: SearchRoute = (opts) =>
106 | search({ ...opts, from: this.id } as any) as any;
107 |
108 | params$: ParamsRoute = (opts) =>
109 | params$({ ...opts, from: this.id } as any) as any;
110 | params: ParamsRoute = (opts) =>
111 | params({ ...opts, from: this.id } as any) as any;
112 |
113 | loaderDeps$: LoaderDepsRoute = (opts) =>
114 | loaderDeps$({ ...opts, from: this.id } as any);
115 | loaderDeps: LoaderDepsRoute = (opts) =>
116 | loaderDeps({ ...opts, from: this.id, strict: false } as any);
117 |
118 | loaderData$: LoaderDataRoute = (opts) =>
119 | loaderData$({ ...opts, from: this.id } as any);
120 | loaderData: LoaderDataRoute = (opts) =>
121 | loaderData({ ...opts, from: this.id, strict: false } as any);
122 | }
123 |
124 | export class Route<
125 | in out TParentRoute extends RouteConstraints['TParentRoute'] = AnyRoute,
126 | in out TPath extends RouteConstraints['TPath'] = '/',
127 | in out TFullPath extends RouteConstraints['TFullPath'] = ResolveFullPath<
128 | TParentRoute,
129 | TPath
130 | >,
131 | in out TCustomId extends RouteConstraints['TCustomId'] = string,
132 | in out TId extends RouteConstraints['TId'] = ResolveId<
133 | TParentRoute,
134 | TCustomId,
135 | TPath
136 | >,
137 | in out TSearchValidator = undefined,
138 | in out TParams = ResolveParams,
139 | in out TRouterContext = AnyContext,
140 | in out TRouteContextFn = AnyContext,
141 | in out TBeforeLoadFn = AnyContext,
142 | in out TLoaderDeps extends Record = {},
143 | in out TLoaderFn = undefined,
144 | in out TChildren = unknown,
145 | in out TFileRouteTypes = unknown,
146 | > extends BaseRoute<
147 | TParentRoute,
148 | TPath,
149 | TFullPath,
150 | TCustomId,
151 | TId,
152 | TSearchValidator,
153 | TParams,
154 | TRouterContext,
155 | TRouteContextFn,
156 | TBeforeLoadFn,
157 | TLoaderDeps,
158 | TLoaderFn,
159 | TChildren,
160 | TFileRouteTypes
161 | > {
162 | /**
163 | * @deprecated Use the `createRoute` function instead.
164 | */
165 | constructor(
166 | options?: RouteOptions<
167 | TParentRoute,
168 | TId,
169 | TCustomId,
170 | TFullPath,
171 | TPath,
172 | TSearchValidator,
173 | TParams,
174 | TLoaderDeps,
175 | TLoaderFn,
176 | TRouterContext,
177 | TRouteContextFn,
178 | TBeforeLoadFn
179 | >
180 | ) {
181 | super(options);
182 | }
183 |
184 | match$: MatchRoute = (opts) =>
185 | match$({ ...opts, from: this.id } as any) as any;
186 | match: MatchRoute = (opts) =>
187 | match({ ...opts, from: this.id } as any) as any;
188 |
189 | routeContext$: RouteContextRoute = (opts) =>
190 | routeContext$({ ...opts, from: this.id } as any);
191 | routeContext: RouteContextRoute = (opts) =>
192 | routeContext({ ...opts, from: this.id } as any);
193 |
194 | search$: SearchRoute = (opts) =>
195 | search$({ ...opts, from: this.id } as any) as any;
196 | search: SearchRoute = (opts) =>
197 | search({ ...opts, from: this.id } as any) as any;
198 |
199 | params$: ParamsRoute = (opts) =>
200 | params$({ ...opts, from: this.id } as any) as any;
201 | params: ParamsRoute = (opts) =>
202 | params({ ...opts, from: this.id } as any) as any;
203 |
204 | loaderDeps$: LoaderDepsRoute = (opts) =>
205 | loaderDeps$({ ...opts, from: this.id } as any);
206 | loaderDeps: LoaderDepsRoute = (opts) =>
207 | loaderDeps({ ...opts, from: this.id } as any);
208 |
209 | loaderData$: LoaderDataRoute = (opts) =>
210 | loaderData$({ ...opts, from: this.id } as any);
211 | loaderData: LoaderDataRoute = (opts) =>
212 | loaderData({ ...opts, from: this.id } as any);
213 | }
214 |
215 | export function createRoute<
216 | TParentRoute extends RouteConstraints['TParentRoute'] = AnyRoute,
217 | TPath extends RouteConstraints['TPath'] = '/',
218 | TFullPath extends RouteConstraints['TFullPath'] = ResolveFullPath<
219 | TParentRoute,
220 | TPath
221 | >,
222 | TCustomId extends RouteConstraints['TCustomId'] = string,
223 | TId extends RouteConstraints['TId'] = ResolveId<
224 | TParentRoute,
225 | TCustomId,
226 | TPath
227 | >,
228 | TSearchValidator = undefined,
229 | TParams = ResolveParams,
230 | TRouteContextFn = AnyContext,
231 | TBeforeLoadFn = AnyContext,
232 | TLoaderDeps extends Record = {},
233 | TLoaderFn = undefined,
234 | TChildren = unknown,
235 | >(
236 | options: RouteOptions<
237 | TParentRoute,
238 | TId,
239 | TCustomId,
240 | TFullPath,
241 | TPath,
242 | TSearchValidator,
243 | TParams,
244 | TLoaderDeps,
245 | TLoaderFn,
246 | AnyContext,
247 | TRouteContextFn,
248 | TBeforeLoadFn
249 | >
250 | ): Route<
251 | TParentRoute,
252 | TPath,
253 | TFullPath,
254 | TCustomId,
255 | TId,
256 | TSearchValidator,
257 | TParams,
258 | AnyContext,
259 | TRouteContextFn,
260 | TBeforeLoadFn,
261 | TLoaderDeps,
262 | TLoaderFn,
263 | TChildren,
264 | unknown
265 | > {
266 | if (options.loader) {
267 | options.loader = runFnInInjectionContext(options.loader);
268 | }
269 |
270 | if (options.shouldReload && typeof options.shouldReload === 'function') {
271 | options.shouldReload = runFnInInjectionContext(options.shouldReload);
272 | }
273 |
274 | if (options.beforeLoad) {
275 | options.beforeLoad = runFnInInjectionContext(options.beforeLoad);
276 | }
277 |
278 | return new Route<
279 | TParentRoute,
280 | TPath,
281 | TFullPath,
282 | TCustomId,
283 | TId,
284 | TSearchValidator,
285 | TParams,
286 | AnyContext,
287 | TRouteContextFn,
288 | TBeforeLoadFn,
289 | TLoaderDeps,
290 | TLoaderFn,
291 | TChildren,
292 | unknown
293 | >(options);
294 | }
295 |
296 | export type AnyRootRoute = RootRoute;
297 |
298 | export function createRootRouteWithContext() {
299 | return <
300 | TRouteContextFn = AnyContext,
301 | TBeforeLoadFn = AnyContext,
302 | TSearchValidator = undefined,
303 | TLoaderDeps extends Record = {},
304 | TLoaderFn = undefined,
305 | >(
306 | options?: RootRouteOptions<
307 | TSearchValidator,
308 | TRouterContext,
309 | TRouteContextFn,
310 | TBeforeLoadFn,
311 | TLoaderDeps,
312 | TLoaderFn
313 | >
314 | ) => {
315 | return createRootRoute<
316 | TSearchValidator,
317 | TRouterContext,
318 | TRouteContextFn,
319 | TBeforeLoadFn,
320 | TLoaderDeps,
321 | TLoaderFn
322 | >(options as any);
323 | };
324 | }
325 |
326 | export class RootRoute<
327 | in out TSearchValidator = undefined,
328 | in out TRouterContext = {},
329 | in out TRouteContextFn = AnyContext,
330 | in out TBeforeLoadFn = AnyContext,
331 | in out TLoaderDeps extends Record = {},
332 | in out TLoaderFn = undefined,
333 | in out TChildren = unknown,
334 | in out TFileRouteTypes = unknown,
335 | > extends BaseRootRoute<
336 | TSearchValidator,
337 | TRouterContext,
338 | TRouteContextFn,
339 | TBeforeLoadFn,
340 | TLoaderDeps,
341 | TLoaderFn,
342 | TChildren,
343 | TFileRouteTypes
344 | > {
345 | /**
346 | * @deprecated `RootRoute` is now an internal implementation detail. Use `createRootRoute()` instead.
347 | */
348 | constructor(
349 | options?: RootRouteOptions<
350 | TSearchValidator,
351 | TRouterContext,
352 | TRouteContextFn,
353 | TBeforeLoadFn,
354 | TLoaderDeps,
355 | TLoaderFn
356 | >
357 | ) {
358 | super(options);
359 | }
360 |
361 | match$: MatchRoute = (opts) =>
362 | match$({ ...opts, from: this.id } as any) as any;
363 | match: MatchRoute = (opts) =>
364 | match({ ...opts, from: this.id } as any) as any;
365 |
366 | routeContext$: RouteContextRoute = (opts) =>
367 | routeContext$({ ...opts, from: this.id } as any);
368 | routeContext: RouteContextRoute = (opts) =>
369 | routeContext({ ...opts, from: this.id } as any);
370 |
371 | search$: SearchRoute = (opts) =>
372 | search$({ ...opts, from: this.id } as any) as any;
373 | search: SearchRoute = (opts) =>
374 | search({ ...opts, from: this.id } as any) as any;
375 |
376 | params$: ParamsRoute = (opts) =>
377 | params$({ ...opts, from: this.id } as any) as any;
378 | params: ParamsRoute = (opts) =>
379 | params({ ...opts, from: this.id } as any) as any;
380 |
381 | loaderDeps$: LoaderDepsRoute = (opts) =>
382 | loaderDeps$({ ...opts, from: this.id } as any);
383 | loaderDeps: LoaderDepsRoute = (opts) =>
384 | loaderDeps({ ...opts, from: this.id } as any);
385 |
386 | loaderData$: LoaderDataRoute = (opts) =>
387 | loaderData$({ ...opts, from: this.id } as any);
388 | loaderData: LoaderDataRoute = (opts) =>
389 | loaderData({ ...opts, from: this.id } as any);
390 | }
391 |
392 | export function createRootRoute<
393 | TSearchValidator = undefined,
394 | TRouterContext = {},
395 | TRouteContextFn = AnyContext,
396 | TBeforeLoadFn = AnyContext,
397 | TLoaderDeps extends Record = {},
398 | TLoaderFn = undefined,
399 | >(
400 | options?: RootRouteOptions<
401 | TSearchValidator,
402 | TRouterContext,
403 | TRouteContextFn,
404 | TBeforeLoadFn,
405 | TLoaderDeps,
406 | TLoaderFn
407 | >
408 | ): RootRoute<
409 | TSearchValidator,
410 | TRouterContext,
411 | TRouteContextFn,
412 | TBeforeLoadFn,
413 | TLoaderDeps,
414 | TLoaderFn,
415 | unknown,
416 | unknown
417 | > {
418 | return new RootRoute<
419 | TSearchValidator,
420 | TRouterContext,
421 | TRouteContextFn,
422 | TBeforeLoadFn,
423 | TLoaderDeps,
424 | TLoaderFn
425 | >(options);
426 | }
427 |
428 | export class NotFoundRoute<
429 | TParentRoute extends AnyRootRoute,
430 | TRouterContext = AnyContext,
431 | TRouteContextFn = AnyContext,
432 | TBeforeLoadFn = AnyContext,
433 | TSearchValidator = undefined,
434 | TLoaderDeps extends Record = {},
435 | TLoaderFn = undefined,
436 | TChildren = unknown,
437 | > extends Route<
438 | TParentRoute,
439 | '/404',
440 | '/404',
441 | '404',
442 | '404',
443 | TSearchValidator,
444 | {},
445 | TRouterContext,
446 | TRouteContextFn,
447 | TBeforeLoadFn,
448 | TLoaderDeps,
449 | TLoaderFn,
450 | TChildren
451 | > {
452 | constructor(
453 | options: Omit<
454 | RouteOptions<
455 | TParentRoute,
456 | string,
457 | string,
458 | string,
459 | string,
460 | TSearchValidator,
461 | {},
462 | TLoaderDeps,
463 | TLoaderFn,
464 | TRouterContext,
465 | TRouteContextFn,
466 | TBeforeLoadFn
467 | >,
468 | | 'caseSensitive'
469 | | 'parseParams'
470 | | 'stringifyParams'
471 | | 'path'
472 | | 'id'
473 | | 'params'
474 | >
475 | ) {
476 | super({
477 | ...(options as any),
478 | id: '404',
479 | });
480 | }
481 | }
482 |
483 | function runFnInInjectionContext any>(fn: TFn) {
484 | const originalFn = fn;
485 | return (...args: Parameters) => {
486 | const { context, location, route } = args[0];
487 | const routeInjector = context.getRouteInjector(route?.id || location.href);
488 | return runInInjectionContext(routeInjector, originalFn.bind(null, ...args));
489 | };
490 | }
491 |
--------------------------------------------------------------------------------
/projects/router/src/lib/router-devtools.ts:
--------------------------------------------------------------------------------
1 | import {
2 | afterNextRender,
3 | booleanAttribute,
4 | Directive,
5 | effect,
6 | ElementRef,
7 | inject,
8 | input,
9 | NgZone,
10 | signal,
11 | untracked,
12 | } from '@angular/core';
13 | import { TanStackRouterDevtoolsCore } from '@tanstack/router-devtools-core';
14 | import { injectRouter } from './router';
15 |
16 | @Directive({
17 | selector: 'router-devtools,RouterDevtools',
18 | host: { style: 'display: block;' },
19 | })
20 | export class RouterDevtools {
21 | private injectedRouter = injectRouter();
22 | private host = inject>(ElementRef);
23 | private ngZone = inject(NgZone);
24 |
25 | router = input(this.injectedRouter);
26 | initialIsOpen = input(undefined, { transform: booleanAttribute });
27 | panelOptions = input>({});
28 | closeButtonOptions = input>({});
29 | toggleButtonOptions = input>({});
30 | shadowDOMTarget = input();
31 | containerElement = input();
32 | position = input<'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'>();
33 |
34 | private devtools = signal(null);
35 |
36 | constructor() {
37 | afterNextRender(() => {
38 | const router = untracked(this.router);
39 | const [
40 | initialIsOpen,
41 | panelOptions,
42 | closeButtonOptions,
43 | toggleButtonOptions,
44 | shadowDOMTarget,
45 | containerElement,
46 | position,
47 | activeRouterState,
48 | ] = [
49 | untracked(this.initialIsOpen),
50 | untracked(this.panelOptions),
51 | untracked(this.closeButtonOptions),
52 | untracked(this.toggleButtonOptions),
53 | untracked(this.shadowDOMTarget),
54 | untracked(this.containerElement),
55 | untracked(this.position),
56 | router.state,
57 | ];
58 |
59 | // initial devTools
60 | this.devtools.set(
61 | new TanStackRouterDevtoolsCore({
62 | router,
63 | routerState: activeRouterState,
64 | initialIsOpen,
65 | position,
66 | panelProps: panelOptions,
67 | closeButtonProps: closeButtonOptions,
68 | toggleButtonProps: toggleButtonOptions,
69 | shadowDOMTarget,
70 | containerElement,
71 | })
72 | );
73 | });
74 |
75 | effect(() => {
76 | const devtools = this.devtools();
77 | if (!devtools) return;
78 | this.ngZone.runOutsideAngular(() => devtools.setRouter(this.router()));
79 | });
80 |
81 | effect((onCleanup) => {
82 | const devtools = this.devtools();
83 | if (!devtools) return;
84 | this.ngZone.runOutsideAngular(() => {
85 | const unsub = untracked(this.router).__store.subscribe(
86 | ({ currentVal }) => {
87 | devtools.setRouterState(currentVal);
88 | }
89 | );
90 | onCleanup(() => unsub());
91 | });
92 | });
93 |
94 | effect(() => {
95 | const devtools = this.devtools();
96 | if (!devtools) return;
97 |
98 | this.ngZone.runOutsideAngular(() => {
99 | devtools.setOptions({
100 | initialIsOpen: this.initialIsOpen(),
101 | panelProps: this.panelOptions(),
102 | closeButtonProps: this.closeButtonOptions(),
103 | toggleButtonProps: this.toggleButtonOptions(),
104 | position: this.position(),
105 | containerElement: this.containerElement(),
106 | shadowDOMTarget: this.shadowDOMTarget(),
107 | });
108 | });
109 | });
110 |
111 | effect((onCleanup) => {
112 | const devtools = this.devtools();
113 | if (!devtools) return;
114 | this.ngZone.runOutsideAngular(() =>
115 | devtools.mount(this.host.nativeElement)
116 | );
117 | onCleanup(() => {
118 | this.ngZone.runOutsideAngular(() => devtools.unmount());
119 | });
120 | });
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/projects/router/src/lib/router-root.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Directive,
3 | effect,
4 | EnvironmentInjector,
5 | inject,
6 | input,
7 | Provider,
8 | } from '@angular/core';
9 | import {
10 | AnyRoute,
11 | type AnyRouter,
12 | type RegisteredRouter,
13 | type RouterOptions,
14 | } from '@tanstack/router-core';
15 | import { Matches } from './matches';
16 | import { injectRouter, NgRouter } from './router';
17 |
18 | export type RouterRootOptions<
19 | TRouter extends AnyRouter = RegisteredRouter,
20 | TDehydrated extends Record = Record,
21 | > = Omit<
22 | RouterOptions<
23 | TRouter['routeTree'],
24 | NonNullable,
25 | false,
26 | TRouter['history'],
27 | TDehydrated
28 | >,
29 | 'context'
30 | > & {
31 | router: TRouter;
32 | context?: Partial<
33 | RouterOptions<
34 | TRouter['routeTree'],
35 | NonNullable,
36 | false,
37 | TRouter['history'],
38 | TDehydrated
39 | >['context']
40 | >;
41 | };
42 |
43 | @Directive({
44 | selector: 'router-root,RouterRoot',
45 | hostDirectives: [Matches],
46 | })
47 | export class RouterRoot<
48 | TRouter extends AnyRouter = RegisteredRouter,
49 | TDehydrated extends Record = Record,
50 | > {
51 | router = input['router']>(
52 | injectRouter() as unknown as TRouter
53 | );
54 | options = input, 'router'>>({});
55 |
56 | constructor() {
57 | const environmentInjector = inject(EnvironmentInjector);
58 | effect(() => {
59 | const [router, options] = [this.router(), this.options()];
60 | router.update({
61 | ...router.options,
62 | ...options,
63 | context: {
64 | ...router.options.context,
65 | ...options.context,
66 | getRouteInjector(routeId: string, providers: Provider[] = []) {
67 | return (
68 | router as unknown as NgRouter
69 | ).getRouteEnvInjector(
70 | routeId,
71 | environmentInjector,
72 | providers,
73 | router
74 | );
75 | },
76 | },
77 | });
78 | });
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/projects/router/src/lib/router-state.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertInInjectionContext,
3 | inject,
4 | Injector,
5 | runInInjectionContext,
6 | Signal,
7 | type ValueEqualityFn,
8 | } from '@angular/core';
9 | import { toSignal } from '@angular/core/rxjs-interop';
10 | import {
11 | type AnyRouter,
12 | type RegisteredRouter,
13 | type RouterState,
14 | shallow,
15 | } from '@tanstack/router-core';
16 | import { distinctUntilChanged, map, Observable } from 'rxjs';
17 | import { injectRouterState } from './router';
18 |
19 | export type RouterStateResult<
20 | TRouter extends AnyRouter,
21 | TSelected,
22 | > = unknown extends TSelected ? RouterState : TSelected;
23 |
24 | export type RouterStateOptions = {
25 | select?: (state: RouterState) => TSelected;
26 | equal?: ValueEqualityFn<
27 | RouterStateResult, NoInfer>
28 | >;
29 | injector?: Injector;
30 | };
31 |
32 | export function routerState$<
33 | TRouter extends AnyRouter = RegisteredRouter,
34 | TSelected = unknown,
35 | >({
36 | select,
37 | injector,
38 | equal = shallow,
39 | }: RouterStateOptions) {
40 | !injector && assertInInjectionContext(routerState$);
41 |
42 | if (!injector) {
43 | injector = inject(Injector);
44 | }
45 |
46 | return runInInjectionContext(injector, () => {
47 | const rootRouterState = injectRouterState();
48 | if (select)
49 | return rootRouterState.pipe(
50 | map((s) => select(s) as any),
51 | distinctUntilChanged(equal)
52 | );
53 | return rootRouterState.pipe(distinctUntilChanged(equal) as any);
54 | }) as Observable>;
55 | }
56 |
57 | export function routerState<
58 | TRouter extends AnyRouter = RegisteredRouter,
59 | TSelected = unknown,
60 | >({
61 | select,
62 | injector,
63 | equal = shallow,
64 | }: RouterStateOptions