A completely different approach of routing. State-based router suggests that routing is just another global state and History API changes are just an optional side-effects of this state.
- Zero-config!
- Just another global state. Ultimate freedom how to apply this state to your app!
- Juggling of different parts of URL (path/query/hash) effective and granularly.
- Automatic parsing of the
query
params, optional parsingpath
params. - Helpers to work with navigation, links, and even html forms.
npm i svelte-pathfinder --save-dev
yarn add svelte-pathfinder
CDN: UNPKG | jsDelivr (available as window.Pathfinder
)
<script src="https://unpkg.com/svelte-pathfinder/dist/pathfinder.min.js"></script>
<!-- OR in modern browsers -->
<script type="module" src="https://unpkg.com/svelte-pathfinder/dist/pathfinder.min.mjs"></script>
/path
?query
#fragment
path
- represents path segments of the URL as an array.
path: Writable<[]>
query
- represents query params of the URL as an object.
query: Writable<{}>
fragment
- represents fragment (hash) string of URL.
fragment: Writable<string>
state
- represents state object associated with the new history entry created by pushState().
state: Writable<{}>
url
- represents full URL string.
url: Readable<string>
pattern
- function to match path patterns and return read-only params
object or null
.
pattern: Readable<<T extends {}>(pattern?: string, options?: ParseParamsOptions) => T | null>
paramable
- constructor of custom params
stores to parse path patterns and manipulate path parameters.
paramable: <T extends {}>(pattern?: string, options?: ParseParamsOptions): Writable<T>;
goto
- perform navigation to the next router state by URL.
goto(url: string, state?: {});
back
- perform navigation to the previous router state.
back(path?: string)
redirect
- update current url without new history record.
redirect(url: string, state?: {})
click
- handle click event from the link and perform navigation to its targets.
click(event: MouseEvent)
submit
- handle submit event from the GET-form and perform navigation using its inputs.
submit(event: SubmitEvent)
prefs
- preferences objectsideEffect
- manually disable/enable History API usage (changing URL) (default: auto).hashbang
- manually activate hashbang-routing.basePath
- set base path if web app is located within a nested basepath.convertTypes
- disable converting types when parsing query/path parameters (default: true).nesting
- number of levels when pasring nested objects in query parameters (default: 3).array.format
- format for arrays in query parameters (possible values: 'bracket' (default), 'separator').array.separator
- if format isseparator
this field give you speficy which separator you want to use (default: ',').
To change the preferences, just import and change them somewhere on top of your code:
import { prefs } from 'svelte-pathfinder';
prefs.convertTypes = false;
prefs.array.format = 'separator';
{#if params = $pattern('/products/:id')} <!-- eg. /products/1 -->
<ProductView productId={params.id} />
{:else if params = $pattern('/products')} <!-- eg. /products?page=2&q=Apple -->
<ProductsList page={$query.page} search={$query.q} />
{:else}
<NotFound path={$path} />
{/if}
<Modal open={$fragment === '#login'}>
<LoginForm />
</Modal>
<script>
import { path, query, fragment, pattern } from 'svelte-pathfinder';
let params;
</script>
{#if page}
<svelte:component this={page.component} {params} />
{/if}
<script>
...
import { path, pattern } from 'svelte-pathfinder';
import routes from './routes.js'; // [{ pattern: '/products', component: ProductsList }, ...]
...
let params;
$: page = routes.find((route) => params = $pattern(route.pattern)) || null; // match path pattens and get parsed params
...
$: if ($path[0] === 'admin' && ! isAuthorized) { // check any specific segment of the path
$path = '/forbidden'; // re-write whole path
}
</script>
<script>
...
$query.page = 10; // set ?page=10 without changing or loose other query params
...
$path[1] = 4; // set second segment of the path, e.g. /products/4
...
$fragment = 'login'; // set url hash to #login
...
$state = { restoreOnBack: 'something' }; // set history record related state object
</script>
<input bind:value={$query.q} placeholder="Search product...">
...
<button on:click={() => $fragment = 'login'}>Login</button>
...
<a href="/products/{product.id}" on:click|preventDefault={e => $path[1] = product.id}>
{product.title}
</a>
// ./stores/params.js
import { paramable } from 'svelte-pathfinder';
export const productPageParams = paramable('/products/:category/:productId?');
...
<select bind:value={$params.category}>
<option value="all">All</option>
{#each categories as category}
<option value={category.slug}>{category.name}</option>
{/each}
</select>
...
{#each products as product}
<a href="/products/{product.id}" on:click|preventDefault={e => $params.productId = product.id}>
{product.title}
</a>
{/each}
<script>
import { productPageParams as params } from './store/params';
</script>
import { derived } from 'svelte/store';
import asyncable from 'svelte-asyncable';
import { path, query } from 'svelte-pathfinder';
import { productPageParams } from './store/params';
// with regular derived store
export const productData = derived(productPageParams, ($params, set) => {
if ($params.productId}) {
fetch(`/api/products/${$params.productId}`)
.then(res => res.json())
.then(set);
}
}, {});
// with svelte-asyncable
export const productsList = asyncable(async $query => {
const res = await fetch(`/api/products${$query}`)
return res.json();
}, undefined, [ query ]);
Auto-handling all links in the application.
<svelte:window on:click={click} />
<!-- links below will be handled by `click` helper -->
<nav class="navigate">
<a href="/">Home</a>
<a href="/products">Products</a>
<a href="/about">About</a>
</nav>
<!-- links below will be EXCLUDED from the navigation -->
<nav class="not-navigate">
<a href="http://google.com">External links</a>
<a href="/shortlink/2hkjhrfwgsd" rel="external">Links with external rel</a>
<a href="/products" target="_blank">Any links with target</a>
<a href="/" target="_self">Any links with target even to _self</a>
<a href="/path/prices.zip" download>Download links</a>
<a href="mailto:mail@example.com">Mailto links</a>
<a href="tel:+432423535">Tel links</a>
<a href="/cart" on:click|preventDefault|stopPropagation={doSomething}>
Just stop click event bubbling
</a>
<a href="javascript:void(0)">Old style JS links</a>
<a href="#hashOnly">Hash-only links</a>
</nav>
<script>
import { click } from 'svelte-pathfinder';
</script>
<button on:click={() => back()}>Back</button>
<button on:click={() => goto('/cart?tab=overview')}>Open cart</button>
<script>
import { goto, back } from 'svelte-pathfinder';
</script>
<!-- handle GET-forms -->
<form on:submit={submit} action="/products" method="GET">
<!-- all hidden fields will be propagated to $state by name attributes -->
<input type="hidden" name="uid" value={$state.uid}>
<!-- all visible fields will be propagated to $query by name attributes -->
<input name="q" value={$query.params.q} placeholder="Title...">
<select name="option" value={$query.params.option}>
<option>1</option>
<option>2</option>
<option>3</option>
</select>
<!-- even pushed submit button will be propagated to $query by name attribute -->
<button name="type" value="quick">Quick search</button>
<button name="type" value="fulltext">Fulltext search</button>
</form>
<script>
import { submit, query, state } from 'svelte-pathfinder';
</script>
require('svelte/register');
const = express require('express');
const app = express();
...
/* any other routes */
...
app.get('*', (req, res) => {
const router = require('svelte-pathfinder/ssr')();
const App = require('./App.svelte').default;
router.goto(req.url);
const { html, head, css } = App.render({ router });
res.send(`
<!DOCTYPE html>
<html>
<head>
${head}
${css}
</head>
<body>
${html}
</body>
</html>
`);
});
⚠️ Note: you can't usepathfinder
stores by just directly import it in SSR rendered applications (just like the other Svelte stores), because, in-fact, they're global to the entire server instance. To avoid it, just passpathfinder
instance to root component via props and use it in all components of application. For example using context:
<!-- App.svelte -->
<svelte:window on:click={router.click}/>
<script>
import { setContext } from 'svelte';
export let router;
setContext('router', router);
</script>
<!-- Nested.svelte -->
<script>
import { getContext } from 'svelte';
const { path } = getContext('router');
function gotoSomething() {
$path = '/something';
}
</script>
Router will automatically perform pushState
to browser History API and listening popstate
event if the following conditions are valid:
- router works in browser and global objects are available (window & history).
- router works in browser which is support
pushState/popstate
. - router works in top-level window and has no parent window (eg. iframe, frame, object, window.open).
If any condition is not applicable, the router will work properly but without side-effect (changing URL).
Router will automatically switch to hashbang
routing in the following conditions:
History API
is not available.- web app has launched under
file:
protocol. - initial path contain exact file name with extension.
MIT © PaulMaly