@typed/router 0.13.0 → 0.15.0
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/dist/Link.d.ts +26 -11
- package/dist/Link.d.ts.map +1 -1
- package/dist/Link.js +47 -23
- package/dist/Link.js.map +1 -1
- package/dist/Match.d.ts +33 -0
- package/dist/Match.d.ts.map +1 -0
- package/dist/Match.js +16 -0
- package/dist/Match.js.map +1 -0
- package/dist/Matcher.d.ts +28 -0
- package/dist/Matcher.d.ts.map +1 -0
- package/dist/Matcher.js +24 -0
- package/dist/Matcher.js.map +1 -0
- package/dist/Navigation.d.ts +10 -0
- package/dist/Navigation.d.ts.map +1 -0
- package/dist/Navigation.js +7 -0
- package/dist/Navigation.js.map +1 -0
- package/dist/Redirect.d.ts +29 -0
- package/dist/Redirect.d.ts.map +1 -0
- package/dist/Redirect.js +17 -0
- package/dist/Redirect.js.map +1 -0
- package/dist/RouteOutlet.d.ts +3 -0
- package/dist/RouteOutlet.d.ts.map +1 -0
- package/dist/RouteOutlet.js +2 -0
- package/dist/RouteOutlet.js.map +1 -0
- package/dist/ScrollRestoration.d.ts +19 -0
- package/dist/ScrollRestoration.d.ts.map +1 -0
- package/dist/ScrollRestoration.js +64 -0
- package/dist/ScrollRestoration.js.map +1 -0
- package/dist/cjs/Link.d.ts +26 -11
- package/dist/cjs/Link.d.ts.map +1 -1
- package/dist/cjs/Link.js +47 -22
- package/dist/cjs/Link.js.map +1 -1
- package/dist/cjs/Match.d.ts +33 -0
- package/dist/cjs/Match.d.ts.map +1 -0
- package/dist/cjs/Match.js +43 -0
- package/dist/cjs/Match.js.map +1 -0
- package/dist/cjs/Matcher.d.ts +28 -0
- package/dist/cjs/Matcher.d.ts.map +1 -0
- package/dist/cjs/Matcher.js +52 -0
- package/dist/cjs/Matcher.js.map +1 -0
- package/dist/cjs/Navigation.d.ts +10 -0
- package/dist/cjs/Navigation.d.ts.map +1 -0
- package/dist/cjs/Navigation.js +34 -0
- package/dist/cjs/Navigation.js.map +1 -0
- package/dist/cjs/Redirect.d.ts +29 -0
- package/dist/cjs/Redirect.d.ts.map +1 -0
- package/dist/cjs/Redirect.js +44 -0
- package/dist/cjs/Redirect.js.map +1 -0
- package/dist/cjs/ScrollRestoration.d.ts +19 -0
- package/dist/cjs/ScrollRestoration.d.ts.map +1 -0
- package/dist/cjs/ScrollRestoration.js +91 -0
- package/dist/cjs/ScrollRestoration.js.map +1 -0
- package/dist/cjs/index.d.ts +7 -3
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +7 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/matchRoutes.d.ts +8 -0
- package/dist/cjs/matchRoutes.d.ts.map +1 -0
- package/dist/cjs/matchRoutes.js +77 -0
- package/dist/cjs/matchRoutes.js.map +1 -0
- package/dist/cjs/router.d.ts +24 -63
- package/dist/cjs/router.d.ts.map +1 -1
- package/dist/cjs/router.js +22 -160
- package/dist/cjs/router.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/matchRoutes.d.ts +8 -0
- package/dist/matchRoutes.d.ts.map +1 -0
- package/dist/matchRoutes.js +50 -0
- package/dist/matchRoutes.js.map +1 -0
- package/dist/router.d.ts +24 -63
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +18 -153
- package/dist/router.js.map +1 -1
- package/dist/tsconfig.cjs.build.tsbuildinfo +1 -1
- package/package.json +13 -11
- package/project.json +12 -10
- package/src/Link.ts +129 -39
- package/src/Match.ts +114 -0
- package/src/Matcher.ts +139 -0
- package/src/Navigation.ts +24 -0
- package/src/Redirect.ts +21 -0
- package/src/ScrollRestoration.ts +110 -0
- package/src/index.ts +7 -3
- package/src/matchRoutes.ts +112 -0
- package/src/router.ts +53 -311
- package/tsconfig.build.json +5 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.cjs.build.json +6 -0
- package/tsconfig.json +6 -0
- package/vite.config.js +3 -0
- package/src/RouteMatch.ts +0 -56
- package/src/RouteMatcher.ts +0 -264
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -19,18 +19,20 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@effect/data": "^0.
|
|
23
|
-
"@effect/io": "^0.
|
|
24
|
-
"@typed/context": "0.
|
|
25
|
-
"@typed/dom": "8.
|
|
26
|
-
"@typed/
|
|
27
|
-
"@typed/
|
|
28
|
-
"@typed/
|
|
29
|
-
"@typed/
|
|
22
|
+
"@effect/data": "^0.16.0",
|
|
23
|
+
"@effect/io": "^0.35.1",
|
|
24
|
+
"@typed/context": "0.15.0",
|
|
25
|
+
"@typed/dom": "8.15.0",
|
|
26
|
+
"@typed/html": "3.15.0",
|
|
27
|
+
"@typed/navigation": "0.2.0",
|
|
28
|
+
"@typed/fx": "1.15.0",
|
|
29
|
+
"@typed/error": "0.15.0",
|
|
30
|
+
"@typed/route": "0.12.0",
|
|
31
|
+
"@typed/path": "0.5.0"
|
|
30
32
|
},
|
|
31
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "781662109038798bdc35ec7a6de3551ec16aaa0a",
|
|
32
34
|
"publishConfig": {
|
|
33
35
|
"access": "public"
|
|
34
36
|
},
|
|
35
37
|
"sideEffects": false
|
|
36
|
-
}
|
|
38
|
+
}
|
package/project.json
CHANGED
|
@@ -11,20 +11,24 @@
|
|
|
11
11
|
}
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
|
-
"outputs": [
|
|
15
|
-
"{projectRoot}/dist"
|
|
16
|
-
]
|
|
14
|
+
"outputs": ["{projectRoot}/dist"]
|
|
17
15
|
},
|
|
18
16
|
"lint": {
|
|
19
17
|
"executor": "@nrwl/linter:eslint",
|
|
20
18
|
"options": {
|
|
21
|
-
"lintFilePatterns": [
|
|
22
|
-
"packages/router/src/**/*.ts"
|
|
23
|
-
]
|
|
19
|
+
"lintFilePatterns": ["packages/router/src/**/*.ts"]
|
|
24
20
|
}
|
|
25
21
|
},
|
|
26
22
|
"test": {
|
|
27
|
-
"executor": "@nrwl/vite:test"
|
|
23
|
+
"executor": "@nrwl/vite:test",
|
|
24
|
+
"options": {
|
|
25
|
+
"globals": true
|
|
26
|
+
},
|
|
27
|
+
"configurations": {
|
|
28
|
+
"watch": {
|
|
29
|
+
"watch": true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
28
32
|
},
|
|
29
33
|
"build:cjs": {
|
|
30
34
|
"executor": "nx:run-commands",
|
|
@@ -36,9 +40,7 @@
|
|
|
36
40
|
],
|
|
37
41
|
"parallel": false
|
|
38
42
|
},
|
|
39
|
-
"outputs": [
|
|
40
|
-
"{projectRoot}/dist/cjs"
|
|
41
|
-
]
|
|
43
|
+
"outputs": ["{projectRoot}/dist/cjs"]
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
}
|
package/src/Link.ts
CHANGED
|
@@ -1,48 +1,138 @@
|
|
|
1
1
|
import * as Effect from '@effect/io/Effect'
|
|
2
2
|
import * as Scope from '@effect/io/Scope'
|
|
3
|
-
import { assign } from '@typed/dom'
|
|
4
3
|
import * as Fx from '@typed/fx'
|
|
5
|
-
import {
|
|
4
|
+
import { Placeholder } from '@typed/html'
|
|
5
|
+
import * as Navigation from '@typed/navigation'
|
|
6
6
|
import { pathJoin } from '@typed/path'
|
|
7
7
|
|
|
8
|
-
import { Router,
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
8
|
+
import { Router, getCurrentPathFromUrl } from './router.js'
|
|
9
|
+
|
|
10
|
+
export interface UseLinkParams<
|
|
11
|
+
R = never,
|
|
12
|
+
E = never,
|
|
13
|
+
R2 = never,
|
|
14
|
+
E2 = never,
|
|
15
|
+
R3 = never,
|
|
16
|
+
E3 = never,
|
|
17
|
+
R4 = never,
|
|
18
|
+
E4 = never,
|
|
19
|
+
R5 = never,
|
|
20
|
+
E5 = never,
|
|
21
|
+
> {
|
|
22
|
+
readonly to: Placeholder<R, E, string>
|
|
23
|
+
readonly replace?: Placeholder<R2, E2, boolean>
|
|
24
|
+
readonly state?: Placeholder<R3, E3, unknown> | unknown
|
|
25
|
+
readonly relative?: Placeholder<R4, E4, boolean>
|
|
26
|
+
readonly key?: Placeholder<R5, E5, string>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export namespace UseLinkParams {
|
|
30
|
+
export type Any = UseLinkParams<any, any, any, any, any, any, any, any, any, any>
|
|
31
|
+
|
|
32
|
+
export type Context<T extends Any> = Placeholder.ResourcesOf<
|
|
33
|
+
T['to'] | T['replace'] | T['state'] | T['relative'] | T['key']
|
|
34
|
+
>
|
|
35
|
+
|
|
36
|
+
export type Error<T extends Any> = Placeholder.ErrorsOf<
|
|
37
|
+
T['to'] | T['replace'] | T['state'] | T['relative'] | T['key']
|
|
38
|
+
>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface UseLink<E> {
|
|
42
|
+
readonly url: Fx.Computed<Router, E, string>
|
|
43
|
+
readonly options: Fx.Computed<never, E, Navigation.NavigateOptions>
|
|
44
|
+
readonly navigate: Effect.Effect<Router, E, Navigation.Destination>
|
|
45
|
+
readonly active: Fx.Computed<Router, E, boolean>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export namespace UseLink {
|
|
49
|
+
export type FromParams<T extends UseLinkParams.Any> = [UseLink<UseLinkParams.Error<T>>] extends [
|
|
50
|
+
infer U,
|
|
51
|
+
]
|
|
52
|
+
? U
|
|
53
|
+
: never
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function useLink<Params extends UseLinkParams.Any>(
|
|
57
|
+
params: Params,
|
|
58
|
+
): Effect.Effect<UseLinkParams.Context<Params> | Scope.Scope, never, UseLink.FromParams<Params>> {
|
|
59
|
+
return Effect.map(
|
|
60
|
+
Effect.all([
|
|
61
|
+
Placeholder.asRef(params.to),
|
|
62
|
+
Placeholder.asRef(params.replace ?? false),
|
|
63
|
+
Placeholder.asRef(params.state ?? null),
|
|
64
|
+
Placeholder.asRef(params.key),
|
|
65
|
+
Placeholder.asRef(params.relative ?? true),
|
|
66
|
+
] as const),
|
|
67
|
+
([to, replace, state, key, relative]) => {
|
|
68
|
+
const url = Fx.RefSubject.tuple(to, relative).mapEffect(([to, relative]) =>
|
|
69
|
+
Effect.gen(function* ($) {
|
|
70
|
+
let url = to
|
|
71
|
+
|
|
72
|
+
// Check if we should make the URL relative to the current route
|
|
73
|
+
if (relative) {
|
|
74
|
+
const { route, params } = yield* $(Router)
|
|
75
|
+
const matched = yield* $(params)
|
|
76
|
+
const basePath = route.make(matched)
|
|
77
|
+
|
|
78
|
+
url = pathJoin(basePath, url)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return url
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
84
|
+
const options = Fx.RefSubject.tuple(replace, state, key).map(
|
|
85
|
+
([replace, state, key]): Navigation.NavigateOptions => ({
|
|
86
|
+
history: replace ? 'replace' : 'push',
|
|
87
|
+
state,
|
|
88
|
+
key: key ?? undefined,
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const active: Fx.Computed<Router, UseLinkParams.Error<Params>, boolean> = url.mapEffect(
|
|
93
|
+
(url) =>
|
|
94
|
+
Effect.gen(function* ($) {
|
|
95
|
+
const {
|
|
96
|
+
navigation: { currentEntry },
|
|
97
|
+
} = yield* $(Router)
|
|
98
|
+
|
|
99
|
+
return isActive(url, (yield* $(currentEntry)).url)
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const navigate: Effect.Effect<
|
|
104
|
+
Router,
|
|
105
|
+
UseLinkParams.Error<Params>,
|
|
106
|
+
Navigation.Destination
|
|
107
|
+
> = Effect.flatMap(Effect.all([url, options] as const), ([url, options]) =>
|
|
108
|
+
Router.withEffect((r) => r.navigation.navigate(url, options)),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
url,
|
|
113
|
+
options,
|
|
114
|
+
navigate,
|
|
115
|
+
active,
|
|
116
|
+
} satisfies UseLink.FromParams<Params>
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function Link<Params extends UseLinkParams.Any, R, E, A>(
|
|
122
|
+
params: Params,
|
|
123
|
+
render: (use: UseLink.FromParams<Params>) => Fx.Fx<R, E, A>,
|
|
124
|
+
): Fx.Fx<Scope.Scope | R | UseLinkParams.Context<Params>, E, A> {
|
|
125
|
+
return Fx.fromFxEffect(Effect.map(useLink(params), render))
|
|
36
126
|
}
|
|
37
127
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
readonly label: string | Placeholder<R, E>
|
|
41
|
-
readonly className?: string
|
|
42
|
-
readonly onClick?: (
|
|
43
|
-
event: MouseEvent & { currentTarget: HTMLAnchorElement },
|
|
44
|
-
) => Effect.Effect<R2, never, unknown>
|
|
128
|
+
function isActive(url: string, current: URL): boolean {
|
|
129
|
+
const { pathname } = current
|
|
45
130
|
|
|
46
|
-
|
|
47
|
-
|
|
131
|
+
return (
|
|
132
|
+
url === current.href ||
|
|
133
|
+
url === pathname ||
|
|
134
|
+
url === pathname + current.search ||
|
|
135
|
+
url === pathname + current.hash ||
|
|
136
|
+
url === getCurrentPathFromUrl(current)
|
|
137
|
+
)
|
|
48
138
|
}
|
package/src/Match.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as Effect from '@effect/io/Effect'
|
|
2
|
+
import * as Fx from '@typed/fx'
|
|
3
|
+
import { NavigationError } from '@typed/navigation'
|
|
4
|
+
import { ParamsOf } from '@typed/path'
|
|
5
|
+
import { Route } from '@typed/route'
|
|
6
|
+
|
|
7
|
+
export type MatchOptions<
|
|
8
|
+
P extends string,
|
|
9
|
+
Guard extends Effect.Effect<any, NavigationError, boolean> = Effect.Effect<
|
|
10
|
+
never,
|
|
11
|
+
NavigationError,
|
|
12
|
+
boolean
|
|
13
|
+
>,
|
|
14
|
+
Matched extends Effect.Effect<any, any, any> = Effect.Effect<never, never, unknown>,
|
|
15
|
+
> = {
|
|
16
|
+
readonly guard?: (params: ParamsOf<P>) => Guard
|
|
17
|
+
readonly onMatch?: (params: ParamsOf<P>) => Matched
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export namespace MatchOptions {
|
|
21
|
+
export type Any<P extends string> =
|
|
22
|
+
| MatchOptions<P, any, any>
|
|
23
|
+
| MatchOptions<P, never, never>
|
|
24
|
+
| MatchOptions<P, any, never>
|
|
25
|
+
| MatchOptions<P, never, any>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type Match<
|
|
29
|
+
in out P extends string,
|
|
30
|
+
out Rendered extends Fx.Fx<any, any, any>,
|
|
31
|
+
in out Options extends MatchOptions.Any<P> = MatchOptions<P>,
|
|
32
|
+
> = {
|
|
33
|
+
readonly route: Route<P>
|
|
34
|
+
readonly render: (params: Fx.Filtered<never, never, ParamsOf<P>>) => Rendered
|
|
35
|
+
readonly options?: Options
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export namespace Match {
|
|
39
|
+
export type Any = Match<any, any, any> | Match<any, any, never>
|
|
40
|
+
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
42
|
+
export type Rendered<M extends Any> = M extends Match<any, infer R, infer _> ? R : never
|
|
43
|
+
|
|
44
|
+
export type Options<M extends Any> = M extends Match<any, any, infer Options> ? Options : never
|
|
45
|
+
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
47
|
+
export type Guard<M extends Any> = [Options<M>] extends [MatchOptions<any, infer Guard, infer _>]
|
|
48
|
+
? Guard
|
|
49
|
+
: never
|
|
50
|
+
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
52
|
+
export type Matched<M extends Any> = Options<M> extends MatchOptions<any, infer _, infer Matched>
|
|
53
|
+
? Matched
|
|
54
|
+
: never
|
|
55
|
+
|
|
56
|
+
export type Context<M extends Any> =
|
|
57
|
+
| Fx.Fx.ResourcesOf<Rendered<M>>
|
|
58
|
+
| ([Guard<M>] extends [never] ? never : Effect.Effect.Context<Guard<M>>)
|
|
59
|
+
| ([Matched<M>] extends [never] ? never : Effect.Effect.Context<Matched<M>>)
|
|
60
|
+
|
|
61
|
+
export type Error<M extends Any> =
|
|
62
|
+
| Fx.Fx.ErrorsOf<Rendered<M>>
|
|
63
|
+
| ([Guard<M>] extends [never] ? never : Exclude<Effect.Effect.Error<Guard<M>>, NavigationError>)
|
|
64
|
+
| ([Matched<M>] extends [never] ? never : Effect.Effect.Error<Matched<M>>)
|
|
65
|
+
|
|
66
|
+
export type Success<M extends Any> = Fx.Fx.OutputOf<Rendered<M>>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function Match<P extends string, Rendered extends Fx.Fx<any, any, any>>(
|
|
70
|
+
route: Route<P>,
|
|
71
|
+
render: (params: Fx.Filtered<never, never, ParamsOf<P>>) => Rendered,
|
|
72
|
+
): Match<P, Rendered, MatchOptions<P>>
|
|
73
|
+
|
|
74
|
+
export function Match<
|
|
75
|
+
P extends string,
|
|
76
|
+
Rendered extends Fx.Fx<any, any, any>,
|
|
77
|
+
Options extends MatchOptions.Any<P>,
|
|
78
|
+
>(
|
|
79
|
+
route: Route<P>,
|
|
80
|
+
render: (params: Fx.Filtered<never, never, ParamsOf<P>>) => Rendered,
|
|
81
|
+
options: Options,
|
|
82
|
+
): Match<P, Rendered, Options>
|
|
83
|
+
|
|
84
|
+
export function Match<
|
|
85
|
+
P extends string,
|
|
86
|
+
Rendered extends Fx.Fx<any, any, any>,
|
|
87
|
+
Options extends MatchOptions.Any<P> = MatchOptions<P, never, never>,
|
|
88
|
+
>(
|
|
89
|
+
route: Route<P>,
|
|
90
|
+
render: (params: Fx.Filtered<never, never, ParamsOf<P>>) => Rendered,
|
|
91
|
+
options?: Options,
|
|
92
|
+
): Match<P, Rendered, Options> | Match<P, Rendered, never> {
|
|
93
|
+
return {
|
|
94
|
+
route,
|
|
95
|
+
render,
|
|
96
|
+
options,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Match.lazy = function <
|
|
101
|
+
P extends string,
|
|
102
|
+
Rendered extends Fx.Fx<any, any, any>,
|
|
103
|
+
Options extends MatchOptions.Any<P> = MatchOptions<P>,
|
|
104
|
+
>(
|
|
105
|
+
route: Route<P>,
|
|
106
|
+
render: () => Promise<(params: Fx.Filtered<never, never, ParamsOf<P>>) => Rendered>,
|
|
107
|
+
options?: Options,
|
|
108
|
+
): Match<P, Rendered, Options> | Match<P, Rendered, never> {
|
|
109
|
+
return {
|
|
110
|
+
route,
|
|
111
|
+
render: (params) => Fx.promiseFx(() => render().then((f) => f(params))) as Rendered,
|
|
112
|
+
options,
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/Matcher.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as Chunk from '@effect/data/Chunk'
|
|
2
|
+
import * as Debug from '@effect/data/Function'
|
|
3
|
+
import * as Effect from '@effect/io/Effect'
|
|
4
|
+
import * as Fx from '@typed/fx'
|
|
5
|
+
import { RenderContext } from '@typed/html'
|
|
6
|
+
import { ParamsOf } from '@typed/path'
|
|
7
|
+
import { Route } from '@typed/route'
|
|
8
|
+
|
|
9
|
+
import { Match, MatchOptions } from './Match.js'
|
|
10
|
+
import { Redirect } from './Redirect.js'
|
|
11
|
+
import { matchRoutes } from './matchRoutes.js'
|
|
12
|
+
import { Router } from './router.js'
|
|
13
|
+
|
|
14
|
+
export interface Matcher<Matches extends ReadonlyArray<Match.Any>> {
|
|
15
|
+
/** @internal */
|
|
16
|
+
readonly matches: Chunk.Chunk<Match.Any>
|
|
17
|
+
|
|
18
|
+
readonly match: <P extends string, R, E, A, Opts extends MatchOptions.Any<P> = MatchOptions<P>>(
|
|
19
|
+
route: Route<P>,
|
|
20
|
+
render: (params: Fx.Filtered<never, never, ParamsOf<P>>) => Fx.Fx<R, E, A>,
|
|
21
|
+
options?: Opts,
|
|
22
|
+
) => Matcher<readonly [...Matches, Match<P, Fx.Fx<R, E, A>, Opts>]>
|
|
23
|
+
|
|
24
|
+
readonly matchLazy: <
|
|
25
|
+
P extends string,
|
|
26
|
+
R,
|
|
27
|
+
E,
|
|
28
|
+
A,
|
|
29
|
+
Opts extends MatchOptions.Any<P> = MatchOptions<P>,
|
|
30
|
+
>(
|
|
31
|
+
route: Route<P>,
|
|
32
|
+
render: () => Promise<(params: Fx.Filtered<never, never, ParamsOf<P>>) => Fx.Fx<R, E, A>>,
|
|
33
|
+
options?: Opts,
|
|
34
|
+
) => Matcher<readonly [...Matches, Match<P, Fx.Fx<R, E, A>, Opts>]>
|
|
35
|
+
|
|
36
|
+
readonly notFound: <R, E, A>(
|
|
37
|
+
render: (params: Fx.Filtered<never, never, Readonly<Record<string, string>>>) => Fx.Fx<R, E, A>,
|
|
38
|
+
) => Fx.Fx<
|
|
39
|
+
Router | RenderContext | R | Match.Context<Matches[number]>,
|
|
40
|
+
Exclude<E | Match.Error<Matches[number]>, Redirect>,
|
|
41
|
+
A | Match.Success<Matches[number]>
|
|
42
|
+
>
|
|
43
|
+
|
|
44
|
+
readonly concat: <OtherMatches extends ReadonlyArray<Match.Any>>(
|
|
45
|
+
other: Matcher<OtherMatches>,
|
|
46
|
+
) => Matcher<readonly [...Matches, ...OtherMatches]>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function Matcher<const Matches extends ReadonlyArray<Match.Any>>(
|
|
50
|
+
matches: Chunk.Chunk<Match.Any>,
|
|
51
|
+
): Matcher<Matches> {
|
|
52
|
+
return {
|
|
53
|
+
matches,
|
|
54
|
+
|
|
55
|
+
match<P extends string, R, E, A, Opts extends MatchOptions.Any<P> = never>(
|
|
56
|
+
route: Route<P>,
|
|
57
|
+
render: (params: Fx.Filtered<never, never, ParamsOf<P>>) => Fx.Fx<R, E, A>,
|
|
58
|
+
options?: Opts,
|
|
59
|
+
): Matcher<readonly [...Matches, Match<P, Fx.Fx<R, E, A>, Opts>]> {
|
|
60
|
+
return Matcher(Chunk.append(matches, Match(route, render, options || {})))
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
matchLazy<P extends string, R, E, A, Opts extends MatchOptions.Any<P> = never>(
|
|
64
|
+
route: Route<P>,
|
|
65
|
+
render: () => Promise<(params: Fx.Filtered<never, never, ParamsOf<P>>) => Fx.Fx<R, E, A>>,
|
|
66
|
+
options?: Opts,
|
|
67
|
+
): Matcher<readonly [...Matches, Match<P, Fx.Fx<R, E, A>, Opts>]> {
|
|
68
|
+
return Matcher(Chunk.append(matches, Match.lazy(route, render, options || {})))
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
notFound: <R, E, A>(
|
|
72
|
+
render: (
|
|
73
|
+
params: Fx.Filtered<never, never, Readonly<Record<string, string>>>,
|
|
74
|
+
) => Fx.Fx<R, E, A>,
|
|
75
|
+
) => Fx.scoped(matchRoutes(Chunk.toReadonlyArray(matches) as Matches, render)),
|
|
76
|
+
|
|
77
|
+
concat: <OtherMatches extends ReadonlyArray<Match.Any>>(
|
|
78
|
+
other: Matcher<OtherMatches>,
|
|
79
|
+
): Matcher<readonly [...Matches, ...OtherMatches]> => {
|
|
80
|
+
return Matcher(Chunk.appendAll(matches, other.matches))
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const { match, matchLazy } = Matcher<readonly []>(Chunk.empty())
|
|
86
|
+
|
|
87
|
+
export const notFound: {
|
|
88
|
+
<R, E, A>(
|
|
89
|
+
render: (params: Fx.Filtered<never, never, Readonly<Record<string, string>>>) => Fx.Fx<R, E, A>,
|
|
90
|
+
): <Matches extends readonly Match.Any[]>(
|
|
91
|
+
matcher: Matcher<Matches>,
|
|
92
|
+
) => Fx.Fx<
|
|
93
|
+
Router | R | Match.Context<Matches[number]>,
|
|
94
|
+
Exclude<E | Match.Error<Matches[number]>, Redirect>,
|
|
95
|
+
A | Fx.Fx.OutputOf<Match.Rendered<Matches[number]>>
|
|
96
|
+
>
|
|
97
|
+
|
|
98
|
+
<Matches extends readonly Match.Any[], R, E, A>(
|
|
99
|
+
matcher: Matcher<Matches>,
|
|
100
|
+
render: (params: Fx.Filtered<never, never, Readonly<Record<string, string>>>) => Fx.Fx<R, E, A>,
|
|
101
|
+
): Fx.Fx<
|
|
102
|
+
Router | R | Match.Context<Matches[number]>,
|
|
103
|
+
Exclude<E | Match.Error<Matches[number]>, Redirect>,
|
|
104
|
+
A | Fx.Fx.OutputOf<Match.Rendered<Matches[number]>>
|
|
105
|
+
>
|
|
106
|
+
} = Debug.dual(
|
|
107
|
+
2,
|
|
108
|
+
<Matches extends readonly Match.Any[], R, E, A>(
|
|
109
|
+
matcher: Matcher<Matches>,
|
|
110
|
+
render: (params: Fx.Filtered<never, never, Readonly<Record<string, string>>>) => Fx.Fx<R, E, A>,
|
|
111
|
+
) => matcher.notFound(render),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
export const redirectEffect: {
|
|
115
|
+
<R, E>(
|
|
116
|
+
effect: Effect.Effect<R, E, never>,
|
|
117
|
+
): <Matches extends readonly Match.Any[]>(
|
|
118
|
+
matcher: Matcher<Matches>,
|
|
119
|
+
) => Fx.Fx<
|
|
120
|
+
Router | R | Match.Context<Matches[number]>,
|
|
121
|
+
Exclude<E | Match.Error<Matches[number]>, Redirect>,
|
|
122
|
+
Match.Success<Matches[number]>
|
|
123
|
+
>
|
|
124
|
+
|
|
125
|
+
<Matches extends readonly Match.Any[], R, E>(
|
|
126
|
+
matcher: Matcher<Matches>,
|
|
127
|
+
effect: Effect.Effect<R, E, never>,
|
|
128
|
+
): Fx.Fx<
|
|
129
|
+
Router | R | Match.Context<Matches[number]>,
|
|
130
|
+
Exclude<E | Match.Error<Matches[number]>, Redirect>,
|
|
131
|
+
Match.Success<Matches[number]>
|
|
132
|
+
>
|
|
133
|
+
} = Debug.dual(
|
|
134
|
+
2,
|
|
135
|
+
<Matches extends readonly Match.Any[], R, E>(
|
|
136
|
+
matcher: Matcher<Matches>,
|
|
137
|
+
effect: Effect.Effect<R, E, never>,
|
|
138
|
+
) => notFound(matcher, () => Fx.fromEffect(effect)),
|
|
139
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as Fx from '@typed/fx'
|
|
2
|
+
import { Placeholder } from '@typed/html'
|
|
3
|
+
import { NavigateOptions } from '@typed/navigation'
|
|
4
|
+
|
|
5
|
+
import { Redirect } from './Redirect.js'
|
|
6
|
+
|
|
7
|
+
export interface NavigationParams<R, E, R2, E2> {
|
|
8
|
+
// Configure the URL to navigate to
|
|
9
|
+
readonly url: Placeholder<R, E, string>
|
|
10
|
+
// Configure the options for the navigation, using @typed/navigation
|
|
11
|
+
readonly options?: Placeholder<R2, E2, NavigateOptions>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Navigation<R = never, E = never, R2 = never, E2 = never>(
|
|
15
|
+
params: NavigationParams<R, E, R2, E2>,
|
|
16
|
+
): Fx.Fx<R | R2, E | E2 | Redirect, null> {
|
|
17
|
+
return Fx.startWith(
|
|
18
|
+
Fx.switchMapEffect(
|
|
19
|
+
Fx.combine(Placeholder.asFx(params.url), Placeholder.asFx(params.options)),
|
|
20
|
+
([url, options]) => Redirect.redirect(url, options),
|
|
21
|
+
),
|
|
22
|
+
null,
|
|
23
|
+
)
|
|
24
|
+
}
|
package/src/Redirect.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as Effect from '@effect/io/Effect'
|
|
2
|
+
import * as E from '@typed/error'
|
|
3
|
+
import * as Fx from '@typed/fx'
|
|
4
|
+
import { NavigateOptions } from '@typed/navigation'
|
|
5
|
+
|
|
6
|
+
export class Redirect extends E.tagged('@typed/router/Redirect') {
|
|
7
|
+
constructor(
|
|
8
|
+
readonly url: string,
|
|
9
|
+
readonly options?: NavigateOptions,
|
|
10
|
+
) {
|
|
11
|
+
super(`Redirect to ${url}`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static redirect(url: string, options?: NavigateOptions): Effect.Effect<never, Redirect, never> {
|
|
15
|
+
return Effect.fail(new Redirect(url, options))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static redirectFx(url: string, options?: NavigateOptions): Fx.Fx<never, Redirect, never> {
|
|
19
|
+
return Fx.fail(new Redirect(url, options))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as Duration from '@effect/data/Duration'
|
|
2
|
+
import { pipe } from '@effect/data/Function'
|
|
3
|
+
import * as Effect from '@effect/io/Effect'
|
|
4
|
+
import * as Scope from '@effect/io/Scope'
|
|
5
|
+
import * as Fx from '@typed/fx'
|
|
6
|
+
import { ElementRef } from '@typed/html'
|
|
7
|
+
import * as Navigation from '@typed/navigation'
|
|
8
|
+
|
|
9
|
+
export interface ScrollRestorationParams<A extends HTMLElement> {
|
|
10
|
+
readonly ref: ElementRef<A>
|
|
11
|
+
readonly behavior?: ScrollBehavior
|
|
12
|
+
readonly retries?: number
|
|
13
|
+
readonly retryDelay?: (depth: number) => Duration.Duration
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ScrollRestorationState = {
|
|
17
|
+
readonly scrollRestoration?: {
|
|
18
|
+
readonly scrollLeft: number
|
|
19
|
+
readonly scrollTop: number
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaultDelay = (depth: number) => Duration.millis(10 * depth)
|
|
24
|
+
|
|
25
|
+
export function ScrollRestoration<A extends HTMLElement>(
|
|
26
|
+
params: ScrollRestorationParams<A>,
|
|
27
|
+
): Fx.Fx<Navigation.Navigation | Scope.Scope, never, null> {
|
|
28
|
+
return Fx.gen(function* ($) {
|
|
29
|
+
const { retries = 3, retryDelay = defaultDelay } = params
|
|
30
|
+
const { onNavigation, onNavigationEnd } = yield* $(Navigation.Navigation)
|
|
31
|
+
|
|
32
|
+
yield* $(
|
|
33
|
+
onNavigation((ev) =>
|
|
34
|
+
Effect.catchTag(
|
|
35
|
+
Effect.gen(function* ($) {
|
|
36
|
+
const state = (ev.destination.state ?? {}) as ScrollRestorationState
|
|
37
|
+
|
|
38
|
+
// If ScrollRestoration is not set, set it
|
|
39
|
+
if (!state.scrollRestoration) {
|
|
40
|
+
const el = yield* $(params.ref.element)
|
|
41
|
+
const scrollLeft = el.scrollLeft
|
|
42
|
+
const scrollTop = el.scrollTop
|
|
43
|
+
|
|
44
|
+
return yield* $(
|
|
45
|
+
Navigation.redirect(ev.destination.url.href, {
|
|
46
|
+
history:
|
|
47
|
+
ev.navigationType === Navigation.NavigationType.Replace ? 'replace' : 'push',
|
|
48
|
+
state: {
|
|
49
|
+
...state,
|
|
50
|
+
scrollRestoration: {
|
|
51
|
+
scrollLeft,
|
|
52
|
+
scrollTop,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
// If there is not Element to scroll, ignore the error
|
|
60
|
+
'NoSuchElementException',
|
|
61
|
+
Effect.succeed,
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
yield* $(
|
|
67
|
+
onNavigationEnd(function restoreScroll(ev, depth = 0): Effect.Effect<never, never, void> {
|
|
68
|
+
return pipe(
|
|
69
|
+
Effect.gen(function* ($) {
|
|
70
|
+
if (depth > retries) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const state = (ev.destination.state ?? {}) as ScrollRestorationState
|
|
75
|
+
|
|
76
|
+
// Restore scroll position on back/forward navigation
|
|
77
|
+
if (
|
|
78
|
+
ev.navigationType === Navigation.NavigationType.Back ||
|
|
79
|
+
ev.navigationType === Navigation.NavigationType.Forward
|
|
80
|
+
) {
|
|
81
|
+
const scrollRestoration = state?.scrollRestoration
|
|
82
|
+
|
|
83
|
+
if (scrollRestoration) {
|
|
84
|
+
const el = yield* $(params.ref.element)
|
|
85
|
+
|
|
86
|
+
el.scroll({
|
|
87
|
+
left: scrollRestoration.scrollLeft,
|
|
88
|
+
top: scrollRestoration.scrollTop,
|
|
89
|
+
behavior: params.behavior || 'smooth',
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}),
|
|
94
|
+
// HACK: This isn't great, we should have some kind of way of letting the
|
|
95
|
+
// navigation know when an external render has finished.
|
|
96
|
+
Effect.catchTag(
|
|
97
|
+
// If there is not Element to scroll, attempt to retry a few times.
|
|
98
|
+
'NoSuchElementException',
|
|
99
|
+
() => {
|
|
100
|
+
const d = depth + 1
|
|
101
|
+
return Effect.delay(restoreScroll(ev, d), retryDelay(d))
|
|
102
|
+
},
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return Fx.succeed(null)
|
|
109
|
+
})
|
|
110
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export * from './router.js'
|
|
2
1
|
export * from './Link.js'
|
|
3
|
-
export * from './
|
|
4
|
-
export * from './
|
|
2
|
+
export * from './Match.js'
|
|
3
|
+
export * from './Matcher.js'
|
|
4
|
+
export * from './matchRoutes.js'
|
|
5
|
+
export * from './Navigation.js'
|
|
6
|
+
export * from './Redirect.js'
|
|
7
|
+
export * from './router.js'
|
|
8
|
+
export * from './ScrollRestoration.js'
|