Bridging the gap between Native Apps and WebApps. A Svelte Router that caches page components and will make your WebApp feel more native
npm install svelte-stack-router
The demo files can be found in src/
npm install --save-dev svelte-stack-router
const myRoutes = {
"/": Home,
"/my-page": PageComponent,
"/my-other-page/:someString": OtherPageThatAcceptsAParameter,
"/my-other-page2/:optionalParam?": OtherPageThatAcceptsAnOptionalParameter,
"*": NotFound,
};
<StackRouter routes={myRoutes} />
somewhere in the HTML sectionExample:
App.svelte
<script>
import { StackRouter } from 'svelte-stack-router';
import Home from './Home.svelte';
import NotFound from './NotFound.svelte';
const routes = {
"/": Home,
"*": NotFound
};
</script>
<StackRouter {routes} />
Page components are cached, this ensures that going back in the browser history resumes the complete previous state.
In other words: previously instantiated pages don't get destroyed by default, they just get paused and resumed to reduce re-renders and preserve their full state.
Note that a cached page is uniquely identified by its route template (i.e. the key in the routes/myRoutes
objects in the examples above), not by the component.
In addition to the onMount
and onDestroy
lifecycle functions provided by Svelte, this library offers onPause
, onResume
, onBeforeLoad
, onBeforeUnload
, onAfterLoad
and onAfterUnload
.
All these new lifecycle functions accept synchronous and asynchronous callbacks. In case of asynchronous callbacks they are executed one by one in the same order they were registered.
The complete lifecycle is (|| = semi-parallel execution, achieved with Promise.all):
All these additional lifecycle functions can be called by the Page component and by its children during initialization, this means they should be invoked directly in the script section of the Svelte component (e.g. not inside onMount or in reactive statements).
If you have a component which shouldn't be paused or resumed by the StackRouter, you can call setResumable(false)
Doing this will make your component disposable, so that it will be mounted and destroyed and never paused or resumed.
When the <StackRouter />
components gets unmounted (e.g. if it is inside an {#if ...}
), all the components that were previously cached by it
will be destroyed. In particular, if the currently active component has registered any onBeforeUnload
, onPause
or onAfterUnload
callbacks, those
will get executed before the destruction of the component with a boolean parameter set to true
to signal the urgency to clean up.
<script>
onAfterUnload((force) => {
if (force) {
// non interactive operations
} else {
// asks for a user interaction (e.g. confirmation modal)
}
});
</script>
You can listen for the following events:
on:navigation-start
emitted before unloading the old page and before loading the new pageon:navigation-end
emitted after unloading the old page and after loading the new pageon:forbidden
emitted when a route couldn't be reached because a guard returned a falsy value {
message: 'access forbidden by guard',
params: <page params preview, if any>,
location: <location the router was trying to navigate to>
}
on:error
emitted when a route couldn't be reached due to an error. The error can be one of the following (<...>
indicates some value at runtime): {
message: 'no route found',
location: <location the router was trying to navigate to>
}
{
message: 'unable to get component from provider',
location: <location the router was trying to navigate to>,
err: <error thrown by the provider function>
}
{
message: 'guard error',
params: <page params preview, if any>,
location: <location the router was trying to navigate to>,
err: <error thrown by the guard>
}
Example:
<StackRouter {routes} on:navigation-start={() => alert('navigation started')} />
The following functions enable programmatic navigation:
push('/some-route')
pop()
or pop({ some: 'return value' })
(see Returning values)replace('/a-route')
This library also provides a custom use:link
action that you can add to your <a>
elements to create links. This action serves two purposes:
#
prefix to all the href
. For example <a href="/example-1" use:link>Example</a>
(or <a use:link={"/example-1"}>Example</a>
) is automatically
converted to <a href="#/example-1">Example</a>
. This is particularly helpful if you later decide to switch to "path mode" (see next point)To easily access information about the current route, you can import the following stores:
location
: reflects the relative URL, without the leading '#' if in hash modepathname
: store derived from location that extracts the path section of the URLsearch
: store derived from location that extracts the query string (starting from '?' included)For example, if you are on a page like http://localhost:5000/#/home/some-page/?someParam=a%20string, the following comparisons will be true:
$location === "/home/some-page/?someParam=a%20string"
$pathname === "/home/some-page/"
$search === "?someParam=a%20string"
These features have been heavily inspired by svelte-spa-router.
When the pop
function is called it can receive an optional parameter, which acts like a return value.
This value will be passed on as an argument to all the callback functions that are registered in the onResume
hook of the component that is about to be resumed, thus allowing two components to communicate with each other.
For example:
/selection
, a page that presents them with a list of items and expects them to pick one. In the same page there is an Add
buttonAdd
button, thus navigating to /new
, a page with a form where they can POST a new item to the listpop
is called with the id
of the newly created entity/selection
, which, being resumable, can handle the return value in its onResume
callback(s) and show the selection on the newly created entityFor example:
<script>
onResume((newElementId) => {
// handle newElementId
})
</script>
<script>
async function add() {
const newElementId = await axios.post('/some-endpoint', {
// data
});
pop(newElementId);
}
</script>
You can look at an actual implementation in the live demo source code.The <StackRouter>
component supports a variety of options:
name | type | description | default |
---|---|---|---|
defaultResumable | boolean | whether or not the default behavior should be to resume or recreate the components | true |
useHash | boolean | whether or not to prefix routes with '#' to implement a server-agnostic client side routing (e.g. no need to redirect 404 to index) | true |
restoreScroll | boolean | whether or not to restore the scroll position when navigating backwards | true |
transitionFn | TransitionFunction | a function that handles the transition between two pages | dive(300) |
routes | Record.<string, SvelteComponentConstructor> | a key-value object associating a route path (e.g. '/a/route/path/:variable1?) to a SvelteComponent constructor | N/A - required |
This router supports guards and asynchronously provided components (e.g. when using lazy loading).
Guards are functions that can either return a boolean
or a Promise<boolean>
. They are called sequentially and
the first one to return (or resolve to) false
causes a navigation error ("access forbidden by guard").
If a route has some parameters, the guard will receive them before the component.
Guard type:
export type Guard = (paramsPreview?: Params) => boolean | Promise<boolean>;
Here are some examples:
provide a component asynchronously:
App.svelte
<script>
import { StackRouter } from 'svelte-stack-router';
import Home from './Home.svelte';
const routes = {
"/": Home,
"*": { componentProvider: () => import('./NotFound.svelte') }
};
</script>
<StackRouter {routes} />
add guards to a route:
App.svelte
<script>
import { StackRouter } from 'svelte-stack-router';
import Home from './Home.svelte';
import Login from './Login.svelte';
import NotFound from './NotFound.svelte';
import user from './user';
const routes = {
"/login": {
component: Login,
// You can have multiple guards
guards: [
() => $user === null,
// Each guard gets a preview of the params that are going to
// be passed to the component if all guards return true
(paramsPreview) => Number.isNumber(paramsPreview.id),
]
},
"/": Home,
"*": NotFound
};
</script>
<StackRouter {routes} />
add guards to a route with an async component provider:
App.svelte
<script>
import { StackRouter } from 'svelte-stack-router';
import Home from './Home.svelte';
import NotFound from './NotFound.svelte';
const routes = {
"/": Home,
"/login": {
componentProvider: () => import('./Login.svelte'),
// You can use "guard" instead of "guards" if you
// want to pass only one function instead of an array
guard: () => $user === null,
},
"*": NotFound
};
</script>
<StackRouter {routes} />
This library provides 3 types of transitions between pages:
dive(milliseconds)
transition with a dive effectslide(milliseconds)
transition with a slide effectnoAnimation()
transition without any animationYou can also implement a custom transition animation by implementing a transition function that reflect the following type definition:
/**
* A function that handles the transition between two pages
* @param {NavigationType} data.navigationType describes the navigation that occurred (e.g. backward, replace, forward, ...)
* @param {HTMLElement} data.mountPointToLoad the mount point of the page that is being loaded
* @param {HTMLElement} data.mountPointToUnload the mount point of the page that is being unloaded
* @param {HTMLElement} data.routerMountPoint the router mount point, when this function is called it contains both the mountPointToLoad and the mountPointToUnload
* @param {{x: number, y: number}} data.scroll if scroll restoration is enabled and the current component is being resumed, this object contains the x and y coordinates needed to bring the window scrollbars back to where they were when the component was paused
* @return {Promise} a promise that resolves once the transition has finished
*/
export type TransitionFunction = (data: TransitionFunctionData) => Promise<void>;
You can also generate a TransitionFunction using the helpers provided in transition-functions.js