@real-router/svelte 0.0.1
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/README.md +355 -0
- package/dist/RouterProvider.svelte +51 -0
- package/dist/RouterProvider.svelte.d.ts +10 -0
- package/dist/actions/link.svelte.d.ts +26 -0
- package/dist/actions/link.svelte.js +61 -0
- package/dist/components/Lazy.svelte +47 -0
- package/dist/components/Lazy.svelte.d.ts +10 -0
- package/dist/components/Link.svelte +69 -0
- package/dist/components/Link.svelte.d.ts +18 -0
- package/dist/components/RouteView.svelte +46 -0
- package/dist/components/RouteView.svelte.d.ts +9 -0
- package/dist/composables/useIsActiveRoute.svelte.d.ts +4 -0
- package/dist/composables/useIsActiveRoute.svelte.js +11 -0
- package/dist/composables/useNavigator.svelte.d.ts +2 -0
- package/dist/composables/useNavigator.svelte.js +9 -0
- package/dist/composables/useRoute.svelte.d.ts +2 -0
- package/dist/composables/useRoute.svelte.js +9 -0
- package/dist/composables/useRouteNode.svelte.d.ts +2 -0
- package/dist/composables/useRouteNode.svelte.js +27 -0
- package/dist/composables/useRouteUtils.svelte.d.ts +2 -0
- package/dist/composables/useRouteUtils.svelte.js +7 -0
- package/dist/composables/useRouter.svelte.d.ts +2 -0
- package/dist/composables/useRouter.svelte.js +9 -0
- package/dist/composables/useRouterTransition.svelte.d.ts +4 -0
- package/dist/composables/useRouterTransition.svelte.js +8 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.js +3 -0
- package/dist/createReactiveSource.svelte.d.ts +4 -0
- package/dist/createReactiveSource.svelte.js +14 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +18 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.js +1 -0
- package/package.json +74 -0
- package/src/RouterProvider.svelte +51 -0
- package/src/actions/link.svelte.ts +90 -0
- package/src/components/Lazy.svelte +47 -0
- package/src/components/Link.svelte +69 -0
- package/src/components/RouteView.svelte +46 -0
- package/src/composables/useIsActiveRoute.svelte.ts +22 -0
- package/src/composables/useNavigator.svelte.ts +15 -0
- package/src/composables/useRoute.svelte.ts +15 -0
- package/src/composables/useRouteNode.svelte.ts +33 -0
- package/src/composables/useRouteUtils.svelte.ts +12 -0
- package/src/composables/useRouter.svelte.ts +15 -0
- package/src/composables/useRouterTransition.svelte.ts +16 -0
- package/src/context.ts +5 -0
- package/src/createReactiveSource.svelte.ts +21 -0
- package/src/index.ts +39 -0
- package/src/types.ts +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# @real-router/svelte
|
|
2
|
+
|
|
3
|
+
[](../../LICENSE)
|
|
4
|
+
|
|
5
|
+
> Svelte 5 integration for [Real-Router](https://github.com/greydragon888/real-router) — composables, components, and context providers.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @real-router/svelte @real-router/core @real-router/browser-plugin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Peer dependency:** `svelte` >= 5.7.0
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```svelte
|
|
18
|
+
<!-- App.svelte -->
|
|
19
|
+
<script lang="ts">
|
|
20
|
+
import { createRouter } from "@real-router/core";
|
|
21
|
+
import { browserPluginFactory } from "@real-router/browser-plugin";
|
|
22
|
+
import { RouterProvider, RouteView, Link } from "@real-router/svelte";
|
|
23
|
+
import HomePage from "./HomePage.svelte";
|
|
24
|
+
import UsersPage from "./UsersPage.svelte";
|
|
25
|
+
import NotFoundPage from "./NotFoundPage.svelte";
|
|
26
|
+
|
|
27
|
+
const router = createRouter([
|
|
28
|
+
{ name: "home", path: "/" },
|
|
29
|
+
{
|
|
30
|
+
name: "users",
|
|
31
|
+
path: "/users",
|
|
32
|
+
children: [{ name: "profile", path: "/:id" }],
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
router.usePlugin(browserPluginFactory());
|
|
37
|
+
router.start();
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<RouterProvider {router}>
|
|
41
|
+
<nav>
|
|
42
|
+
<Link routeName="home">Home</Link>
|
|
43
|
+
<Link routeName="users">Users</Link>
|
|
44
|
+
</nav>
|
|
45
|
+
|
|
46
|
+
<RouteView nodeName="">
|
|
47
|
+
{#snippet home()}
|
|
48
|
+
<HomePage />
|
|
49
|
+
{/snippet}
|
|
50
|
+
{#snippet users()}
|
|
51
|
+
<UsersPage />
|
|
52
|
+
{/snippet}
|
|
53
|
+
{#snippet notFound()}
|
|
54
|
+
<NotFoundPage />
|
|
55
|
+
{/snippet}
|
|
56
|
+
</RouteView>
|
|
57
|
+
</RouterProvider>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Composables
|
|
61
|
+
|
|
62
|
+
All composables must be called during component initialization (not inside `$effect` or event handlers). Reactive composables return `{ current: T }` getter objects — read `.current` inside a template or `$derived` to register a reactive dependency.
|
|
63
|
+
|
|
64
|
+
| Composable | Returns | Reactive? |
|
|
65
|
+
| ----------------------- | --------------------------------------------------------------- | ------------------------------------------ |
|
|
66
|
+
| `useRouter()` | `Router` | Never |
|
|
67
|
+
| `useNavigator()` | `Navigator` | Never (stable ref, safe to use directly) |
|
|
68
|
+
| `useRoute()` | `{ navigator, route: { current }, previousRoute: { current } }` | `.current` on every navigation |
|
|
69
|
+
| `useRouteNode(name)` | `{ navigator, route: { current }, previousRoute: { current } }` | `.current` when node activates/deactivates |
|
|
70
|
+
| `useRouteUtils()` | `RouteUtils` | Never |
|
|
71
|
+
| `useRouterTransition()` | `{ current: RouterTransitionSnapshot }` | `.current` on transition start/end |
|
|
72
|
+
|
|
73
|
+
```svelte
|
|
74
|
+
<!-- useRouteNode — updates only when "users.*" changes -->
|
|
75
|
+
<script lang="ts">
|
|
76
|
+
import { useRouteNode } from "@real-router/svelte";
|
|
77
|
+
|
|
78
|
+
const { route } = useRouteNode("users");
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
{#if route.current}
|
|
82
|
+
{#if route.current.name === "users"}
|
|
83
|
+
<UsersList />
|
|
84
|
+
{:else if route.current.name === "users.profile"}
|
|
85
|
+
<UserProfile id={route.current.params.id} />
|
|
86
|
+
{/if}
|
|
87
|
+
{/if}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```svelte
|
|
91
|
+
<!-- useNavigator — stable reference, never reactive -->
|
|
92
|
+
<script lang="ts">
|
|
93
|
+
import { useNavigator } from "@real-router/svelte";
|
|
94
|
+
|
|
95
|
+
const navigator = useNavigator();
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<button onclick={() => navigator.navigate("home")}>Back</button>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```svelte
|
|
102
|
+
<!-- useRouterTransition — progress bars, loading states -->
|
|
103
|
+
<script lang="ts">
|
|
104
|
+
import { useRouterTransition } from "@real-router/svelte";
|
|
105
|
+
|
|
106
|
+
const transition = useRouterTransition();
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
{#if transition.current.isTransitioning}
|
|
110
|
+
<div class="progress-bar"></div>
|
|
111
|
+
{/if}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Components
|
|
115
|
+
|
|
116
|
+
### `<Link>`
|
|
117
|
+
|
|
118
|
+
Navigation link with automatic active state detection. Uses `$derived` for href and class — only the DOM attributes update when active state changes.
|
|
119
|
+
|
|
120
|
+
```svelte
|
|
121
|
+
<Link
|
|
122
|
+
routeName="users.profile"
|
|
123
|
+
routeParams={{ id: "123" }}
|
|
124
|
+
activeClassName="active"
|
|
125
|
+
activeStrict={false}
|
|
126
|
+
ignoreQueryParams={true}
|
|
127
|
+
routeOptions={{ replace: true }}
|
|
128
|
+
>
|
|
129
|
+
View Profile
|
|
130
|
+
</Link>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Props:**
|
|
134
|
+
|
|
135
|
+
| Prop | Type | Default | Description |
|
|
136
|
+
| ------------------- | ------------------- | ----------- | --------------------------------------- |
|
|
137
|
+
| `routeName` | `string` | required | Target route name |
|
|
138
|
+
| `routeParams` | `Params` | `{}` | Route parameters |
|
|
139
|
+
| `routeOptions` | `NavigationOptions` | `{}` | Navigation options (replace, etc.) |
|
|
140
|
+
| `class` | `string` | `undefined` | CSS class |
|
|
141
|
+
| `activeClassName` | `string` | `"active"` | Class added when route is active |
|
|
142
|
+
| `activeStrict` | `boolean` | `false` | Exact match only (no ancestor matching) |
|
|
143
|
+
| `ignoreQueryParams` | `boolean` | `true` | Query params don't affect active state |
|
|
144
|
+
| `target` | `string` | `undefined` | Link target (`_blank`, etc.) |
|
|
145
|
+
|
|
146
|
+
All other props are spread onto the `<a>` element.
|
|
147
|
+
|
|
148
|
+
### `<Lazy>`
|
|
149
|
+
|
|
150
|
+
Lazy-load route content with a fallback component while loading. Useful for code-splitting and dynamic imports.
|
|
151
|
+
|
|
152
|
+
```svelte
|
|
153
|
+
<RouteView nodeName="">
|
|
154
|
+
{#snippet dashboard()}
|
|
155
|
+
<Lazy loader={() => import('./Dashboard.svelte')} fallback={Spinner} />
|
|
156
|
+
{/snippet}
|
|
157
|
+
</RouteView>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Props:**
|
|
161
|
+
|
|
162
|
+
| Prop | Type | Default | Description |
|
|
163
|
+
| ---------- | --------------------------------------- | ----------- | ----------------------------------------- |
|
|
164
|
+
| `loader` | `() => Promise<{ default: Component }>` | required | Async function that imports the component |
|
|
165
|
+
| `fallback` | `Component` | `undefined` | Component to render while loading |
|
|
166
|
+
|
|
167
|
+
The `loader` function should return a dynamic import promise. The `fallback` component is rendered while the import is pending. If an error occurs during loading, an error message is displayed.
|
|
168
|
+
|
|
169
|
+
### `<RouteView>`
|
|
170
|
+
|
|
171
|
+
Declarative route matching. Renders the snippet whose name matches the active route segment.
|
|
172
|
+
|
|
173
|
+
```svelte
|
|
174
|
+
<RouteView nodeName="">
|
|
175
|
+
{#snippet users()}
|
|
176
|
+
<UsersPage />
|
|
177
|
+
{/snippet}
|
|
178
|
+
{#snippet settings()}
|
|
179
|
+
<SettingsPage />
|
|
180
|
+
{/snippet}
|
|
181
|
+
{#snippet notFound()}
|
|
182
|
+
<NotFoundPage />
|
|
183
|
+
{/snippet}
|
|
184
|
+
</RouteView>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Props:**
|
|
188
|
+
|
|
189
|
+
| Prop | Type | Description |
|
|
190
|
+
| ----------- | --------- | ------------------------------------------- |
|
|
191
|
+
| `nodeName` | `string` | Route node to match against. `""` for root. |
|
|
192
|
+
| `notFound` | `Snippet` | Rendered when route is `UNKNOWN_ROUTE` |
|
|
193
|
+
| `[segment]` | `Snippet` | Named snippet matching a route segment |
|
|
194
|
+
|
|
195
|
+
Snippet names must be valid JavaScript identifiers and match the first segment of the active route after `nodeName`. For a route `users.profile` with `nodeName=""`, the snippet named `users` matches.
|
|
196
|
+
|
|
197
|
+
> **Note:** `keepAlive` is not supported. Svelte has no equivalent of React's `<Activity>` API or Vue's `<KeepAlive>`. Components are destroyed when navigating away.
|
|
198
|
+
|
|
199
|
+
## Actions
|
|
200
|
+
|
|
201
|
+
### `createLinkAction`
|
|
202
|
+
|
|
203
|
+
Factory function that creates a low-level action for adding navigation to any element. Must be called during component initialization to capture the router context.
|
|
204
|
+
|
|
205
|
+
```svelte
|
|
206
|
+
<script lang="ts">
|
|
207
|
+
import { createLinkAction } from "@real-router/svelte";
|
|
208
|
+
|
|
209
|
+
const link = createLinkAction();
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
<a use:link={{ name: "users.profile", params: { id: "123" } }}>
|
|
213
|
+
User Profile
|
|
214
|
+
</a>
|
|
215
|
+
|
|
216
|
+
<button use:link={{ name: "home" }}>
|
|
217
|
+
Go Home
|
|
218
|
+
</button>
|
|
219
|
+
|
|
220
|
+
<div use:link={{ name: "settings", params: {}, options: { replace: true } }} role="link" tabindex="0">
|
|
221
|
+
Settings
|
|
222
|
+
</div>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Parameters:**
|
|
226
|
+
|
|
227
|
+
| Property | Type | Default | Description |
|
|
228
|
+
| --------- | -------- | ------- | ---------------------------------- |
|
|
229
|
+
| `name` | `string` | — | Target route name |
|
|
230
|
+
| `params` | `Params` | `{}` | Route parameters |
|
|
231
|
+
| `options` | `object` | `{}` | Navigation options (replace, etc.) |
|
|
232
|
+
|
|
233
|
+
The action automatically adds `role="link"` + `tabindex="0"` to non-interactive elements for accessibility. It handles click events and Enter key navigation.
|
|
234
|
+
|
|
235
|
+
## Reactive Primitives
|
|
236
|
+
|
|
237
|
+
### `createReactiveSource`
|
|
238
|
+
|
|
239
|
+
Public building block that bridges any `RouterSource<T>` to Svelte's reactivity system. Returns a `{ current: T }` getter object that lazily subscribes via `createSubscriber`.
|
|
240
|
+
|
|
241
|
+
```svelte
|
|
242
|
+
<script lang="ts">
|
|
243
|
+
import { createReactiveSource, useRouter } from "@real-router/svelte";
|
|
244
|
+
import { createActiveRouteSource } from "@real-router/sources";
|
|
245
|
+
|
|
246
|
+
const router = useRouter();
|
|
247
|
+
const isActive = createReactiveSource(
|
|
248
|
+
createActiveRouteSource(router, "users.profile", {})
|
|
249
|
+
);
|
|
250
|
+
</script>
|
|
251
|
+
|
|
252
|
+
{#if isActive.current}
|
|
253
|
+
<span class="badge">Active</span>
|
|
254
|
+
{/if}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Use cases: custom active route indicators, domain-specific composables, integration with other reactive primitives.
|
|
258
|
+
|
|
259
|
+
## Svelte-Specific Patterns
|
|
260
|
+
|
|
261
|
+
### Reading .current in Reactive Contexts
|
|
262
|
+
|
|
263
|
+
Unlike Vue (ShallowRefs) or Solid (Accessors), Svelte composables return `{ current: T }` getter objects. Read `.current` inside a template or `$derived` to register a reactive dependency:
|
|
264
|
+
|
|
265
|
+
```svelte
|
|
266
|
+
<script lang="ts">
|
|
267
|
+
import { useRoute } from "@real-router/svelte";
|
|
268
|
+
|
|
269
|
+
const { route } = useRoute();
|
|
270
|
+
|
|
271
|
+
// CORRECT — $derived registers a reactive dependency
|
|
272
|
+
const routeName = $derived(route.current?.name);
|
|
273
|
+
|
|
274
|
+
// WRONG — read outside reactive context, no subscription
|
|
275
|
+
console.log(route.current?.name);
|
|
276
|
+
</script>
|
|
277
|
+
|
|
278
|
+
<!-- CORRECT — template is a reactive context -->
|
|
279
|
+
<p>{route.current?.name}</p>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Reacting to Route Changes
|
|
283
|
+
|
|
284
|
+
Use `$effect` to run side effects when the route changes:
|
|
285
|
+
|
|
286
|
+
```svelte
|
|
287
|
+
<script lang="ts">
|
|
288
|
+
import { useRouteNode } from "@real-router/svelte";
|
|
289
|
+
|
|
290
|
+
const { route } = useRouteNode("users");
|
|
291
|
+
|
|
292
|
+
$effect(() => {
|
|
293
|
+
if (route.current) {
|
|
294
|
+
document.title = `Users — ${route.current.params.id ?? "list"}`;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
</script>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Nested RouteView
|
|
301
|
+
|
|
302
|
+
For nested routes, use `RouteView` at each level with the appropriate `nodeName`:
|
|
303
|
+
|
|
304
|
+
```svelte
|
|
305
|
+
<!-- Top-level: matches "users", "settings", etc. -->
|
|
306
|
+
<RouteView nodeName="">
|
|
307
|
+
{#snippet users()}
|
|
308
|
+
<!-- Nested: matches "users.list", "users.profile", etc. -->
|
|
309
|
+
<RouteView nodeName="users">
|
|
310
|
+
{#snippet list()}
|
|
311
|
+
<UsersList />
|
|
312
|
+
{/snippet}
|
|
313
|
+
{#snippet profile()}
|
|
314
|
+
<UserProfile />
|
|
315
|
+
{/snippet}
|
|
316
|
+
</RouteView>
|
|
317
|
+
{/snippet}
|
|
318
|
+
</RouteView>
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Accessibility
|
|
322
|
+
|
|
323
|
+
Enable screen reader announcements for route changes:
|
|
324
|
+
|
|
325
|
+
```svelte
|
|
326
|
+
<RouterProvider {router} announceNavigation>
|
|
327
|
+
{/* Your app */}
|
|
328
|
+
</RouterProvider>
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
When enabled, a visually hidden `aria-live` region announces each navigation. Focus moves to the first `<h1>` on the new page. See [Accessibility guide](https://github.com/greydragon888/real-router/wiki/Accessibility) for details.
|
|
332
|
+
|
|
333
|
+
## Documentation
|
|
334
|
+
|
|
335
|
+
Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki)
|
|
336
|
+
|
|
337
|
+
- [RouterProvider](https://github.com/greydragon888/real-router/wiki/RouterProvider) · [RouteView](https://github.com/greydragon888/real-router/wiki/RouteView) · [Link](https://github.com/greydragon888/real-router/wiki/Link)
|
|
338
|
+
- [useRouter](https://github.com/greydragon888/real-router/wiki/useRouter) · [useRoute](https://github.com/greydragon888/real-router/wiki/useRoute) · [useRouteNode](https://github.com/greydragon888/real-router/wiki/useRouteNode) · [useNavigator](https://github.com/greydragon888/real-router/wiki/useNavigator) · [useRouteUtils](https://github.com/greydragon888/real-router/wiki/useRouteUtils) · [useRouterTransition](https://github.com/greydragon888/real-router/wiki/useRouterTransition)
|
|
339
|
+
|
|
340
|
+
## Related Packages
|
|
341
|
+
|
|
342
|
+
| Package | Description |
|
|
343
|
+
| ---------------------------------------------------------------------------------------- | ------------------------------------ |
|
|
344
|
+
| [@real-router/core](https://www.npmjs.com/package/@real-router/core) | Core router (required dependency) |
|
|
345
|
+
| [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) | Browser History API integration |
|
|
346
|
+
| [@real-router/sources](https://www.npmjs.com/package/@real-router/sources) | Subscription layer (used internally) |
|
|
347
|
+
| [@real-router/route-utils](https://www.npmjs.com/package/@real-router/route-utils) | Route tree queries (`useRouteUtils`) |
|
|
348
|
+
|
|
349
|
+
## Contributing
|
|
350
|
+
|
|
351
|
+
See [contributing guidelines](../../CONTRIBUTING.md) for development setup and PR process.
|
|
352
|
+
|
|
353
|
+
## License
|
|
354
|
+
|
|
355
|
+
[MIT](../../LICENSE) © [Oleg Ivanov](https://github.com/greydragon888)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getNavigator } from "@real-router/core";
|
|
3
|
+
import { createRouteSource } from "@real-router/sources";
|
|
4
|
+
import { createRouteAnnouncer } from "dom-utils";
|
|
5
|
+
import { setContext } from "svelte";
|
|
6
|
+
|
|
7
|
+
import { createReactiveSource } from "./createReactiveSource.svelte";
|
|
8
|
+
import { NAVIGATOR_KEY, ROUTE_KEY, ROUTER_KEY } from "./context";
|
|
9
|
+
|
|
10
|
+
import type { Router } from "@real-router/core";
|
|
11
|
+
import type { Snippet } from "svelte";
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
router,
|
|
15
|
+
children,
|
|
16
|
+
announceNavigation,
|
|
17
|
+
}: { router: Router; children: Snippet; announceNavigation?: boolean } =
|
|
18
|
+
$props();
|
|
19
|
+
|
|
20
|
+
$effect(() => {
|
|
21
|
+
if (!announceNavigation) return;
|
|
22
|
+
const announcer = createRouteAnnouncer(router);
|
|
23
|
+
return () => announcer.destroy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const navigator = getNavigator(router);
|
|
27
|
+
const source = createRouteSource(router);
|
|
28
|
+
const reactive = createReactiveSource(source);
|
|
29
|
+
|
|
30
|
+
setContext(ROUTER_KEY, router);
|
|
31
|
+
setContext(NAVIGATOR_KEY, navigator);
|
|
32
|
+
setContext(ROUTE_KEY, {
|
|
33
|
+
navigator,
|
|
34
|
+
get route() {
|
|
35
|
+
return {
|
|
36
|
+
get current() {
|
|
37
|
+
return reactive.current.route;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
get previousRoute() {
|
|
42
|
+
return {
|
|
43
|
+
get current() {
|
|
44
|
+
return reactive.current.previousRoute;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
{@render children()}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Router } from "@real-router/core";
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
router: Router;
|
|
5
|
+
children: Snippet;
|
|
6
|
+
announceNavigation?: boolean;
|
|
7
|
+
};
|
|
8
|
+
declare const RouterProvider: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type RouterProvider = ReturnType<typeof RouterProvider>;
|
|
10
|
+
export default RouterProvider;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ActionReturn } from "svelte/action";
|
|
2
|
+
import type { Params, NavigationOptions } from "@real-router/core";
|
|
3
|
+
export interface LinkActionParams {
|
|
4
|
+
name: string;
|
|
5
|
+
params?: Params;
|
|
6
|
+
options?: NavigationOptions;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Factory function that captures router context during component initialization.
|
|
10
|
+
* Must be called during component init (not inside event handlers or effects).
|
|
11
|
+
*
|
|
12
|
+
* @returns Action function for use with `use:` directive
|
|
13
|
+
* @throws Error if called outside RouterProvider
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```svelte
|
|
17
|
+
* <script>
|
|
18
|
+
* import { createLinkAction } from '@real-router/svelte';
|
|
19
|
+
* const link = createLinkAction();
|
|
20
|
+
* </script>
|
|
21
|
+
*
|
|
22
|
+
* <button use:link={{ name: 'home' }}>Home</button>
|
|
23
|
+
* <a use:link={{ name: 'users', params: { id: '123' } }}>User Profile</a>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function createLinkAction(): (node: HTMLElement, params: LinkActionParams) => ActionReturn<LinkActionParams>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getContext } from "svelte";
|
|
2
|
+
import { ROUTER_KEY } from "../context";
|
|
3
|
+
import { shouldNavigate, applyLinkA11y } from "dom-utils";
|
|
4
|
+
/**
|
|
5
|
+
* Factory function that captures router context during component initialization.
|
|
6
|
+
* Must be called during component init (not inside event handlers or effects).
|
|
7
|
+
*
|
|
8
|
+
* @returns Action function for use with `use:` directive
|
|
9
|
+
* @throws Error if called outside RouterProvider
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```svelte
|
|
13
|
+
* <script>
|
|
14
|
+
* import { createLinkAction } from '@real-router/svelte';
|
|
15
|
+
* const link = createLinkAction();
|
|
16
|
+
* </script>
|
|
17
|
+
*
|
|
18
|
+
* <button use:link={{ name: 'home' }}>Home</button>
|
|
19
|
+
* <a use:link={{ name: 'users', params: { id: '123' } }}>User Profile</a>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function createLinkAction() {
|
|
23
|
+
const router = getContext(ROUTER_KEY);
|
|
24
|
+
if (!router) {
|
|
25
|
+
throw new Error("createLinkAction must be called inside a RouterProvider");
|
|
26
|
+
}
|
|
27
|
+
return function link(node, params) {
|
|
28
|
+
let currentParams = params;
|
|
29
|
+
applyLinkA11y(node);
|
|
30
|
+
function handleClick(evt) {
|
|
31
|
+
if (!shouldNavigate(evt))
|
|
32
|
+
return;
|
|
33
|
+
evt.preventDefault();
|
|
34
|
+
// router is guaranteed to exist due to check in factory
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
36
|
+
router
|
|
37
|
+
.navigate(currentParams.name, currentParams.params ?? {}, currentParams.options ?? {})
|
|
38
|
+
.catch(() => { });
|
|
39
|
+
}
|
|
40
|
+
function handleKeyDown(evt) {
|
|
41
|
+
if (evt.key === "Enter" && !(node instanceof HTMLButtonElement)) {
|
|
42
|
+
// router is guaranteed to exist due to check in factory
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
44
|
+
router
|
|
45
|
+
.navigate(currentParams.name, currentParams.params ?? {}, currentParams.options ?? {})
|
|
46
|
+
.catch(() => { });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
node.addEventListener("click", handleClick);
|
|
50
|
+
node.addEventListener("keydown", handleKeyDown);
|
|
51
|
+
return {
|
|
52
|
+
update(newParams) {
|
|
53
|
+
currentParams = newParams;
|
|
54
|
+
},
|
|
55
|
+
destroy() {
|
|
56
|
+
node.removeEventListener("click", handleClick);
|
|
57
|
+
node.removeEventListener("keydown", handleKeyDown);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component } from "svelte";
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
loader,
|
|
6
|
+
fallback,
|
|
7
|
+
}: {
|
|
8
|
+
loader: () => Promise<{ default: Component }>;
|
|
9
|
+
fallback?: Component | undefined;
|
|
10
|
+
} = $props();
|
|
11
|
+
|
|
12
|
+
let LoadedComponent = $state<Component | null>(null);
|
|
13
|
+
let error = $state<Error | null>(null);
|
|
14
|
+
let loading = $state(true);
|
|
15
|
+
|
|
16
|
+
$effect(() => {
|
|
17
|
+
loading = true;
|
|
18
|
+
error = null;
|
|
19
|
+
LoadedComponent = null;
|
|
20
|
+
let active = true;
|
|
21
|
+
|
|
22
|
+
loader()
|
|
23
|
+
.then((module) => {
|
|
24
|
+
if (!active) return;
|
|
25
|
+
LoadedComponent = module.default;
|
|
26
|
+
loading = false;
|
|
27
|
+
})
|
|
28
|
+
.catch((err) => {
|
|
29
|
+
if (!active) return;
|
|
30
|
+
error = err;
|
|
31
|
+
loading = false;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
active = false;
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
{#if loading && fallback}
|
|
41
|
+
{@const Fallback = fallback}
|
|
42
|
+
<Fallback />
|
|
43
|
+
{:else if error}
|
|
44
|
+
<p>Error loading component: {error.message}</p>
|
|
45
|
+
{:else if LoadedComponent}
|
|
46
|
+
<LoadedComponent />
|
|
47
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
loader: () => Promise<{
|
|
4
|
+
default: Component;
|
|
5
|
+
}>;
|
|
6
|
+
fallback?: Component | undefined;
|
|
7
|
+
};
|
|
8
|
+
declare const Lazy: Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type Lazy = ReturnType<typeof Lazy>;
|
|
10
|
+
export default Lazy;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useIsActiveRoute } from "../composables/useIsActiveRoute.svelte";
|
|
3
|
+
import { useRouter } from "../composables/useRouter.svelte";
|
|
4
|
+
import { shouldNavigate, buildHref, buildActiveClassName } from "dom-utils";
|
|
5
|
+
|
|
6
|
+
import type { NavigationOptions, Params } from "@real-router/core";
|
|
7
|
+
import type { Snippet } from "svelte";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
routeName,
|
|
11
|
+
routeParams = {} as Params,
|
|
12
|
+
routeOptions = {} as NavigationOptions,
|
|
13
|
+
class: className = undefined,
|
|
14
|
+
activeClassName = "active",
|
|
15
|
+
activeStrict = false,
|
|
16
|
+
ignoreQueryParams = true,
|
|
17
|
+
target = undefined,
|
|
18
|
+
children = undefined,
|
|
19
|
+
onclick: userOnClick = undefined,
|
|
20
|
+
...restProps
|
|
21
|
+
}: {
|
|
22
|
+
routeName: string;
|
|
23
|
+
routeParams?: Params;
|
|
24
|
+
routeOptions?: NavigationOptions;
|
|
25
|
+
class?: string;
|
|
26
|
+
activeClassName?: string;
|
|
27
|
+
activeStrict?: boolean;
|
|
28
|
+
ignoreQueryParams?: boolean;
|
|
29
|
+
target?: string;
|
|
30
|
+
children?: Snippet;
|
|
31
|
+
onclick?: (evt: MouseEvent) => void;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
} = $props();
|
|
34
|
+
|
|
35
|
+
const router = useRouter();
|
|
36
|
+
const activeState = useIsActiveRoute(
|
|
37
|
+
routeName,
|
|
38
|
+
routeParams,
|
|
39
|
+
activeStrict,
|
|
40
|
+
ignoreQueryParams,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const href = $derived(buildHref(router, routeName, routeParams));
|
|
44
|
+
|
|
45
|
+
const finalClassName = $derived(
|
|
46
|
+
buildActiveClassName(activeState.current, activeClassName, className),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
function handleClick(evt: MouseEvent) {
|
|
50
|
+
if (userOnClick) {
|
|
51
|
+
userOnClick(evt);
|
|
52
|
+
|
|
53
|
+
if (evt.defaultPrevented) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!shouldNavigate(evt) || target === "_blank") {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
evt.preventDefault();
|
|
63
|
+
router.navigate(routeName, routeParams, routeOptions).catch(() => {});
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<a {href} class={finalClassName} {target} onclick={handleClick} {...restProps}>
|
|
68
|
+
{@render children?.()}
|
|
69
|
+
</a>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NavigationOptions, Params } from "@real-router/core";
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
routeName: string;
|
|
5
|
+
routeParams?: Params;
|
|
6
|
+
routeOptions?: NavigationOptions;
|
|
7
|
+
class?: string;
|
|
8
|
+
activeClassName?: string;
|
|
9
|
+
activeStrict?: boolean;
|
|
10
|
+
ignoreQueryParams?: boolean;
|
|
11
|
+
target?: string;
|
|
12
|
+
children?: Snippet;
|
|
13
|
+
onclick?: (evt: MouseEvent) => void;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
declare const Link: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
17
|
+
type Link = ReturnType<typeof Link>;
|
|
18
|
+
export default Link;
|