Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 94 additions & 92 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

> **📝 Small and Unique!**
>
> + Less than **1,400** lines of code, including TypeScript typing.
> + Less than **1,450** lines of code, including TypeScript typing.
> + Always-on path and hash routing. Simultaneous and independent routing modes.
> + The router that invented multi hash routing.
> + **NEW!** Supports extension packages (Sveltekit support coming soon)
> + **NEW!** Supports Sveltekit (via [@wjfe/n-savant-sk](https://github.com/WJSoftware/wjfe-n-savant-sk))

+ **Electron support**: Works with Electron (all routing modes)
+ **Reactivity-based**: All data is reactive, reducing the need for events and imperative programming.
Expand Down Expand Up @@ -133,27 +133,27 @@ For applications that also run in the browser, condition the navigation to Elect

```svelte
<script lang="ts">
import { Router, Route } from "@wjfe/n-savant";
import NavBar from "./lib/NavBar.svelte";
import UserView from "./lib/UserView.svelte";
import { Router, Route } from "@wjfe/n-savant";
import NavBar from "./lib/NavBar.svelte";
import UserView from "./lib/UserView.svelte";
</script>

<Router>
<NavBar />
<div class="container">
<!-- content outside routes is always rendered -->
<h1>Routing Demo</h1>
<Route key="users" path="/users">
<!-- content here -->
</Route>
<Route key="user" path="/users/:userId">
<!-- access parameters via the snippet parameter -->
{#snippet children(params)}
<UserView id={params.userId} /> <!-- Intellisense will work here!! -->
{/snippet}
</Route>
...
</div>
<NavBar />
<div class="container">
<!-- content outside routes is always rendered -->
<h1>Routing Demo</h1>
<Route key="users" path="/users">
<!-- content here -->
</Route>
<Route key="user" path="/users/:userId">
<!-- access parameters via the snippet parameter -->
{#snippet children(params)}
<UserView id={params.userId} /> <!-- Intellisense will work here!! -->
{/snippet}
</Route>
...
</div>
</Router>
```

Expand All @@ -165,18 +165,20 @@ functionality. Still, this is not mandatory.
```svelte
<!-- NavBar.svelte -->
<script lang="ts">
import { Link } from "@wjfe/n-savant";
import { Link } from "@wjfe/n-savant";
</script>

<nav>
<div class="nav-links">
<ul>
<li class="nav-link">
<Link href="/users" activeState={{ key: 'users', class: 'active' }}>All Users</Link>
</li>
...
</ul>
</div>
<div class="nav-links">
<ul>
<li class="nav-link">
<Link href="/users" activeFor="users" activeState={{ class: 'active' }}>
All Users
</Link>
</li>
...
</ul>
</div>
</nav>
```

Expand All @@ -189,7 +191,7 @@ strategies that are possible with this router.

Routers always evaluate all defined routes, so it is possible for more than one route to match. This facilitates the
layout of micro-frontends. For example, a navigation micro-frontend could be inside a route that either always matches
or matches most of the time, so navigation links are available the mayority/all of the time.
or matches most of the time, so navigation links are available the majority/all of the time.

### Simultaneous, Always-On Path and Hash Routing

Expand All @@ -201,13 +203,13 @@ name, while specialty MFE's could route using the path in the hash part of the U

### Multi-Hash Routing

As of Februrary 2025, no other router in the world can do this.
As of February 2025, no other router in the world can do this.

Imagine a scenario where your MFE application would like to show side-by-side two micro-frontends that are
router-enabled (meaning they use or need to work with a path). With traditional routing, you could not have this setup
because one MFE would take over the path, leaving the other MFE without one.

Mutli-hash routing creates named paths in the hash value, giving routers the ability to share the hash value with other
Multi-hash routing creates named paths in the hash value, giving routers the ability to share the hash value with other
routers. A hash value of the form `#path1=/path/1;path2=/path/2;...` could power side-by-side MFE's on, say, 4K
layouts.

Expand All @@ -218,7 +220,7 @@ abandon the use of `registerApplication()` and `start()` and just mount parcels

[single-spa](https://single-spa.js.org)

## Unintrusive Philosophy
## Unobtrusive Philosophy

This mini router library imposes minimal restrictions. Here are some features provided by other much larger codebases
that are not provided here because Svelte already has the capability.
Expand All @@ -229,11 +231,11 @@ Nothing prevents you to add transitions to anything.

```svelte
<Route key="users" path="/users/:userId">
{#snippet children(params)}
<div transition:fade>
...
</div>
{/snippet}
{#snippet children(params)}
<div transition:fade>
...
</div>
{/snippet}
</Route>
```

Expand All @@ -252,7 +254,7 @@ the `rest` parameter specifier (`/*`):

```svelte
<Route key="admin" path="/admin/*">
...
...
</Route>
```

Expand All @@ -265,19 +267,19 @@ Lazy-loading components is very simple:

```svelte
<script lang="ts">
function loadUsersComponent() {
return import('./lib/Users.svelte').then(m => m.default);
}
function loadUsersComponent() {
return import('./lib/Users.svelte').then(m => m.default);
}
</script>

<Route key="users" path="/users">
{#await loadUsersComponent()}
<span>Loading...</span>
{:then Users}
<Users />
{:catch}
<p>Ooops!</p>
{/await}
{#await loadUsersComponent()}
<span>Loading...</span>
{:then Users}
<Users />
{:catch}
<p>Oops!</p>
{/await}
</Route>
```

Expand All @@ -291,13 +293,13 @@ import { location } from "@wjfe/n-savant";

// Or $derived, whichever you need.
$effect(() => {
// Read location.url to re-run on URL changes (navigation).
location.url;
// Read location.state to re-run on state changes.
location.state;
// Read location.hashPaths to re-run on hash changes (hash navigation).
// The route named "single" is the one you want if doing hash routing.
location.hashPaths.single;
// Read location.url to re-run on URL changes (navigation).
location.url;
// Read location.state to re-run on state changes.
location.state;
// Read location.hashPaths to re-run on hash changes (hash navigation).
// The route named "single" is the one you want if doing hash routing.
location.hashPaths.single;
});
```

Expand All @@ -317,25 +319,25 @@ numeric parameter value uses the `and` property to type-check the value:

```svelte
<Route path="/users/:userId" and={(rp) => typeof rp.userId === 'number'}>
{#snippet children(rp)}
<UserDetails userId={rp.userId} />
{/snippet}
{#snippet children(rp)}
<UserDetails userId={rp.userId} />
{/snippet}
</Route>
<Route path="/users/summary">
<UsersSummary />
<UsersSummary />
</Route>
```

This is the version using a regular expression for the `path` property:

```svelte
<Route path={/\/users\/(?<userId>\d+)/i}>
{#snippet children(rp)}
<UserDetails userId={rp.userId} />
{/snippet}
{#snippet children(rp)}
<UserDetails userId={rp.userId} />
{/snippet}
</Route>
<Route path="/users/summary">
<UsersSummary />
<UsersSummary />
</Route>
```

Expand All @@ -346,23 +348,23 @@ property of router engines (which is reactive) by binding to a router's `router`

```svelte
<script lang="ts">
import { RouterEngine } from "@wjfe/n-savant/core";

let router: $state<RouterEngine>();

$effect(() => {
for (let [key, rs] of Object.entries(router.routeStatus)) {
// key: Route's key
// rs: RouteStatus for the route.
if (rs.match) {
// Do stuff with rs.routeParams, for example.
}
}
});
import { RouterEngine } from "@wjfe/n-savant/core";

let router: $state<RouterEngine>();

$effect(() => {
for (let [key, rs] of Object.entries(router.routeStatus)) {
// key: Route's key
// rs: RouteStatus for the route.
if (rs.match) {
// Do stuff with rs.routeParams, for example.
}
}
});
</script>

<Router bind:router>
...
...
</Router>
```

Expand All @@ -384,29 +386,29 @@ import { location } from "@wjfe/n-savant";

// Path routing navigation:
location.navigate('/new/path', {
replace: true,
state: { custom: 'Hi' },
hash: false
replace: true,
state: { custom: 'Hi' },
hash: false
});

// Hash routing navigation:
location.navigate('/new/path', {
replace: true,
state: { custom: 'Hi' },
hash: true
replace: true,
state: { custom: 'Hi' },
hash: true
});

// Multi-hash routing navigation:
location.navigate('/new/path', {
replace: true,
state: { custom: 'Hi' },
hash: 'path1'
replace: true,
state: { custom: 'Hi' },
hash: 'path1'
});

// Preserve existing query parameters:
location.navigate('/new/path', {
preserveQuery: true,
hash: false
preserveQuery: true,
hash: false
});
```

Expand All @@ -424,16 +426,16 @@ import { location } from "@wjfe/n-savant";

// Direct URL navigation:
location.goTo('https://example.com/new/path', {
replace: true,
state: { path: undefined, hash: {} } // Must provide complete State object
replace: true,
state: { path: undefined, hash: {} } // Must provide complete State object
});

// Shallow routing (navigate to current URL):
location.goTo('', { replace: true });

// Preserve query parameters:
location.goTo('/new/path', {
preserveQuery: ['param1', 'param2']
preserveQuery: ['param1', 'param2']
});
```

Expand Down
1 change: 1 addition & 0 deletions src/lib/Fallback/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ route status data is calculated.
| Property | Type | Default Value | Bindable | Description |
|-|-|-|-|-|
| `hash` | `boolean \| string` | `undefined` | | Sets the hash mode of the component. |
| `when` | `WhenPredicate` | `undefined` | | Overrides the default activation conditions for the fallback content inside the component. |
| `children` | `Snippet<[any, Record<string, RouteStatus>]>` | `undefined` | | Renders the children of the component. |

[Online Documentation](https://wjfe-n-savant.hashnode.space/wjfe-n-savant/components/fallback)
Expand Down
28 changes: 15 additions & 13 deletions src/lib/Link/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ SPA-friendly navigation (navigation without reloading).
| `href` | `string` | (none) | | Sets the URL to navigate to. |
| `replace` | `boolean` | `false` | | Configures the link so it replaces the current URL as opposed to pushing the URL as a new entry in the browser's History API. |
| `state` | `any` | `undefined` | | Sets the state object to pass to the browser's History API when pushing or replacing the URL. |
| `activeFor` | `string` | `undefined` | | Sets the route key that the link will use to determine if it should render as active. |
| `activeState` | `ActiveState` | `undefined` | | Sets the various options that are used to automatically style the anchor tag whenever a particular route becomes active. |
| `prependBasePath` | `boolean` | `false` | | Configures the component to prepend the parent router's base path to the `href` property. |
| `preserveQuery` | `PreserveQuery` | `false` | | Configures the component to preserve the query string whenever it triggers navigation. |
Expand All @@ -30,8 +31,8 @@ These don't require a parent router:
<Link hash="true" href="/new/path">Hash Routing => https://example.com/#/new/path</Link>

<Link hash="path1" href="/new/path">
Multi Hash Routing => https://example.com/#path1=/new/path
Will also preserve any other named paths
Multi Hash Routing => https://example.com/#path1=/new/path
Will also preserve any other named paths
</Link>
```

Expand All @@ -42,16 +43,17 @@ automatically trigger its active appearance based on a specific route becoming a

```svelte
<Router basePath="/some/base">
<Link
hash="path1"
href="/admin/users"
prependBasePath
activeState={{ key: 'adminUsers', class: 'active', }}
>
Click Me!
</Link>
<Route key="adminUsers" path="/admin/users">
...
</Route>
<Link
hash="path1"
href="/admin/users"
prependBasePath
activeFor="adminUsers"
activeState={{ class: 'active', aria: { 'aria-current': 'page' } }}
>
Click Me!
</Link>
<Route key="adminUsers" path="/admin/users">
...
</Route>
</Router>
```
Loading