@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.
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/fesm2022/tanstack-angular-router-experimental-experimental.mjs +920 -0
- package/dist/fesm2022/tanstack-angular-router-experimental.mjs +4131 -0
- package/dist/types/tanstack-angular-router-experimental-experimental.d.ts +110 -0
- package/dist/types/tanstack-angular-router-experimental.d.ts +733 -0
- package/experimental/injectRouteErrorHandler.ts +51 -0
- package/experimental/public_api.ts +8 -0
- package/package.json +98 -0
- package/src/DefaultNotFound.ts +9 -0
- package/src/Link.ts +352 -0
- package/src/Match.ts +338 -0
- package/src/Matches.ts +37 -0
- package/src/RouterProvider.ts +162 -0
- package/src/document/build-match-managed-document.ts +308 -0
- package/src/document/document-dehydration.ts +27 -0
- package/src/document/document-equality.ts +29 -0
- package/src/document/document-router-token.ts +6 -0
- package/src/document/index.ts +33 -0
- package/src/document/install-unified-document-sync.ts +108 -0
- package/src/document/managed-document-types.ts +36 -0
- package/src/document/managed-dom.ts +307 -0
- package/src/document/provide-tanstack-body-managed-tags.ts +78 -0
- package/src/document/provide-tanstack-document-title.ts +59 -0
- package/src/document/provide-tanstack-document.ts +62 -0
- package/src/document/provide-tanstack-head-managed-tags.ts +63 -0
- package/src/fileRoute.ts +232 -0
- package/src/index.ts +173 -0
- package/src/injectBlocker.ts +196 -0
- package/src/injectCanGoBack.ts +11 -0
- package/src/injectErrorState.ts +21 -0
- package/src/injectIntersectionObserver.ts +28 -0
- package/src/injectLoaderData.ts +49 -0
- package/src/injectLoaderDeps.ts +45 -0
- package/src/injectLocation.ts +38 -0
- package/src/injectMatch.ts +122 -0
- package/src/injectMatchRoute.ts +58 -0
- package/src/injectMatches.ts +79 -0
- package/src/injectNavigate.ts +24 -0
- package/src/injectParams.ts +71 -0
- package/src/injectRouteContext.ts +31 -0
- package/src/injectRouter.ts +17 -0
- package/src/injectRouterState.ts +53 -0
- package/src/injectSearch.ts +71 -0
- package/src/injectStore.ts +87 -0
- package/src/matchInjectorToken.ts +23 -0
- package/src/renderer/injectIsCatchingError.ts +40 -0
- package/src/renderer/injectRender.ts +69 -0
- package/src/route.ts +641 -0
- package/src/router.ts +141 -0
- package/src/routerInjectionToken.ts +24 -0
- package/src/routerStores.ts +107 -0
- package/src/ssr-scroll-restoration.ts +48 -0
- 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
|
+
}
|