├── .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 = {}) { 65 | !injector && assertInInjectionContext(routerState); 66 | 67 | if (!injector) { 68 | injector = inject(Injector); 69 | } 70 | 71 | return runInInjectionContext(injector, () => 72 | toSignal(routerState$({ select, injector, equal }), { injector }) 73 | ) as Signal>; 74 | } 75 | -------------------------------------------------------------------------------- /projects/router/src/lib/router.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationRef, 3 | createEnvironmentInjector, 4 | EnvironmentInjector, 5 | inject, 6 | InjectionToken, 7 | Injector, 8 | type Provider, 9 | } from '@angular/core'; 10 | import type { HistoryLocation, RouterHistory } from '@tanstack/history'; 11 | import { 12 | type AnyRoute, 13 | type AnyRouter, 14 | type CreateRouterFn, 15 | rootRouteId, 16 | type RouterConstructorOptions, 17 | RouterCore, 18 | type RouterState, 19 | type TrailingSlashOption, 20 | } from '@tanstack/router-core'; 21 | import { BehaviorSubject, type Observable } from 'rxjs'; 22 | import type { RouteComponent } from './route'; 23 | 24 | declare module '@tanstack/history' { 25 | interface HistoryState { 26 | __tempLocation?: HistoryLocation; 27 | __tempKey?: string; 28 | __hashScrollIntoViewOptions?: boolean | ScrollIntoViewOptions; 29 | } 30 | } 31 | 32 | declare module '@tanstack/router-core' { 33 | export interface RouterOptionsExtensions { 34 | /** 35 | * The default `component` a route should use if no component is provided. 36 | * 37 | * @default Outlet 38 | * @link [API Docs](https://tanstack.com/router/latest/docs/framework/solid/api/router/RouterOptionsType#defaultcomponent-property) 39 | */ 40 | defaultComponent?: () => RouteComponent; 41 | /** 42 | * The default `errorComponent` a route should use if no error component is provided. 43 | * 44 | * @default ErrorComponent 45 | * @link [API Docs](https://tanstack.com/router/latest/docs/framework/solid/api/router/RouterOptionsType#defaulterrorcomponent-property) 46 | * @link [Guide](https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#handling-errors-with-routeoptionserrorcomponent) 47 | */ 48 | defaultErrorComponent?: () => RouteComponent; 49 | /** 50 | * The default `pendingComponent` a route should use if no pending component is provided. 51 | * 52 | * @link [API Docs](https://tanstack.com/router/latest/docs/framework/solid/api/router/RouterOptionsType#defaultpendingcomponent-property) 53 | * @link [Guide](https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#showing-a-pending-component) 54 | */ 55 | defaultPendingComponent?: () => RouteComponent; 56 | /** 57 | * The default `notFoundComponent` a route should use if no notFound component is provided. 58 | * 59 | * @default NotFound 60 | * @link [API Docs](https://tanstack.com/router/latest/docs/framework/solid/api/router/RouterOptionsType#defaultnotfoundcomponent-property) 61 | * @link [Guide](https://tanstack.com/router/latest/docs/framework/solid/guide/not-found-errors#default-router-wide-not-found-handling) 62 | */ 63 | defaultNotFoundComponent?: () => RouteComponent; 64 | /** 65 | * The default `onCatch` handler for errors caught by the Router ErrorBoundary 66 | * 67 | * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultoncatch-property) 68 | * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#handling-errors-with-routeoptionsoncatch) 69 | */ 70 | defaultOnCatch?: (error: Error) => void; 71 | } 72 | } 73 | 74 | export const ROUTER = new InjectionToken>('ROUTER'); 75 | export const ROUTER_STATE = new InjectionToken< 76 | Observable> 77 | >('ROUTER_STATE'); 78 | 79 | export function injectRouter() { 80 | return inject(ROUTER); 81 | } 82 | 83 | export function injectRouterState() { 84 | return inject(ROUTER_STATE); 85 | } 86 | 87 | export function provideRouter(router: AnyRouter) { 88 | return [ 89 | { provide: ROUTER, useValue: router }, 90 | { 91 | provide: ROUTER_STATE, 92 | useFactory: () => { 93 | const state = new BehaviorSubject(router.state); 94 | const appRef = inject(ApplicationRef); 95 | 96 | const unsub = router.__store.subscribe(({ currentVal }) => { 97 | state.next(currentVal); 98 | }); 99 | 100 | appRef.onDestroy(() => { 101 | state.complete(); 102 | unsub(); 103 | }); 104 | 105 | return state.asObservable(); 106 | }, 107 | }, 108 | ]; 109 | } 110 | 111 | export const createRouter: CreateRouterFn = (options) => { 112 | return new NgRouter(options); 113 | }; 114 | 115 | export class NgRouter< 116 | in out TRouteTree extends AnyRoute, 117 | in out TTrailingSlashOption extends TrailingSlashOption = 'never', 118 | in out TDefaultStructuralSharingOption extends boolean = false, 119 | in out TRouterHistory extends RouterHistory = RouterHistory, 120 | in out TDehydrated extends Record = Record, 121 | > extends RouterCore< 122 | TRouteTree, 123 | TTrailingSlashOption, 124 | TDefaultStructuralSharingOption, 125 | TRouterHistory, 126 | TDehydrated 127 | > { 128 | private injectors = new Map(); 129 | private envInjectors = new Map(); 130 | 131 | constructor( 132 | options: RouterConstructorOptions< 133 | TRouteTree, 134 | TTrailingSlashOption, 135 | TDefaultStructuralSharingOption, 136 | TRouterHistory, 137 | TDehydrated 138 | > 139 | ) { 140 | super(options); 141 | } 142 | 143 | getRouteInjector( 144 | routeId: string, 145 | parent: Injector, 146 | providers: Provider[] = [] 147 | ) { 148 | const existingInjector = this.injectors.get(routeId); 149 | if (existingInjector) return existingInjector; 150 | 151 | const injector = Injector.create({ 152 | providers, 153 | parent, 154 | name: routeId, 155 | }); 156 | 157 | // cache 158 | this.injectors.set(routeId, injector); 159 | return injector; 160 | } 161 | 162 | getRouteEnvInjector( 163 | routeId: string, 164 | parent: EnvironmentInjector, 165 | providers: Provider[] = [], 166 | router: AnyRouter 167 | ) { 168 | const existingInjector = this.envInjectors.get(routeId); 169 | if (existingInjector) return existingInjector; 170 | 171 | let route = router.routesById[routeId] || router.routesByPath[routeId]; 172 | 173 | // walk up the route hierarchy to build the providers 174 | while (route) { 175 | if (route.options?.providers) { 176 | providers.push(...route.options.providers); 177 | } 178 | 179 | const parentInjector = route.parentRoute 180 | ? this.envInjectors.get(route.parentRoute.id) 181 | : null; 182 | 183 | if (parentInjector) { 184 | parent = parentInjector; 185 | break; 186 | } 187 | 188 | route = route.parentRoute; 189 | } 190 | 191 | const envInjector = createEnvironmentInjector(providers, parent, routeId); 192 | 193 | // if routeId is rootRouteId, we'll switch existing injectors' parent to the __root__ injector 194 | if (routeId === rootRouteId) { 195 | this.envInjectors.forEach((ele) => { 196 | if ('parent' in ele && ele.parent === parent) { 197 | ele.parent = envInjector; 198 | } 199 | }); 200 | } 201 | 202 | // cache 203 | this.envInjectors.set(routeId, envInjector); 204 | return envInjector; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /projects/router/src/lib/search.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 | ResolveUseSearch, 13 | StrictOrFrom, 14 | ThrowConstraint, 15 | ThrowOrOptional, 16 | UseSearchResult, 17 | } from '@tanstack/router-core'; 18 | import { Observable } from 'rxjs'; 19 | import { match$ } from './match'; 20 | 21 | export interface SearchBaseOptions< 22 | TRouter extends AnyRouter, 23 | TFrom, 24 | TStrict extends boolean, 25 | TThrow extends boolean, 26 | TSelected, 27 | > { 28 | select?: (state: ResolveUseSearch) => TSelected; 29 | shouldThrow?: TThrow; 30 | injector?: Injector; 31 | } 32 | 33 | export type SearchOptions< 34 | TRouter extends AnyRouter, 35 | TFrom, 36 | TStrict extends boolean, 37 | TThrow extends boolean, 38 | TSelected, 39 | > = StrictOrFrom & 40 | SearchBaseOptions; 41 | 42 | export type SearchRoute = < 43 | TRouter extends AnyRouter = RegisteredRouter, 44 | TSelected = unknown, 45 | >( 46 | opts?: SearchBaseOptions< 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 search$< 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 | }: SearchOptions< 67 | TRouter, 68 | TFrom, 69 | TStrict, 70 | ThrowConstraint, 71 | TSelected 72 | >): Observable< 73 | ThrowOrOptional, TThrow> 74 | > { 75 | !injector && assertInInjectionContext(search); 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.search) : match.search; 88 | }, 89 | }) as any; 90 | }); 91 | } 92 | 93 | export function search< 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 | }: SearchOptions< 103 | TRouter, 104 | TFrom, 105 | TStrict, 106 | ThrowConstraint, 107 | TSelected 108 | >): Signal< 109 | ThrowOrOptional, TThrow> 110 | > { 111 | !injector && assertInInjectionContext(search); 112 | 113 | if (!injector) { 114 | injector = inject(Injector); 115 | } 116 | 117 | return runInInjectionContext(injector, () => { 118 | return toSignal(search$({ injector, ...opts } as any)) as any; 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /projects/router/src/lib/transitioner.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { 3 | afterNextRender, 4 | ChangeDetectorRef, 5 | DestroyRef, 6 | Directive, 7 | inject, 8 | OnInit, 9 | untracked, 10 | } from '@angular/core'; 11 | import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core'; 12 | import { 13 | BehaviorSubject, 14 | combineLatest, 15 | distinctUntilChanged, 16 | map, 17 | pairwise, 18 | Subscription, 19 | tap, 20 | } from 'rxjs'; 21 | import { injectRouter } from './router'; 22 | import { routerState$ } from './router-state'; 23 | 24 | @Directive() 25 | export class Transitioner implements OnInit { 26 | private router = injectRouter(); 27 | private destroyRef = inject(DestroyRef); 28 | private document = inject(DOCUMENT); 29 | private cdr = inject(ChangeDetectorRef); 30 | 31 | private matches$ = routerState$({ select: (s) => s.matches }); 32 | private hasPendingMatches$ = this.matches$.pipe( 33 | map((matches) => matches.some((d) => d.status === 'pending')), 34 | distinctUntilChanged(() => false) 35 | ); 36 | private isLoading$ = routerState$({ 37 | select: (s) => s.isLoading, 38 | equal: () => false, 39 | }); 40 | private previousIsLoading$ = this.isLoading$.pipe( 41 | pairwise(), 42 | map(([prev, curr]) => prev ?? curr) 43 | ); 44 | 45 | private isTransitioning$ = new BehaviorSubject(false); 46 | private isAnyPending$ = combineLatest([ 47 | this.isLoading$, 48 | this.isTransitioning$, 49 | this.hasPendingMatches$, 50 | ]).pipe( 51 | map( 52 | ([isLoading, isTransitioning, hasPendingMatches]) => 53 | isLoading || isTransitioning || hasPendingMatches 54 | ), 55 | distinctUntilChanged(() => false) 56 | ); 57 | private previousIsAnyPending$ = this.isAnyPending$.pipe( 58 | pairwise(), 59 | map(([prev, curr]) => prev ?? curr) 60 | ); 61 | 62 | private isPagePending$ = combineLatest([ 63 | this.isLoading$, 64 | this.hasPendingMatches$, 65 | ]).pipe( 66 | map(([isLoading, hasPendingMatches]) => isLoading || hasPendingMatches), 67 | distinctUntilChanged(() => false) 68 | ); 69 | private previousIsPagePending$ = this.isPagePending$.pipe( 70 | pairwise(), 71 | map(([prev, curr]) => prev ?? curr) 72 | ); 73 | 74 | private mountLoadForRouter = { router: this.router, mounted: false }; 75 | 76 | private load$ = combineLatest([ 77 | this.previousIsLoading$, 78 | this.isLoading$, 79 | ]).pipe( 80 | tap(([previousIsLoading, isLoading]) => { 81 | if (previousIsLoading && !isLoading) { 82 | this.router.emit({ 83 | type: 'onLoad', 84 | ...getLocationChangeInfo(this.router.state), 85 | }); 86 | } 87 | }) 88 | ); 89 | private pagePending$ = combineLatest([ 90 | this.previousIsPagePending$, 91 | this.isPagePending$, 92 | ]).pipe( 93 | tap(([previousIsPagePending, isPagePending]) => { 94 | // emit onBeforeRouteMount 95 | if (previousIsPagePending && !isPagePending) { 96 | this.router.emit({ 97 | type: 'onBeforeRouteMount', 98 | ...getLocationChangeInfo(this.router.state), 99 | }); 100 | } 101 | }) 102 | ); 103 | private pending$ = combineLatest([ 104 | this.previousIsAnyPending$, 105 | this.isAnyPending$, 106 | ]).pipe( 107 | tap(([previousIsAnyPending, isAnyPending]) => { 108 | // The router was pending and now it's not 109 | if (previousIsAnyPending && !isAnyPending) { 110 | this.router.emit({ 111 | type: 'onResolved', 112 | ...getLocationChangeInfo(this.router.state), 113 | }); 114 | 115 | this.router.__store.setState((s) => ({ 116 | ...s, 117 | status: 'idle', 118 | resolvedLocation: s.location, 119 | })); 120 | if ( 121 | typeof this.document !== 'undefined' && 122 | 'querySelector' in this.document 123 | ) { 124 | const hashScrollIntoViewOptions = 125 | this.router.state.location.state.__hashScrollIntoViewOptions ?? 126 | true; 127 | 128 | if ( 129 | hashScrollIntoViewOptions && 130 | this.router.state.location.hash !== '' 131 | ) { 132 | const el = this.document.getElementById( 133 | this.router.state.location.hash 134 | ); 135 | if (el) el.scrollIntoView(hashScrollIntoViewOptions); 136 | } 137 | } 138 | } 139 | }) 140 | ); 141 | 142 | constructor() { 143 | if (!this.router.isServer) { 144 | this.router.startTransition = (fn) => { 145 | this.isTransitioning$.next(true); 146 | fn(); 147 | this.isTransitioning$.next(false); 148 | this.cdr.detectChanges(); 149 | }; 150 | } 151 | 152 | const subscription = new Subscription(); 153 | 154 | // Try to load the initial location 155 | afterNextRender(() => { 156 | untracked(() => { 157 | const window = this.document.defaultView; 158 | if ( 159 | (typeof window !== 'undefined' && this.router.clientSsr) || 160 | (this.mountLoadForRouter.router === this.router && 161 | this.mountLoadForRouter.mounted) 162 | ) { 163 | return; 164 | } 165 | this.mountLoadForRouter = { router: this.router, mounted: true }; 166 | const tryLoad = async () => { 167 | try { 168 | await this.router.load(); 169 | this.router.__store.setState((s) => ({ ...s, status: 'idle' })); 170 | } catch (err) { 171 | console.error(err); 172 | } 173 | }; 174 | void tryLoad(); 175 | }); 176 | 177 | subscription.add(this.load$.subscribe()); 178 | subscription.add(this.pagePending$.subscribe()); 179 | subscription.add(this.pending$.subscribe()); 180 | }); 181 | 182 | this.destroyRef.onDestroy(() => subscription.unsubscribe()); 183 | } 184 | 185 | ngOnInit() { 186 | // Subscribe to location changes 187 | // and try to load the new location 188 | const unsub = this.router.history.subscribe(() => this.router.load()); 189 | 190 | const nextLocation = this.router.buildLocation({ 191 | to: this.router.latestLocation.pathname, 192 | search: true, 193 | params: true, 194 | hash: true, 195 | state: true, 196 | _includeValidateSearch: true, 197 | }); 198 | 199 | if ( 200 | trimPathRight(this.router.latestLocation.href) !== 201 | trimPathRight(nextLocation.href) 202 | ) { 203 | void this.router.commitLocation({ ...nextLocation, replace: true }); 204 | } 205 | 206 | this.destroyRef.onDestroy(() => unsub()); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /projects/router/src/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/index'; 2 | -------------------------------------------------------------------------------- /projects/router/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/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/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 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandonroberts/tanstack-angular-router/a89ddd27d8116392c6fbc1205c17f488fd19f660/public/favicon.ico -------------------------------------------------------------------------------- /src/app/about/about.route.ts: -------------------------------------------------------------------------------- 1 | import { createRoute } from 'tanstack-angular-router-experimental'; 2 | 3 | import { inject } from '@angular/core'; 4 | import { firstValueFrom } from 'rxjs'; 5 | import { Route as RootRoute } from '../root.route'; 6 | import { Spinner } from '../spinner'; 7 | import { TodosClient } from '../todos-client'; 8 | 9 | export const AboutRoute = createRoute({ 10 | getParentRoute: () => RootRoute, 11 | path: 'about', 12 | pendingComponent: () => Spinner, 13 | loader: async () => { 14 | const todosService = inject(TodosClient); 15 | const todos = await firstValueFrom(todosService.getTodo(1)); 16 | await new Promise((resolve) => setTimeout(resolve, 5_000)); 17 | return { todos }; 18 | }, 19 | }).lazy(() => import('./about').then((m) => m.LazyAboutRoute)); 20 | -------------------------------------------------------------------------------- /src/app/about/about.ts: -------------------------------------------------------------------------------- 1 | import { JsonPipe } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | import { createLazyRoute } from 'tanstack-angular-router-experimental'; 4 | 5 | export const LazyAboutRoute = createLazyRoute('/about')({ 6 | component: () => About, 7 | }); 8 | 9 | @Component({ 10 | selector: 'about', 11 | imports: [JsonPipe], 12 | template: ` 13 | TanStack Routing in Angular 14 | 15 |
16 | 17 |

Loader Data

18 | @if (loaderData()?.todos; as todos) { 19 |
{{ todos | json }}
20 | } @else { 21 |

Loading...

22 | } 23 | 24 |
25 | `, 26 | }) 27 | export class About { 28 | loaderData = LazyAboutRoute.loaderData(); 29 | } 30 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { provideHttpClient, withFetch } from '@angular/common/http'; 2 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; 3 | 4 | import { provideRouter } from 'tanstack-angular-router-experimental'; 5 | 6 | import { routeTree } from './router'; 7 | 8 | export const appConfig: ApplicationConfig = { 9 | providers: [ 10 | provideZoneChangeDetection({ eventCoalescing: true }), 11 | // provideExperimentalZonelessChangeDetection(), 12 | provideRouter({ routeTree }), 13 | provideHttpClient(withFetch()), 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { App } from './app'; 3 | 4 | describe('App', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [App], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(App); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have the 'tanstack-router-angular' title`, () => { 18 | const fixture = TestBed.createComponent(App); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual('tanstack-router-angular'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(App); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement as HTMLElement; 27 | expect(compiled.querySelector('h1')?.textContent).toContain( 28 | 'Hello, tanstack-router-angular' 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { 3 | Link, 4 | linkOptions, 5 | Outlet, 6 | RouterDevtools, 7 | } from 'tanstack-angular-router-experimental'; 8 | 9 | @Component({ 10 | selector: 'app-root', 11 | imports: [Outlet, Link, RouterDevtools], 12 | template: ` 13 |

Welcome to {{ title }}!

14 | @for (link of links; track link.to) { 15 | {{ link.label }} 16 | | 17 | } 18 |
19 | 20 | 21 | 22 | 23 | `, 24 | styles: [ 25 | ` 26 | a[data-active='true'] { 27 | font-weight: bold; 28 | padding: 0.5rem; 29 | border: 1px solid; 30 | } 31 | `, 32 | ], 33 | }) 34 | export class App { 35 | title = 'tanstack-router-angular'; 36 | 37 | protected links = linkOptions([ 38 | { to: '/', label: 'Home' }, 39 | { to: '/about', preload: 'intent', label: 'About' }, 40 | { to: '/parent', label: 'Parent', activeOptions: { exact: false } }, 41 | { to: '/protected', label: 'Protected' }, 42 | { to: '/login', label: 'Login', search: { redirect: '' } }, 43 | ]); 44 | } 45 | -------------------------------------------------------------------------------- /src/app/auth-state.ts: -------------------------------------------------------------------------------- 1 | import { computed, Injectable, signal } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class AuthState { 5 | username = signal(''); 6 | isAuthenticated = computed(() => this.username() !== ''); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/child.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { createRoute } from 'tanstack-angular-router-experimental'; 4 | 5 | import { Route as ParentRoute } from './parent'; 6 | 7 | export const Route = createRoute({ 8 | getParentRoute: () => ParentRoute, 9 | path: '$id', 10 | component: () => Child, 11 | }); 12 | 13 | @Component({ 14 | selector: 'child', 15 | template: ` 16 | Child {{ params().id }} 17 | `, 18 | }) 19 | export class Child { 20 | params = Route.routeParams(); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/home.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { createRoute } from 'tanstack-angular-router-experimental'; 4 | 5 | import { Route as RootRoute } from './root.route'; 6 | 7 | export const Route = createRoute({ 8 | getParentRoute: () => RootRoute, 9 | path: '/', 10 | component: () => Home, 11 | }); 12 | 13 | @Component({ 14 | selector: 'home', 15 | template: ` 16 | Hello from TanStack Router 17 | `, 18 | }) 19 | export class Home {} 20 | -------------------------------------------------------------------------------- /src/app/login.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | import { 3 | createRoute, 4 | injectRouter, 5 | redirect, 6 | } from 'tanstack-angular-router-experimental'; 7 | import { AuthState } from './auth-state'; 8 | 9 | import { Route as RootRoute } from './root.route'; 10 | 11 | export const Route = createRoute({ 12 | getParentRoute: () => RootRoute, 13 | path: 'login', 14 | component: () => Login, 15 | validateSearch: (search) => ({ redirect: search['redirect'] as string }), 16 | beforeLoad: ({ search }) => { 17 | const authState = inject(AuthState); 18 | if (authState.isAuthenticated()) { 19 | console.log('already logged in'); 20 | throw redirect({ to: search.redirect || '/' }); 21 | } 22 | }, 23 | }); 24 | 25 | @Component({ 26 | selector: 'login', 27 | template: ` 28 |

Login

29 | 30 |
31 | 37 | 38 |
39 | `, 40 | }) 41 | export class Login { 42 | authState = inject(AuthState); 43 | router = injectRouter(); 44 | search = Route.routeSearch(); 45 | 46 | onSubmit(event: SubmitEvent) { 47 | event.preventDefault(); 48 | const form = event.target as HTMLFormElement; 49 | const formData = new FormData(form); 50 | const username = formData.get('username'); 51 | if (!username || typeof username !== 'string') return; 52 | 53 | this.authState.username.set(username); 54 | this.router.navigate({ to: this.search().redirect || '/' }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/parent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { 4 | createRoute, 5 | injectRouter, 6 | Link, 7 | Outlet, 8 | } from 'tanstack-angular-router-experimental'; 9 | 10 | import { Route as RootRoute } from './root.route'; 11 | 12 | export const Route = createRoute({ 13 | getParentRoute: () => RootRoute, 14 | path: 'parent', 15 | component: () => Parent, 16 | }); 17 | 18 | @Component({ 19 | selector: 'parent', 20 | imports: [Outlet, Link], 21 | template: ` 22 | Parent - 23 | Child 24 | | 25 | Child 1 26 | | 27 | Child 2 28 |
29 | 30 | 31 | `, 32 | styles: [ 33 | ` 34 | a { 35 | text-decoration: underline; 36 | } 37 | 38 | a[data-active='true'] { 39 | font-weight: bold; 40 | padding: 0.5rem; 41 | border: 1px solid red; 42 | } 43 | `, 44 | ], 45 | }) 46 | export class Parent { 47 | router = injectRouter(); 48 | } 49 | -------------------------------------------------------------------------------- /src/app/protected.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | import { createRoute, redirect } from 'tanstack-angular-router-experimental'; 3 | import { AuthState } from './auth-state'; 4 | import { Route as RootRoute } from './root.route'; 5 | 6 | export const Route = createRoute({ 7 | getParentRoute: () => RootRoute, 8 | path: 'protected', 9 | component: () => Protected, 10 | beforeLoad: ({ location }) => { 11 | const authState = inject(AuthState); 12 | if (!authState.isAuthenticated()) { 13 | throw redirect({ 14 | to: '/login', 15 | search: { 16 | redirect: location.href, 17 | }, 18 | }); 19 | } 20 | }, 21 | }); 22 | 23 | @Component({ 24 | selector: 'protected', 25 | template: ` 26 |

This is protected route

27 | `, 28 | }) 29 | export class Protected {} 30 | -------------------------------------------------------------------------------- /src/app/root.route.ts: -------------------------------------------------------------------------------- 1 | import { createRootRoute } from 'tanstack-angular-router-experimental'; 2 | 3 | import { App } from './app'; 4 | 5 | export const Route = createRootRoute({ component: () => App }); 6 | -------------------------------------------------------------------------------- /src/app/router.ts: -------------------------------------------------------------------------------- 1 | import { TypedRouter } from 'tanstack-angular-router-experimental'; 2 | 3 | import { AboutRoute } from './about/about.route'; 4 | import { Route as ChildRoute } from './child'; 5 | import { Route as HomeRoute } from './home'; 6 | import { Route as LoginRoute } from './login'; 7 | import { Route as ParentRoute } from './parent'; 8 | import { Route as ProtectedRoute } from './protected'; 9 | import { Route as RootRoute } from './root.route'; 10 | 11 | export const routeTree = RootRoute.addChildren([ 12 | HomeRoute, 13 | AboutRoute, 14 | ParentRoute.addChildren([ChildRoute]), 15 | ProtectedRoute, 16 | LoginRoute, 17 | ]); 18 | 19 | export type router = TypedRouter; 20 | 21 | declare module '@tanstack/router-core' { 22 | interface Register { 23 | // This infers the type of our router and registers it across your entire project 24 | router: router; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/spinner.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'spinner', 5 | template: ` 6 | 12 | 13 | 22 | 23 | 24 | 32 | 33 | 34 | 43 | 44 | 45 | `, 46 | }) 47 | export class Spinner {} 48 | -------------------------------------------------------------------------------- /src/app/todos-client.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { inject, Injectable } from '@angular/core'; 3 | 4 | @Injectable({ providedIn: 'root' }) 5 | export class TodosClient { 6 | private http = inject(HttpClient); 7 | 8 | getTodo(id: number) { 9 | return this.http.get<{ id: string }>( 10 | `https://jsonplaceholder.typicode.com/todos/${id}` 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TanstackRouterAngular 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { App } from './app/app'; 3 | import { appConfig } from './app/app.config'; 4 | 5 | bootstrapApplication(App, appConfig).catch((err) => console.error(err)); 6 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.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 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "bundler", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022", 20 | "paths": { 21 | "tanstack-angular-router-experimental": [ 22 | "./projects/router/src/public-api.ts" 23 | ] 24 | } 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsr.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "routesDirectory": "./projects/file-routing/src/routes", 3 | "generatedRouteTree": "./projects/file-routing/src/routeTree.gen.ts", 4 | "routeFileIgnorePrefix": "-", 5 | "quoteStyle": "single" 6 | } 7 | --------------------------------------------------------------------------------