@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/Match.ts ADDED
@@ -0,0 +1,338 @@
1
+ import {
2
+ Component,
3
+ DestroyRef,
4
+ EnvironmentInjector,
5
+ afterNextRender,
6
+ computed,
7
+ effect,
8
+ inject,
9
+ input,
10
+ isDevMode,
11
+ } from '@angular/core'
12
+ import {
13
+ AnyRoute,
14
+ AnyRouter,
15
+ getLocationChangeInfo,
16
+ rootRouteId,
17
+ } from '@benjavicente/router-core'
18
+ import { injectRouter } from './injectRouter'
19
+ import { injectStore } from './injectStore'
20
+ import { DefaultNotFoundComponent } from './DefaultNotFound'
21
+ import { MATCH_CONTEXT_INJECTOR_TOKEN } from './matchInjectorToken'
22
+ import { injectRender } from './renderer/injectRender'
23
+ import { ERROR_STATE_INJECTOR_TOKEN } from './injectErrorState'
24
+ import { injectIsCatchingError } from './renderer/injectIsCatchingError'
25
+ import type { Signal } from '@angular/core'
26
+ import type { NearestMatchContextValue } from './matchInjectorToken'
27
+
28
+ function injectOnRendered({
29
+ parentRouteIsRoot,
30
+ }: {
31
+ parentRouteIsRoot: Signal<boolean>
32
+ }) {
33
+ const router = injectRouter({ warn: false })
34
+ const envInjector = inject(EnvironmentInjector)
35
+ const destroyRef = inject(DestroyRef)
36
+ const location = injectStore(
37
+ router.stores.resolvedLocation,
38
+ (resolvedLocation) => resolvedLocation?.state.__TSR_key,
39
+ )
40
+ const loadedAt = injectStore(router.stores.loadedAt, (value) => value)
41
+
42
+ let prevHref: string | undefined
43
+ let renderGeneration = 0
44
+
45
+ destroyRef.onDestroy(() => {
46
+ renderGeneration++
47
+ })
48
+
49
+ effect(() => {
50
+ if (!parentRouteIsRoot()) return
51
+ location()
52
+ loadedAt()
53
+
54
+ const gen = ++renderGeneration
55
+ afterNextRender(
56
+ {
57
+ read: () => {
58
+ if (gen !== renderGeneration) return
59
+ if (!parentRouteIsRoot()) return
60
+ if (router.isServer) return
61
+
62
+ const currentHref = router.latestLocation.href
63
+ if (prevHref !== undefined && prevHref === currentHref) {
64
+ return
65
+ }
66
+ prevHref = currentHref
67
+
68
+ router.emit({
69
+ type: 'onRendered',
70
+ ...getLocationChangeInfo(
71
+ router.stores.location.state,
72
+ router.stores.resolvedLocation.state,
73
+ ),
74
+ })
75
+ },
76
+ },
77
+ { injector: envInjector },
78
+ )
79
+ })
80
+ }
81
+
82
+ @Component({
83
+ selector: 'router-match,[router-match]',
84
+ template: '',
85
+ standalone: true,
86
+ host: {
87
+ '[attr.data-matchId]': 'matchId()',
88
+ },
89
+ })
90
+ export class RouteMatch {
91
+ matchId = input.required<string>()
92
+
93
+ router = injectRouter()
94
+
95
+ match = computed(() => {
96
+ const matchId = this.matchId()
97
+ return matchId
98
+ ? this.router.stores.activeMatchStoresById.get(matchId)?.state
99
+ : undefined
100
+ })
101
+
102
+ matchData = computed(() => {
103
+ const match = this.match()
104
+ if (!match) return null
105
+
106
+ const routeId = match.routeId
107
+ const route = this.router.routesById[routeId] as AnyRoute
108
+ const parentRouteId = route.parentRoute?.id ?? null
109
+ const remountFn =
110
+ route.options.remountDeps ?? this.router.options.defaultRemountDeps
111
+
112
+ const remountDeps = remountFn?.({
113
+ routeId,
114
+ loaderDeps: match.loaderDeps,
115
+ params: match._strictParams,
116
+ search: match._strictSearch,
117
+ })
118
+ const key = remountDeps ? JSON.stringify(remountDeps) : undefined
119
+
120
+ return {
121
+ key,
122
+ route,
123
+ match,
124
+ parentRouteId,
125
+ }
126
+ })
127
+
128
+ isFistRouteInRouteTree = computed(
129
+ () => this.matchData()?.parentRouteId === rootRouteId,
130
+ )
131
+
132
+ resolvedNoSsr = computed(() => {
133
+ const match = this.matchData()?.match
134
+ if (!match) return true
135
+ return match.ssr === false || match.ssr === 'data-only'
136
+ })
137
+
138
+ shouldClientOnly = computed(() => {
139
+ const match = this.matchData()?.match
140
+ if (!match) return true
141
+ return this.resolvedNoSsr() || !!match._displayPending
142
+ })
143
+
144
+ parentRouteIdSignal = computed(
145
+ () => this.matchData()?.parentRouteId ?? '',
146
+ )
147
+ rootRouteIdSignal = computed(() => rootRouteId)
148
+
149
+ hasPendingMatch = computed(() => {
150
+ const routeId = this.matchData()?.route.id
151
+ return routeId ? Boolean(this.pendingRouteIds()[routeId]) : false
152
+ })
153
+ pendingRouteIds = injectStore(this.router.stores.pendingRouteIds, (ids) => ids)
154
+ nearestMatchContext: NearestMatchContextValue = {
155
+ matchId: this.matchId,
156
+ routeId: computed(() => this.matchData()?.route.id),
157
+ match: this.match,
158
+ hasPending: this.hasPendingMatch,
159
+ }
160
+
161
+ isCatchingError = injectIsCatchingError({
162
+ matchId: this.matchId,
163
+ })
164
+
165
+ render = injectRender(() => {
166
+ const matchData = this.matchData()
167
+ if (!matchData) return null
168
+
169
+ if (this.shouldClientOnly() && this.router.isServer) {
170
+ return null
171
+ }
172
+
173
+ const { match, route } = matchData
174
+
175
+ if (match.status === 'notFound') {
176
+ const NotFoundComponent = getNotFoundComponent(this.router, route)
177
+
178
+ return {
179
+ component: NotFoundComponent,
180
+ }
181
+ } else if (match.status === 'error' || this.isCatchingError()) {
182
+ const RouteErrorComponent =
183
+ getComponent(route.options.errorComponent) ??
184
+ getComponent(this.router.options.defaultErrorComponent)
185
+
186
+ return {
187
+ component: RouteErrorComponent || null,
188
+ providers: [
189
+ {
190
+ provide: ERROR_STATE_INJECTOR_TOKEN,
191
+ useValue: {
192
+ error: match.error,
193
+ reset: () => {
194
+ this.router.invalidate()
195
+ },
196
+ info: { componentStack: '' },
197
+ },
198
+ },
199
+ ],
200
+ }
201
+ } else if (match.status === 'redirected' || match.status === 'pending') {
202
+ const rootShellComponent =
203
+ route.isRoot &&
204
+ (getComponent(route.options.component) ??
205
+ getComponent(this.router.options.defaultComponent))
206
+
207
+ if (rootShellComponent && rootShellComponent !== Outlet) {
208
+ const key = matchData.key
209
+
210
+ return {
211
+ key,
212
+ component: rootShellComponent,
213
+ providers: [
214
+ {
215
+ provide: MATCH_CONTEXT_INJECTOR_TOKEN,
216
+ useValue: this.nearestMatchContext,
217
+ },
218
+ ],
219
+ }
220
+ }
221
+
222
+ const PendingComponent =
223
+ getComponent(route.options.pendingComponent) ??
224
+ getComponent(this.router.options.defaultPendingComponent)
225
+
226
+ return {
227
+ component: PendingComponent,
228
+ }
229
+ } else {
230
+ const routeViewComponent =
231
+ getComponent(route.options.component) ??
232
+ getComponent(this.router.options.defaultComponent) ??
233
+ Outlet
234
+
235
+ const key = matchData.key
236
+
237
+ return {
238
+ key,
239
+ component: routeViewComponent,
240
+ providers: [
241
+ {
242
+ provide: MATCH_CONTEXT_INJECTOR_TOKEN,
243
+ useValue: this.nearestMatchContext,
244
+ },
245
+ ],
246
+ }
247
+ }
248
+ })
249
+
250
+ onRendered = injectOnRendered({
251
+ parentRouteIsRoot: computed(
252
+ () => this.parentRouteIdSignal() === rootRouteId,
253
+ ),
254
+ })
255
+ }
256
+
257
+ @Component({
258
+ selector: 'outlet,[outlet]',
259
+ template: '',
260
+ standalone: true,
261
+ })
262
+ export class Outlet {
263
+ router = injectRouter()
264
+ nearestMatch = inject(MATCH_CONTEXT_INJECTOR_TOKEN)
265
+
266
+ currentMatch = computed(() => {
267
+ const matchId = this.nearestMatch.matchId()
268
+ return matchId
269
+ ? this.router.stores.activeMatchStoresById.get(matchId)?.state
270
+ : undefined
271
+ })
272
+
273
+ routeId = computed(() => this.currentMatch()?.routeId as string)
274
+
275
+ route = computed(
276
+ () => this.router.routesById[this.routeId()] as AnyRoute,
277
+ )
278
+
279
+ parentGlobalNotFound = computed(
280
+ () => this.currentMatch()?.globalNotFound ?? false,
281
+ )
282
+ childMatchIdByRouteId = injectStore(
283
+ this.router.stores.childMatchIdByRouteId,
284
+ (value) => value,
285
+ )
286
+
287
+ childMatchId = computed(() => {
288
+ const routeId = this.routeId()
289
+ if (!routeId) return null
290
+ return this.childMatchIdByRouteId()[routeId] ?? null
291
+ })
292
+
293
+ render = injectRender(() => {
294
+ if (this.parentGlobalNotFound()) {
295
+ const NotFoundComponent = getNotFoundComponent(this.router, this.route())
296
+ return { component: NotFoundComponent }
297
+ }
298
+ const childMatchId = this.childMatchId()
299
+
300
+ if (!childMatchId) {
301
+ return null
302
+ }
303
+
304
+ return {
305
+ component: RouteMatch,
306
+ inputs: {
307
+ matchId: () => this.childMatchId(),
308
+ },
309
+ }
310
+ })
311
+ }
312
+
313
+ type CalledIfFunction<T> = T extends (...args: Array<any>) => any ? ReturnType<T> : T
314
+
315
+ function getComponent<T>(routeComponent: T): CalledIfFunction<T> {
316
+ if (typeof routeComponent === 'function') {
317
+ return routeComponent()
318
+ }
319
+ return routeComponent as any
320
+ }
321
+
322
+ function getNotFoundComponent(router: AnyRouter, route: AnyRoute) {
323
+ const NotFoundComponent =
324
+ getComponent(route.options.notFoundComponent) ??
325
+ getComponent(router.options.defaultNotFoundComponent)
326
+
327
+ if (NotFoundComponent) {
328
+ return NotFoundComponent
329
+ }
330
+
331
+ if (isDevMode() && !route.options.notFoundComponent) {
332
+ console.warn(
333
+ `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 (<p>Page not found</p>)`,
334
+ )
335
+ }
336
+
337
+ return DefaultNotFoundComponent
338
+ }
package/src/Matches.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { Component } from '@angular/core'
2
+ import { injectRouter } from './injectRouter'
3
+ import { injectStore } from './injectStore'
4
+ import { injectRender } from './renderer/injectRender'
5
+ import { RouteMatch } from './Match'
6
+ import { injectSsrScrollRestorationScript } from './ssr-scroll-restoration'
7
+ import { injectTransitionerSetup } from './transitioner'
8
+
9
+ @Component({
10
+ selector: 'router-matches',
11
+ template: '',
12
+ standalone: true,
13
+ })
14
+ export class Matches {
15
+ router = injectRouter()
16
+
17
+ private matchId = injectStore(this.router.stores.firstMatchId, (id) => id)
18
+
19
+ private ssrScrollRestoration = injectSsrScrollRestorationScript()
20
+
21
+ transitioner = injectTransitionerSetup()
22
+
23
+ render = injectRender(() => {
24
+ const matchId = this.matchId()
25
+
26
+ if (!matchId) {
27
+ return null
28
+ }
29
+
30
+ return {
31
+ component: RouteMatch,
32
+ inputs: {
33
+ matchId: () => matchId,
34
+ },
35
+ }
36
+ })
37
+ }
@@ -0,0 +1,162 @@
1
+ import {
2
+ Component,
3
+ EnvironmentInjector,
4
+ InjectionToken,
5
+ computed,
6
+ effect,
7
+ inject,
8
+ input,
9
+ untracked,
10
+ } from '@angular/core'
11
+ import {
12
+ AnyRouter,
13
+ RegisteredRouter,
14
+ RouterOptions,
15
+ } from '@benjavicente/router-core'
16
+ import { Matches } from './Matches'
17
+ import { injectRender } from './renderer/injectRender'
18
+ import { getRouterInjectionKey } from './routerInjectionToken'
19
+ import type { InputSignal } from '@angular/core'
20
+
21
+ const CONTEXT_INPUT_INJECTION_KEY = new InjectionToken<RouterInputs['context']>(
22
+ 'CONTEXT',
23
+ {
24
+ providedIn: 'root',
25
+ factory: () => ({}),
26
+ },
27
+ )
28
+
29
+ const OPTIONS_INPUT_INJECTION_KEY = new InjectionToken<
30
+ Omit<RouterInputs, 'router' | 'context'>
31
+ >('OPTIONS', {
32
+ providedIn: 'root',
33
+ factory: () => ({}),
34
+ })
35
+
36
+ export type TanstackRouterProviderOptions = {
37
+ router: AnyRouter
38
+ context?: RouterInputs['context']
39
+ options?: Omit<RouterInputs, 'router' | 'context'>
40
+ }
41
+
42
+ function mergeContextWithInject(
43
+ context: RouterInputs['context'],
44
+ ): RouterInputs['context'] {
45
+ const environmentInjector = inject(EnvironmentInjector)
46
+
47
+ return {
48
+ inject: environmentInjector.get.bind(environmentInjector),
49
+ ...context,
50
+ } as RouterInputs['context']
51
+ }
52
+
53
+ export function provideTanstackRouter({
54
+ router,
55
+ context,
56
+ options,
57
+ }: TanstackRouterProviderOptions) {
58
+ return [
59
+ {
60
+ provide: getRouterInjectionKey(),
61
+ useValue: router,
62
+ },
63
+ {
64
+ provide: CONTEXT_INPUT_INJECTION_KEY,
65
+ useFactory: () => mergeContextWithInject(context ?? {}),
66
+ },
67
+ {
68
+ provide: OPTIONS_INPUT_INJECTION_KEY,
69
+ useValue: options ?? {},
70
+ },
71
+ ]
72
+ }
73
+
74
+ @Component({
75
+ selector: 'router-provider,[router-provider]',
76
+ template: '',
77
+ standalone: true,
78
+ })
79
+ export class RouterProvider<TRouter extends AnyRouter = RegisteredRouter> {
80
+ readonly injectedContext: RouterInputs<TRouter>['context'] = inject(
81
+ CONTEXT_INPUT_INJECTION_KEY,
82
+ )
83
+ readonly injectedOptions: Omit<RouterInputs<TRouter>, 'router' | 'context'> =
84
+ inject(OPTIONS_INPUT_INJECTION_KEY)
85
+ readonly injectedRouter: AnyRouter | null = inject(getRouterInjectionKey(), {
86
+ optional: true,
87
+ })
88
+
89
+ readonly context: InputSignal<RouterInputs<TRouter>['context']> = input<
90
+ RouterInputs<TRouter>['context']
91
+ >(this.injectedContext)
92
+ readonly options: InputSignal<
93
+ Omit<RouterInputs<TRouter>, 'router' | 'context'>
94
+ > = input<Omit<RouterInputs<TRouter>, 'router' | 'context'>>(
95
+ this.injectedOptions,
96
+ )
97
+ readonly routerInput = input<TRouter | undefined>(undefined, {
98
+ alias: 'router',
99
+ })
100
+
101
+ readonly router = computed(() => {
102
+ const inputRouter = this.routerInput()
103
+ if (inputRouter) return inputRouter
104
+ if (this.injectedRouter) return this.injectedRouter as TRouter
105
+ throw new Error(
106
+ 'No router provided to <router-provider>. Provide a router with provideTanstackRouter or the router input',
107
+ )
108
+ })
109
+
110
+ readonly updateRouter = effect(() => {
111
+ const router = this.router()
112
+ const context = this.context()
113
+ const options = this.options()
114
+
115
+ router.update({
116
+ ...router.options,
117
+ ...options,
118
+ context: {
119
+ ...router.options.context,
120
+ ...context,
121
+ },
122
+ })
123
+ })
124
+
125
+ readonly render: ReturnType<typeof injectRender> = injectRender(() => {
126
+ const router = untracked(this.router)
127
+ return {
128
+ component: Matches,
129
+ providers: [
130
+ {
131
+ provide: getRouterInjectionKey(),
132
+ useValue: router,
133
+ },
134
+ ],
135
+ }
136
+ })
137
+ }
138
+
139
+ type RouterInputs<
140
+ TRouter extends AnyRouter = RegisteredRouter,
141
+ TDehydrated extends Record<string, any> = Record<string, any>,
142
+ > = Omit<
143
+ RouterOptions<
144
+ TRouter['routeTree'],
145
+ NonNullable<TRouter['options']['trailingSlash']>,
146
+ false,
147
+ TRouter['history'],
148
+ TDehydrated
149
+ >,
150
+ 'context'
151
+ > & {
152
+ router: TRouter
153
+ context?: Partial<
154
+ RouterOptions<
155
+ TRouter['routeTree'],
156
+ NonNullable<TRouter['options']['trailingSlash']>,
157
+ false,
158
+ TRouter['history'],
159
+ TDehydrated
160
+ >['context']
161
+ >
162
+ }