@benjavicente/angular-router-experimental 1.142.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/dist/fesm2022/tanstack-angular-router-experimental-experimental.mjs +920 -0
  4. package/dist/fesm2022/tanstack-angular-router-experimental.mjs +4131 -0
  5. package/dist/types/tanstack-angular-router-experimental-experimental.d.ts +110 -0
  6. package/dist/types/tanstack-angular-router-experimental.d.ts +733 -0
  7. package/experimental/injectRouteErrorHandler.ts +51 -0
  8. package/experimental/public_api.ts +8 -0
  9. package/package.json +98 -0
  10. package/src/DefaultNotFound.ts +9 -0
  11. package/src/Link.ts +352 -0
  12. package/src/Match.ts +338 -0
  13. package/src/Matches.ts +37 -0
  14. package/src/RouterProvider.ts +162 -0
  15. package/src/document/build-match-managed-document.ts +308 -0
  16. package/src/document/document-dehydration.ts +27 -0
  17. package/src/document/document-equality.ts +29 -0
  18. package/src/document/document-router-token.ts +6 -0
  19. package/src/document/index.ts +33 -0
  20. package/src/document/install-unified-document-sync.ts +108 -0
  21. package/src/document/managed-document-types.ts +36 -0
  22. package/src/document/managed-dom.ts +307 -0
  23. package/src/document/provide-tanstack-body-managed-tags.ts +78 -0
  24. package/src/document/provide-tanstack-document-title.ts +59 -0
  25. package/src/document/provide-tanstack-document.ts +62 -0
  26. package/src/document/provide-tanstack-head-managed-tags.ts +63 -0
  27. package/src/fileRoute.ts +232 -0
  28. package/src/index.ts +173 -0
  29. package/src/injectBlocker.ts +196 -0
  30. package/src/injectCanGoBack.ts +11 -0
  31. package/src/injectErrorState.ts +21 -0
  32. package/src/injectIntersectionObserver.ts +28 -0
  33. package/src/injectLoaderData.ts +49 -0
  34. package/src/injectLoaderDeps.ts +45 -0
  35. package/src/injectLocation.ts +38 -0
  36. package/src/injectMatch.ts +122 -0
  37. package/src/injectMatchRoute.ts +58 -0
  38. package/src/injectMatches.ts +79 -0
  39. package/src/injectNavigate.ts +24 -0
  40. package/src/injectParams.ts +71 -0
  41. package/src/injectRouteContext.ts +31 -0
  42. package/src/injectRouter.ts +17 -0
  43. package/src/injectRouterState.ts +53 -0
  44. package/src/injectSearch.ts +71 -0
  45. package/src/injectStore.ts +87 -0
  46. package/src/matchInjectorToken.ts +23 -0
  47. package/src/renderer/injectIsCatchingError.ts +40 -0
  48. package/src/renderer/injectRender.ts +69 -0
  49. package/src/route.ts +641 -0
  50. package/src/router.ts +141 -0
  51. package/src/routerInjectionToken.ts +24 -0
  52. package/src/routerStores.ts +107 -0
  53. package/src/ssr-scroll-restoration.ts +48 -0
  54. package/src/transitioner.ts +255 -0
@@ -0,0 +1,51 @@
1
+ import { DestroyRef, inject, untracked } from '@angular/core'
2
+ import { injectMatch, injectRouter } from '@benjavicente/angular-router-experimental'
3
+ import type { AnyRouter, FromPathOption, RegisteredRouter } from '@benjavicente/router-core'
4
+
5
+ /**
6
+ * EXPERIMENTAL
7
+ *
8
+ * While in other adapters you can use build-in error boundaries,
9
+ * Angular does not provide any. As an workarraound, we export a function
10
+ * to simulate an error boundary by changing the router state to show
11
+ * the error component.
12
+ *
13
+ * Note that an equivalent for suspense can't exist since we can't restore
14
+ * the component state when the promise is resolved as is with other adapters.
15
+ */
16
+ export function injectRouteErrorHandler<
17
+ TRouter extends AnyRouter = RegisteredRouter,
18
+ TDefaultFrom extends string = string,
19
+ >(options: { from?: FromPathOption<TRouter, TDefaultFrom> }) {
20
+ const router = injectRouter()
21
+ const match = injectMatch({ from: options.from })
22
+
23
+ let destroyed = false
24
+
25
+ inject(DestroyRef).onDestroy(() => {
26
+ destroyed = true
27
+ })
28
+
29
+ return {
30
+ throw: (error: Error) => {
31
+ if (destroyed) {
32
+ console.warn(
33
+ 'Attempted to throw error to route after it has been destroyed',
34
+ )
35
+ return
36
+ }
37
+
38
+ const matchId = untracked(match).id
39
+
40
+ router.updateMatch(matchId, (match) => {
41
+ return {
42
+ ...match,
43
+ error,
44
+ status: 'error',
45
+ isFetching: false,
46
+ updatedAt: Date.now(),
47
+ }
48
+ })
49
+ },
50
+ }
51
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @experimental
3
+ *
4
+ * This entrypoint contains experimental APIs that may change or be removed
5
+ * in future versions. Use with caution.
6
+ */
7
+
8
+ export { injectRouteErrorHandler } from './injectRouteErrorHandler'
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "@benjavicente/angular-router-experimental",
3
+ "version": "1.142.11",
4
+ "description": "Modern and scalable routing for Angular applications",
5
+ "author": "Tanner Linsley",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TanStack/router.git",
10
+ "directory": "packages/angular-router-experimental"
11
+ },
12
+ "homepage": "https://tanstack.com/router",
13
+ "funding": {
14
+ "type": "github",
15
+ "url": "https://github.com/sponsors/tannerlinsley"
16
+ },
17
+ "keywords": [
18
+ "angular",
19
+ "location",
20
+ "router",
21
+ "routing",
22
+ "async",
23
+ "async router",
24
+ "typescript"
25
+ ],
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "types": "./dist/types/tanstack-angular-router-experimental.d.ts",
29
+ "exports": {
30
+ "./package.json": {
31
+ "default": "./package.json"
32
+ },
33
+ ".": {
34
+ "types": "./dist/types/tanstack-angular-router-experimental.d.ts",
35
+ "require": null,
36
+ "default": "./dist/fesm2022/tanstack-angular-router-experimental.mjs"
37
+ },
38
+ "./experimental": {
39
+ "types": "./dist/types/tanstack-angular-router-experimental-experimental.d.ts",
40
+ "require": null,
41
+ "default": "./dist/fesm2022/tanstack-angular-router-experimental-experimental.mjs"
42
+ }
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "src",
47
+ "experimental"
48
+ ],
49
+ "engines": {
50
+ "node": ">=20.19"
51
+ },
52
+ "dependencies": {
53
+ "isbot": "^5.1.22",
54
+ "tslib": "^2.3.0",
55
+ "@benjavicente/history": "1.161.6",
56
+ "@benjavicente/router-core": "1.168.9"
57
+ },
58
+ "devDependencies": {
59
+ "@analogjs/vitest-angular": "^2.2.1",
60
+ "@angular/common": "^21.2.6",
61
+ "@angular/compiler": "^21.2.6",
62
+ "@angular/compiler-cli": "^21.2.6",
63
+ "@angular/core": "^21.2.6",
64
+ "@angular/platform-browser": "^21.2.6",
65
+ "@oxc-angular/vite": "^0.0.22",
66
+ "@testing-library/angular": "^19.0.0",
67
+ "@tanstack/vite-config": "0.5.2",
68
+ "@testing-library/jest-dom": "^6.6.3",
69
+ "@types/node": ">=20",
70
+ "combinate": "^1.1.11",
71
+ "jsdom": "^27.4.0",
72
+ "rxjs": "~7.8.0",
73
+ "vibe-rules": "^0.2.57",
74
+ "vite": "^8.0.0",
75
+ "vite-plugin-dts": "^4.5.4",
76
+ "vitest": "^4.0.17",
77
+ "zod": "^3.24.2"
78
+ },
79
+ "peerDependencies": {
80
+ "@angular/common": "^21.2.6",
81
+ "@angular/core": "^21.2.6"
82
+ },
83
+ "scripts": {
84
+ "clean": "rimraf ./dist && rimraf ./coverage",
85
+ "test:eslint": "eslint",
86
+ "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
87
+ "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js -p tsconfig.legacy.json",
88
+ "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js -p tsconfig.legacy.json",
89
+ "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js -p tsconfig.legacy.json",
90
+ "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js -p tsconfig.legacy.json",
91
+ "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js -p tsconfig.legacy.json",
92
+ "test:types:ts59": "tsc -p tsconfig.legacy.json",
93
+ "test:unit": "vitest",
94
+ "test:unit:dev": "vitest --watch",
95
+ "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
96
+ "build": "vite build --config vite.lib.config.ts"
97
+ }
98
+ }
@@ -0,0 +1,9 @@
1
+ import { ChangeDetectionStrategy, Component } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'tanstack-angular-router-default-no-found',
5
+ template: `<p>Not found</p>`,
6
+ changeDetection: ChangeDetectionStrategy.OnPush,
7
+ host: { style: 'display: contents;' },
8
+ })
9
+ export class DefaultNotFoundComponent {}
package/src/Link.ts ADDED
@@ -0,0 +1,352 @@
1
+ import {
2
+ DestroyRef,
3
+ Directive,
4
+ ElementRef,
5
+ Renderer2,
6
+ afterNextRender,
7
+ computed,
8
+ effect,
9
+ inject,
10
+ input,
11
+ signal,
12
+ untracked,
13
+ } from '@angular/core'
14
+ import {
15
+ AnyRouter,
16
+ LinkOptions as CoreLinkOptions,
17
+ RegisteredRouter,
18
+ RoutePaths,
19
+ deepEqual,
20
+ exactPathTest,
21
+ preloadWarning,
22
+ removeTrailingSlash,
23
+ } from '@benjavicente/router-core'
24
+ import { injectLocation } from './injectLocation'
25
+ import { injectRouter } from './injectRouter'
26
+ import { injectIntersectionObserver } from './injectIntersectionObserver'
27
+
28
+ @Directive({
29
+ selector: 'a[link]',
30
+ exportAs: 'link',
31
+ standalone: true,
32
+ host: {
33
+ '[href]': 'hrefOption()?.href',
34
+ '(click)': 'handleClick($event)',
35
+ '(focus)': 'handleFocus()',
36
+ '(mouseenter)': 'handleEnter($event)',
37
+ '(mouseover)': 'handleEnter($event)',
38
+ '(mouseleave)': 'handleLeave($event)',
39
+ '[attr.target]': 'target()',
40
+ '[attr.role]': 'disabled() ? "link" : undefined',
41
+ '[attr.aria-disabled]': 'disabled()',
42
+ '[attr.data-status]': 'isActive() ? "active" : undefined',
43
+ '[attr.aria-current]': 'isActive() ? "page" : undefined',
44
+ '[attr.data-transitioning]':
45
+ 'isTransitioning() ? "transitioning" : undefined',
46
+ '[class]': 'isActiveProps()?.class',
47
+ '[style]': 'isActiveProps()?.style',
48
+ },
49
+ })
50
+ export class Link<
51
+ TRouter extends AnyRouter = RegisteredRouter,
52
+ TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
53
+ TTo extends string | undefined = '.',
54
+ TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
55
+ TMaskTo extends string = '.',
56
+ > {
57
+ passiveEvents = injectPasiveEvents(() => ({
58
+ touchstart: this.handleTouchStart,
59
+ }))
60
+
61
+ options = input.required<
62
+ LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
63
+ >({ alias: 'link' })
64
+
65
+ protected router = injectRouter()
66
+ protected isTransitioning = signal(false)
67
+
68
+ protected from = computed(() =>
69
+ untracked(() => this.options().from),
70
+ )
71
+
72
+ protected disabled = computed(() => this._options().disabled ?? false)
73
+ protected target = computed(() => this._options().target)
74
+
75
+ protected _options = computed<
76
+ LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
77
+ >(() => {
78
+ return {
79
+ ...this.options(),
80
+ from: this.from(),
81
+ }
82
+ })
83
+
84
+ protected nextLocation = computed(() => {
85
+ const currentLocation = this.location()
86
+ return this.router.buildLocation({
87
+ _fromLocation: currentLocation,
88
+ ...this._options(),
89
+ } as any)
90
+ })
91
+
92
+ protected hrefOption = computed(() => {
93
+ if (this._options().disabled) {
94
+ return undefined
95
+ }
96
+
97
+ const location = this.nextLocation().maskedLocation ?? this.nextLocation()
98
+ const external = location.external
99
+ const href = external
100
+ ? location.publicHref
101
+ : this.router.history.createHref(location.publicHref) || '/'
102
+
103
+ return { href, external }
104
+ })
105
+
106
+ protected externalLink = computed(() => {
107
+ const hrefOption = this.hrefOption()
108
+ if (hrefOption?.external) {
109
+ return hrefOption.href
110
+ }
111
+ try {
112
+ new URL(this.options()['to'] as any)
113
+ return this.options()['to']
114
+ } catch { }
115
+ return undefined
116
+ })
117
+
118
+ protected preload = computed(() => {
119
+ if (this.options()['reloadDocument']) {
120
+ return false
121
+ }
122
+ return this.options()['preload'] ?? this.router.options.defaultPreload
123
+ })
124
+
125
+ protected preloadDelay = computed(() => {
126
+ return (
127
+ this.options()['preloadDelay'] ??
128
+ this.router.options.defaultPreloadDelay ??
129
+ 0
130
+ )
131
+ })
132
+
133
+ protected location = injectLocation<TRouter>()
134
+
135
+ protected isActiveProps = computed(() => {
136
+ const opts = this.options()
137
+ const isActive = this.isActive()
138
+ const props = isActive ? opts.activeProps : opts.inactiveProps
139
+ if (!props || typeof props !== 'object') return undefined
140
+ return props
141
+ })
142
+
143
+ protected isActive = computed(() => {
144
+ if (this.externalLink()) return false
145
+
146
+ const options = this.options()
147
+ const activeOptions = options.activeOptions
148
+
149
+ if (activeOptions?.exact) {
150
+ const testExact = exactPathTest(
151
+ this.location().pathname,
152
+ this.nextLocation().pathname,
153
+ this.router.basepath,
154
+ )
155
+ if (!testExact) {
156
+ return false
157
+ }
158
+ } else {
159
+ const currentPathSplit = removeTrailingSlash(
160
+ this.location().pathname,
161
+ this.router.basepath,
162
+ )
163
+ const nextPathSplit = removeTrailingSlash(
164
+ this.nextLocation().pathname,
165
+ this.router.basepath,
166
+ )
167
+
168
+ const pathIsFuzzyEqual =
169
+ currentPathSplit.startsWith(nextPathSplit) &&
170
+ (currentPathSplit.length === nextPathSplit.length ||
171
+ currentPathSplit[nextPathSplit.length] === '/')
172
+
173
+ if (!pathIsFuzzyEqual) {
174
+ return false
175
+ }
176
+ }
177
+
178
+ if (activeOptions?.includeSearch ?? true) {
179
+ const searchTest = deepEqual(
180
+ this.location().search,
181
+ this.nextLocation().search,
182
+ {
183
+ partial: !activeOptions?.exact,
184
+ ignoreUndefined: !activeOptions?.explicitUndefined,
185
+ },
186
+ )
187
+ if (!searchTest) {
188
+ return false
189
+ }
190
+ }
191
+
192
+ if (activeOptions?.includeHash) {
193
+ return this.location().hash === this.nextLocation().hash
194
+ }
195
+ return true
196
+ })
197
+
198
+ protected doPreload = () => {
199
+ this.router.preloadRoute(this.options() as any).catch((err: any) => {
200
+ console.warn(err)
201
+ console.warn(preloadWarning)
202
+ })
203
+ }
204
+
205
+ protected preloadViewportIoCallback = (
206
+ entry: IntersectionObserverEntry | undefined,
207
+ ) => {
208
+ if (entry?.isIntersecting) {
209
+ this.doPreload()
210
+ }
211
+ }
212
+
213
+ private viewportPreloader = injectIntersectionObserver(
214
+ this.preloadViewportIoCallback,
215
+ { rootMargin: '100px' },
216
+ () => !!this._options().disabled || !(this.preload() === 'viewport'),
217
+ )
218
+
219
+ private hasRenderFetched = false
220
+ private rendererPreloader = effect(() => {
221
+ if (this.hasRenderFetched) return
222
+
223
+ if (!this._options().disabled && this.preload() === 'render') {
224
+ this.doPreload()
225
+ this.hasRenderFetched = true
226
+ }
227
+ })
228
+
229
+ protected handleClick = (event: MouseEvent) => {
230
+ const elementTarget = (
231
+ event.currentTarget as HTMLAnchorElement | SVGAElement
232
+ ).getAttribute('target')
233
+ const target = this._options().target
234
+ const effectiveTarget = target !== undefined ? target : elementTarget
235
+
236
+ if (
237
+ !this._options().disabled &&
238
+ !isCtrlEvent(event) &&
239
+ !event.defaultPrevented &&
240
+ (!effectiveTarget || effectiveTarget === '_self') &&
241
+ event.button === 0
242
+ ) {
243
+ event.preventDefault()
244
+
245
+ this.isTransitioning.set(true)
246
+
247
+ const unsub = this.router.subscribe('onResolved', () => {
248
+ unsub()
249
+ this.isTransitioning.set(false)
250
+ })
251
+
252
+ this.router.navigate(this._options())
253
+ }
254
+ }
255
+
256
+ protected handleFocus = () => {
257
+ if (this._options().disabled) return
258
+ if (this.preload()) {
259
+ this.doPreload()
260
+ }
261
+ }
262
+
263
+ protected handleTouchStart = () => {
264
+ if (this._options().disabled) return
265
+ if (this.preload()) {
266
+ this.doPreload()
267
+ }
268
+ }
269
+
270
+ protected handleEnter = (event: MouseEvent) => {
271
+ if (this._options().disabled) return
272
+ const eventTarget = (event.currentTarget || {}) as EventTargetWithPreloadTimeout
273
+
274
+ if (this.preload()) {
275
+ if (eventTarget.preloadTimeout) {
276
+ return
277
+ }
278
+
279
+ eventTarget.preloadTimeout = setTimeout(() => {
280
+ eventTarget.preloadTimeout = null
281
+ this.doPreload()
282
+ }, this.preloadDelay())
283
+ }
284
+ }
285
+
286
+ protected handleLeave = (event: MouseEvent) => {
287
+ if (this._options().disabled) return
288
+ const eventTarget = (event.currentTarget || {}) as EventTargetWithPreloadTimeout
289
+
290
+ if (eventTarget.preloadTimeout) {
291
+ clearTimeout(eventTarget.preloadTimeout)
292
+ eventTarget.preloadTimeout = null
293
+ }
294
+ }
295
+ }
296
+
297
+ interface ActiveLinkProps {
298
+ class?: string
299
+ style?: string
300
+ }
301
+
302
+ interface ActiveLinkOptionProps {
303
+ activeProps?: ActiveLinkProps
304
+ inactiveProps?: ActiveLinkProps
305
+ }
306
+
307
+ type EventTargetWithPreloadTimeout = EventTarget & {
308
+ preloadTimeout?: ReturnType<typeof setTimeout> | null
309
+ }
310
+
311
+ export type LinkOptions<
312
+ TRouter extends AnyRouter = RegisteredRouter,
313
+ TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
314
+ TTo extends string | undefined = '.',
315
+ TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
316
+ TMaskTo extends string = '.',
317
+ > = CoreLinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
318
+ ActiveLinkOptionProps
319
+
320
+ function isCtrlEvent(e: MouseEvent) {
321
+ return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
322
+ }
323
+
324
+ // Angular does not provide by default passive events listeners
325
+ // to some events like React, and does not support a pasive options
326
+ // in the template, so we attach the pasive events manually here
327
+
328
+ type PassiveEvents = {
329
+ touchstart: (event: TouchEvent) => void
330
+ }
331
+
332
+ function injectPasiveEvents(passiveEvents: () => PassiveEvents) {
333
+ const element = inject(ElementRef).nativeElement
334
+ const destroyRef = inject(DestroyRef)
335
+ const renderer = inject(Renderer2)
336
+ const cleanups: Array<() => void> = []
337
+
338
+ afterNextRender(() => {
339
+ for (const [event, handler] of Object.entries(passiveEvents())) {
340
+ const cleanup = renderer.listen(element, event, handler, {
341
+ passive: true,
342
+ })
343
+ cleanups.push(cleanup)
344
+ }
345
+ })
346
+
347
+ destroyRef.onDestroy(() => {
348
+ while (cleanups.length) {
349
+ cleanups.pop()?.()
350
+ }
351
+ })
352
+ }