@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
package/src/router.ts ADDED
@@ -0,0 +1,141 @@
1
+ import { RouterCore } from '@benjavicente/router-core'
2
+ import { getStoreFactory } from './routerStores'
3
+ import type {
4
+ AnyRoute,
5
+ RouterOptions,
6
+ TrailingSlashOption,
7
+ } from '@benjavicente/router-core'
8
+ import type { EnvironmentInjector } from '@angular/core'
9
+ import type { RouterHistory } from '@benjavicente/history'
10
+ import type { ErrorRouteComponent, RouteComponent } from './route'
11
+
12
+ export type AngularInjectFn = EnvironmentInjector['get']
13
+
14
+ export type AngularRouterContext = {
15
+ inject: AngularInjectFn
16
+ }
17
+
18
+ type InferAngularRouterContext<TRouteTree extends AnyRoute> =
19
+ TRouteTree['types']['routerContext']
20
+
21
+ type AngularRouterContextInput<TContext> = Omit<
22
+ TContext,
23
+ keyof AngularRouterContext
24
+ > &
25
+ Partial<Pick<TContext, Extract<keyof TContext, keyof AngularRouterContext>>>
26
+
27
+ type AngularRouterContextOptions<TRouteTree extends AnyRoute> =
28
+ {} extends Omit<InferAngularRouterContext<TRouteTree>, keyof AngularRouterContext>
29
+ ? {
30
+ context?: AngularRouterContextInput<InferAngularRouterContext<TRouteTree>>
31
+ }
32
+ : {
33
+ context: AngularRouterContextInput<InferAngularRouterContext<TRouteTree>>
34
+ }
35
+
36
+ type AngularRouterConstructorOptions<
37
+ TRouteTree extends AnyRoute,
38
+ TTrailingSlashOption extends TrailingSlashOption,
39
+ TDefaultStructuralSharingOption extends boolean,
40
+ TRouterHistory extends RouterHistory,
41
+ TDehydrated extends Record<string, any>,
42
+ > = Omit<
43
+ RouterOptions<
44
+ TRouteTree,
45
+ TTrailingSlashOption,
46
+ TDefaultStructuralSharingOption,
47
+ TRouterHistory,
48
+ TDehydrated
49
+ >,
50
+ 'context' | 'serializationAdapters' | 'defaultSsr'
51
+ > &
52
+ AngularRouterContextOptions<TRouteTree>
53
+
54
+ export type CreateRouterFn = <
55
+ TRouteTree extends AnyRoute,
56
+ TTrailingSlashOption extends TrailingSlashOption = 'never',
57
+ TDefaultStructuralSharingOption extends boolean = false,
58
+ TRouterHistory extends RouterHistory = RouterHistory,
59
+ TDehydrated extends Record<string, any> = Record<string, any>,
60
+ >(
61
+ options: undefined extends number
62
+ ? 'strictNullChecks must be enabled in tsconfig.json'
63
+ : AngularRouterConstructorOptions<
64
+ TRouteTree,
65
+ TTrailingSlashOption,
66
+ TDefaultStructuralSharingOption,
67
+ TRouterHistory,
68
+ TDehydrated
69
+ >,
70
+ ) => Router<
71
+ TRouteTree,
72
+ TTrailingSlashOption,
73
+ TDefaultStructuralSharingOption,
74
+ TRouterHistory,
75
+ TDehydrated
76
+ >
77
+
78
+ declare module '@benjavicente/router-core' {
79
+ export interface RouterOptionsExtensions {
80
+ /**
81
+ * The default `component` a route should use if no component is provided.
82
+ *
83
+ * @default Outlet
84
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/solid/api/router/RouterOptionsType#defaultcomponent-property)
85
+ */
86
+ defaultComponent?: RouteComponent
87
+ /**
88
+ * The default `errorComponent` a route should use if no error component is provided.
89
+ *
90
+ * @default ErrorComponent
91
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/solid/api/router/RouterOptionsType#defaulterrorcomponent-property)
92
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#handling-errors-with-routeoptionserrorcomponent)
93
+ */
94
+ defaultErrorComponent?: ErrorRouteComponent
95
+ /**
96
+ * The default `pendingComponent` a route should use if no pending component is provided.
97
+ *
98
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/solid/api/router/RouterOptionsType#defaultpendingcomponent-property)
99
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#showing-a-pending-component)
100
+ */
101
+ defaultPendingComponent?: RouteComponent
102
+ /**
103
+ * The default `notFoundComponent` a route should use if no notFound component is provided.
104
+ *
105
+ * @default NotFound
106
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/solid/api/router/RouterOptionsType#defaultnotfoundcomponent-property)
107
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/solid/guide/not-found-errors#default-router-wide-not-found-handling)
108
+ */
109
+ defaultNotFoundComponent?: RouteComponent
110
+ }
111
+ }
112
+
113
+ export const createRouter: CreateRouterFn = (options: any) => {
114
+ return new Router(options)
115
+ }
116
+
117
+ export class Router<
118
+ in out TRouteTree extends AnyRoute,
119
+ in out TTrailingSlashOption extends TrailingSlashOption = 'never',
120
+ in out TDefaultStructuralSharingOption extends boolean = false,
121
+ in out TRouterHistory extends RouterHistory = RouterHistory,
122
+ in out TDehydrated extends Record<string, any> = Record<string, any>,
123
+ > extends RouterCore<
124
+ TRouteTree,
125
+ TTrailingSlashOption,
126
+ TDefaultStructuralSharingOption,
127
+ TRouterHistory,
128
+ TDehydrated
129
+ > {
130
+ constructor(
131
+ options: AngularRouterConstructorOptions<
132
+ TRouteTree,
133
+ TTrailingSlashOption,
134
+ TDefaultStructuralSharingOption,
135
+ TRouterHistory,
136
+ TDehydrated
137
+ >,
138
+ ) {
139
+ super(options as any, getStoreFactory)
140
+ }
141
+ }
@@ -0,0 +1,24 @@
1
+ import * as Angular from '@angular/core'
2
+ import type { AnyRouter } from '@benjavicente/router-core'
3
+
4
+ declare global {
5
+ interface Window {
6
+ __TSR_ROUTER_INJECTION_KEY__?: Angular.InjectionToken<AnyRouter>
7
+ }
8
+ }
9
+
10
+ const routerInjectionKey = new Angular.InjectionToken<AnyRouter>('ROUTER')
11
+
12
+ export function getRouterInjectionKey() {
13
+ if (typeof document === 'undefined') {
14
+ return routerInjectionKey
15
+ }
16
+
17
+ if (window.__TSR_ROUTER_INJECTION_KEY__) {
18
+ return window.__TSR_ROUTER_INJECTION_KEY__
19
+ }
20
+
21
+ window.__TSR_ROUTER_INJECTION_KEY__ = routerInjectionKey as any
22
+
23
+ return routerInjectionKey
24
+ }
@@ -0,0 +1,107 @@
1
+ import * as Angular from '@angular/core'
2
+ import {
3
+ createNonReactiveMutableStore,
4
+ createNonReactiveReadonlyStore,
5
+ } from '@benjavicente/router-core'
6
+ import { isServer } from '@benjavicente/router-core/isServer'
7
+ import type {
8
+ AnyRoute,
9
+ GetStoreConfig,
10
+ RouterReadableStore,
11
+ RouterStores,
12
+ RouterWritableStore,
13
+ } from '@benjavicente/router-core'
14
+
15
+ declare module '@benjavicente/router-core' {
16
+ // eslint-disable-next-line unused-imports/no-unused-vars -- generic must match upstream `RouterStores<TRouteTree>` for augmentation
17
+ export interface RouterStores<in out TRouteTree extends AnyRoute> {
18
+ childMatchIdByRouteId: RouterReadableStore<Record<string, string>>
19
+ pendingRouteIds: RouterReadableStore<Record<string, boolean>>
20
+ }
21
+ }
22
+
23
+ function initRouterStores(
24
+ stores: RouterStores<AnyRoute>,
25
+ createReadonlyStore: <TValue>(
26
+ read: () => TValue,
27
+ ) => RouterReadableStore<TValue>,
28
+ ) {
29
+ stores.childMatchIdByRouteId = createReadonlyStore(() => {
30
+ const ids = stores.matchesId.state
31
+ const result: Record<string, string> = {}
32
+
33
+ for (let i = 0; i < ids.length - 1; i++) {
34
+ const matchId = ids[i]
35
+ const childId = ids[i + 1]
36
+ if (matchId === undefined || childId === undefined) continue
37
+ const parentStore = stores.activeMatchStoresById.get(matchId)
38
+ if (parentStore?.routeId) {
39
+ result[parentStore.routeId] = childId
40
+ }
41
+ }
42
+
43
+ return result
44
+ })
45
+
46
+ stores.pendingRouteIds = createReadonlyStore(() => {
47
+ const ids = stores.pendingMatchesId.state
48
+ const result: Record<string, boolean> = {}
49
+
50
+ for (const id of ids) {
51
+ const store = stores.pendingMatchStoresById.get(id)
52
+ if (store?.routeId) {
53
+ result[store.routeId] = true
54
+ }
55
+ }
56
+
57
+ return result
58
+ })
59
+ }
60
+
61
+ function createAngularMutableStore<TValue>(
62
+ initialValue: TValue,
63
+ ): RouterWritableStore<TValue> {
64
+ const signal = Angular.signal(initialValue)
65
+
66
+ return {
67
+ get state() {
68
+ return signal()
69
+ },
70
+ setState(updater) {
71
+ signal.update(updater)
72
+ },
73
+ }
74
+ }
75
+
76
+ function createAngularReadonlyStore<TValue>(
77
+ read: () => TValue,
78
+ ): RouterReadableStore<TValue> {
79
+ const computed = Angular.computed(read)
80
+
81
+ return {
82
+ get state() {
83
+ return computed()
84
+ },
85
+ }
86
+ }
87
+
88
+ export const getStoreFactory: GetStoreConfig = (opts) => {
89
+ const useNonReactive =
90
+ typeof isServer === 'boolean' ? isServer : !!opts.isServer
91
+ if (useNonReactive) {
92
+ return {
93
+ createMutableStore: createNonReactiveMutableStore,
94
+ createReadonlyStore: createNonReactiveReadonlyStore,
95
+ batch: (fn) => fn(),
96
+ init: (stores) =>
97
+ initRouterStores(stores, createNonReactiveReadonlyStore),
98
+ }
99
+ }
100
+
101
+ return {
102
+ createMutableStore: createAngularMutableStore,
103
+ createReadonlyStore: createAngularReadonlyStore,
104
+ batch: (fn) => fn(),
105
+ init: (stores) => initRouterStores(stores, createAngularReadonlyStore),
106
+ }
107
+ }
@@ -0,0 +1,48 @@
1
+ import { DOCUMENT } from '@angular/common'
2
+ import { EnvironmentInjector, afterNextRender, inject } from '@angular/core'
3
+ import { getScrollRestorationScriptForRouter } from '@benjavicente/router-core/scroll-restoration-script'
4
+ import { injectRouter } from './injectRouter'
5
+
6
+ const SSR_SCROLL_MARKER = 'data-tsr-scroll-restoration-inline'
7
+
8
+ /**
9
+ * Injects the same inline scroll-restore script Solid/React emit on SSR
10
+ * (`ScrollRestoration` + `ScriptOnce`), so hydration can align window scroll
11
+ * with sessionStorage before the client router runs.
12
+ */
13
+ export function injectSsrScrollRestorationScript(): void {
14
+ const router = injectRouter()
15
+ const doc = inject(DOCUMENT)
16
+ const envInjector = inject(EnvironmentInjector)
17
+
18
+ if (!router.isServer) {
19
+ return
20
+ }
21
+
22
+ afterNextRender(() => {
23
+ const enabled = router.options.scrollRestoration
24
+ if (!enabled) return
25
+ if (
26
+ typeof enabled === 'function' &&
27
+ !enabled({ location: router.latestLocation })
28
+ ) {
29
+ return
30
+ }
31
+ if (doc.querySelector(`script[${SSR_SCROLL_MARKER}]`)) {
32
+ return
33
+ }
34
+ const script = getScrollRestorationScriptForRouter(router)
35
+ if (!script) return
36
+ const el = doc.createElement('script')
37
+ el.setAttribute(SSR_SCROLL_MARKER, '')
38
+ el.className = '$tsr'
39
+ el.text = `${script};document.currentScript.remove()`
40
+ const nonce = router.options.ssr?.nonce
41
+ if (nonce) {
42
+ el.setAttribute('nonce', nonce)
43
+ }
44
+ ;(doc.head ?? doc.body).appendChild(el)
45
+ },
46
+ { injector: envInjector },
47
+ )
48
+ }
@@ -0,0 +1,255 @@
1
+ import * as Angular from '@angular/core'
2
+ import {
3
+ getLocationChangeInfo,
4
+ handleHashScroll,
5
+ trimPathRight,
6
+ } from '@benjavicente/router-core'
7
+ import { injectRouter } from './injectRouter'
8
+ import { injectStore } from './injectStore'
9
+ import type { AnyRouter } from '@benjavicente/router-core'
10
+
11
+ // Track mount state per router to avoid double-loading
12
+ let mountLoadForRouter: { router: AnyRouter | null; mounted: boolean } = {
13
+ router: null,
14
+ mounted: false,
15
+ }
16
+
17
+ /**
18
+ * Helper function that sets up router transition logic.
19
+ * This should be called from Matches component to set up:
20
+ * - router.startTransition
21
+ * - router.startViewTransition
22
+ * - History subscription
23
+ * - Router event watchers
24
+ *
25
+ * Must be called during component initialization.
26
+ *
27
+ * This is more complicated than the other adapters, since Angular
28
+ * does not have transition support and a mechanism to wait for the next tick.
29
+ */
30
+ export function injectTransitionerSetup() {
31
+ const router = injectRouter()
32
+ const environmentInjector = Angular.inject(Angular.EnvironmentInjector)
33
+
34
+ // Skip on server - no transitions needed
35
+ if (router.isServer) return
36
+
37
+ const destroyRef = Angular.inject(Angular.DestroyRef)
38
+ let destroyed = false
39
+ const isLoading = injectStore(router.stores.isLoading, (value) => value)
40
+
41
+ // Track if we're in a transition
42
+ const isTransitioning = injectStore(
43
+ router.stores.isTransitioning,
44
+ (value) => value,
45
+ )
46
+
47
+ // Track pending state changes
48
+ const hasPendingMatches = injectStore(
49
+ router.stores.hasPendingMatches,
50
+ (value) => value,
51
+ )
52
+ const status = injectStore(router.stores.status, (value) => value)
53
+ const location = injectStore(router.stores.location, (value) => value)
54
+ const resolvedLocation = injectStore(
55
+ router.stores.resolvedLocation,
56
+ (value) => value,
57
+ )
58
+
59
+ const isAnyPending = Angular.computed(
60
+ () => isLoading() || isTransitioning() || hasPendingMatches(),
61
+ )
62
+
63
+ const isPagePending = Angular.computed(
64
+ () => isLoading() || hasPendingMatches(),
65
+ )
66
+
67
+ // Track previous values for comparison using proper previous value tracking
68
+ const prevIsLoading = injectPrevious(() => isLoading())
69
+ const prevIsAnyPending = injectPrevious(() => isAnyPending())
70
+ const prevIsPagePending = injectPrevious(() => isPagePending())
71
+
72
+ // Implement startTransition similar to React/Solid
73
+ // Angular doesn't have a native startTransition like React 18, so we simulate it
74
+ router.startTransition = (fn: () => void | Promise<void>) => {
75
+ router.stores.isTransitioning.setState(() => true)
76
+
77
+ // Helper to end the transition
78
+ const endTransition = () => {
79
+ if (destroyed) return
80
+
81
+ // Use afterNextRender to ensure Angular has processed all change detection
82
+ // This is similar to Vue's nextTick approach
83
+ Angular.afterNextRender(
84
+ {
85
+ read: () => {
86
+ try {
87
+ router.stores.isTransitioning.setState(() => false)
88
+ } catch {
89
+ // Ignore errors if component is unmounted
90
+ }
91
+ },
92
+ },
93
+ { injector: environmentInjector },
94
+ )
95
+ }
96
+
97
+ const result = fn()
98
+
99
+ if (result instanceof Promise) {
100
+ result.finally(() => endTransition())
101
+ } else {
102
+ endTransition()
103
+ }
104
+ }
105
+
106
+ // Subscribe to location changes and try to load the new location
107
+ let unsubscribe: (() => void) | undefined
108
+
109
+ Angular.afterNextRender(() => {
110
+ unsubscribe = router.history.subscribe(router.load)
111
+
112
+ const nextLocation = router.buildLocation({
113
+ to: router.latestLocation.pathname,
114
+ search: true,
115
+ params: true,
116
+ hash: true,
117
+ state: true,
118
+ _includeValidateSearch: true,
119
+ })
120
+
121
+ if (
122
+ trimPathRight(router.latestLocation.href) !==
123
+ trimPathRight(nextLocation.href)
124
+ ) {
125
+ router.commitLocation({ ...nextLocation, replace: true })
126
+ }
127
+ })
128
+
129
+ // Track if component is mounted to prevent updates after unmount
130
+ const isMounted = Angular.signal(false)
131
+
132
+ Angular.afterNextRender(() => {
133
+ isMounted.set(true)
134
+ if (!isAnyPending()) {
135
+ if (status() === 'pending') {
136
+ router.stores.status.setState(() => 'idle')
137
+ router.stores.resolvedLocation.setState(() => location())
138
+ }
139
+ }
140
+ })
141
+
142
+ destroyRef.onDestroy(() => {
143
+ isMounted.set(false)
144
+ destroyed = true
145
+ if (unsubscribe) {
146
+ unsubscribe()
147
+ }
148
+ })
149
+
150
+ // Try to load the initial location
151
+ Angular.afterNextRender(() => {
152
+ if (
153
+ (typeof window !== 'undefined' && router.ssr) ||
154
+ (mountLoadForRouter.router === router && mountLoadForRouter.mounted)
155
+ ) {
156
+ return
157
+ }
158
+ mountLoadForRouter = { router, mounted: true }
159
+ const tryLoad = async () => {
160
+ try {
161
+ await router.load()
162
+ } catch (err) {
163
+ console.error(err)
164
+ }
165
+ }
166
+ tryLoad()
167
+ })
168
+
169
+ // Setup effects for emitting events
170
+ // All effects check isMounted to prevent updates after unmount
171
+
172
+ // Watch for onLoad event
173
+ Angular.effect(() => {
174
+ if (!isMounted()) return
175
+ try {
176
+ if (prevIsLoading() && !isLoading()) {
177
+ router.emit({
178
+ type: 'onLoad',
179
+ ...getLocationChangeInfo(
180
+ location(),
181
+ resolvedLocation(),
182
+ ),
183
+ })
184
+ }
185
+ } catch {
186
+ // Ignore errors if component is unmounted
187
+ }
188
+ })
189
+
190
+ // Watch for onBeforeRouteMount event
191
+ Angular.effect(() => {
192
+ if (!isMounted()) return
193
+ try {
194
+ if (prevIsPagePending() && !isPagePending()) {
195
+ router.emit({
196
+ type: 'onBeforeRouteMount',
197
+ ...getLocationChangeInfo(
198
+ location(),
199
+ resolvedLocation(),
200
+ ),
201
+ })
202
+ }
203
+ } catch {
204
+ // Ignore errors if component is unmounted
205
+ }
206
+ })
207
+
208
+ // Watch for onResolved event
209
+ Angular.effect(() => {
210
+ if (!isMounted()) return
211
+ try {
212
+ if (
213
+ prevIsAnyPending() &&
214
+ !isAnyPending() &&
215
+ status() === 'pending'
216
+ ) {
217
+ router.stores.status.setState(() => 'idle')
218
+ router.stores.resolvedLocation.setState(() => location())
219
+ }
220
+
221
+ // The router was pending and now it's not
222
+ if (prevIsAnyPending() && !isAnyPending()) {
223
+ const changeInfo = getLocationChangeInfo(
224
+ location(),
225
+ resolvedLocation(),
226
+ )
227
+ router.emit({
228
+ type: 'onResolved',
229
+ ...changeInfo,
230
+ })
231
+
232
+ if (changeInfo.hrefChanged) {
233
+ handleHashScroll(router)
234
+ }
235
+ }
236
+ } catch {
237
+ // Ignore errors if component is unmounted
238
+ }
239
+ })
240
+ }
241
+
242
+ export function injectPrevious<T>(
243
+ fn: () => NonNullable<T>,
244
+ ): Angular.Signal<T | null> {
245
+ const value = Angular.computed(fn)
246
+ let previousValue: T | null = null
247
+
248
+ return Angular.computed(() => {
249
+ // We known value is different that the previous one,
250
+ // thanks to signal memoization.
251
+ const lastPreviousValue = previousValue
252
+ previousValue = value()
253
+ return lastPreviousValue
254
+ })
255
+ }