Welcome to the second and final part of the series of routing with page.js. In the first part we got the basic routing in place and in this part we will finish what we started. More specifically we will implement:
- Route protection with the help of the middleware
- Passing custom properties down to our components
- Exposing page.js routing parameters in our routes
- Propagating page.js params down to our components
This is how we want our final solution to look and work.
<Router>
<Route path="/" component="{Home}" {data} />
<Route path="/about" component="{About}" />
<Route path="/profile/:username" middleware="{[guard]}" let:params>
<h2>Hello {params.username}!</h2>
<p>Here is your profile</p>
</Route>
<Route path="/news">
<h2>Latest News</h2>
<p>Finally some good news!</p>
</Route>
<NotFound>
<h2>Sorry. Page not found.</h2>
</NotFound>
</Router>
Exposing params
We will start with the easiest part. Exposing params to the components and in routes. Page.js allows you to define params in the url path and will make them available to you in its context object. We first need to understand how page.js works
page('/profile/:name', (ctx, next) {
console.log('name is ', ctx.params.name);
});
Page.js takes a callback with context
and next
optional parameters. Context is the context object that will be passed to the next callback in the chain in this case. You can put stuff on the context object that will be available to the next callback. This is useful for building middlwares, for example pre-fetching user information, and also caching. Read more what's possible in the context docs.
Propagating params is actually pretty simple, we just have to put it in our activeRoute
store in the Router.svelte
file. Like this.
const setupPage = () => {
for (let [path, route] of Object.entries(routes)) {
page(path, (ctx) => ($activeRoute = { ...route, params: ctx.params }));
}
page.start();
};
And here is how our Route.svelte
file looks now.
<script>
import { register, activeRoute } from './Router.svelte';
export let path = '/';
export let component = null;
// Define empty params object
let params = {};
register({ path, component });
// if active route -> extract params
$: if ($activeRoute.path === path) {
params = $activeRoute.params;
}
</script>
{#if $activeRoute.path === path}
<!-- if component passed in ignore slot property -->
{#if $activeRoute.component}
<!-- passing custom properties and page.js extracted params -->
<svelte:component
this="{$activeRoute.component}"
{...$$restProps}
{...params}
/>
{:else}
<!-- expose params on the route via let:params -->
<slot {params} />
{/if}
{/if}
We use the spread operator to pass page.js params down to the component. That's just one way to do it. You might as well pass down the whole params
object if you want. The interesting part is the $$restProps
property that we also pass down to the underlying component. In Svelte, there are $$props
and $$restProps
properties. Props includes all props in component, the passed in ones and the defined ones, while restProps excludes the ones defined in the component and includes the only ones that are being passed in. This means that we also just solved passing custom properties down to components feature. Hooray!
Our main part of the App.svelte
looks like this now.
<main>
<nav>
<a href="/">home</a>
<a href="/about">about</a>
<a href="/profile/joe">profile</a>
<a href="/news">news</a>
</nav>
<Router>
<Route path="/" component="{Home}" />
<Route path="/about" component="{About}" />
<Route path="/profile/:username" let:params>
<h2>Hello {params.username}!</h2>
<p>Here is your profile</p>
</Route>
<Route path="/news">
<h2>Latest News</h2>
<p>Finally some good news!</p>
</Route>
<NotFound>
<h2>Sorry. Page not found.</h2>
</NotFound>
</Router>
</main>
Give the app a spin and see if our params feature works as expected. I left out custom data properties as an exercise.
Protected routes with middleware
The only missing part now is the protected routes part, which we can solve with the help of middleware. Let's implement this.
Page.js supports multiple callbacks for a route which will be executed in order they are defined. We will leverage this feature and build our middleware on top of it.
page('/profile', guard, loadUser, loadProfile, setActiveComponent);
It works something like this. Our "guard" callback will check for some pre-condition and decide whether to allow the next callback in the chain or not. Our last callback that sets the active route must be last in the chain, named setActiveComponent
in the example above. For that to work we need to refactor the main router file a bit.
// extract our active route callback to its own function
const last = (route) => {
return function (ctx) {
$activeRoute = { ...route, params: ctx.params };
};
};
const registerRoutes = () => {
Object.keys($routes).forEach((path) => {
const route = $routes[path];
// use the spread operator to pass supplied middleware (callbacks) to page.js
page(path, ...route.middleware, last(route));
});
page.start();
};
You might wonder where the route.middleware
comes from. That is something that we pass down to the individual routes.
<!-- Route.svelte -->
<script>
import { register, activeRoute } from './Router.svelte';
export let path = '/';
export let component = null;
// define new middleware property
export let middleware = [];
let params = {};
// pass in middlewares to Router.
register({ path, component, middleware });
$: if ($activeRoute.path === path) {
params = $activeRoute.params;
}
</script>
{#if $activeRoute.path === path}
{#if $activeRoute.component}
<svelte:component
this="{$activeRoute.component}"
{...$$restProps}
{...params}
/>
{:else}
<slot {params} />
{/if}
{/if}
If you try to run the app now you will get a reference error. That's because we have to add middleware property to NotFound.svelte
too.
<!-- NotFound.svelte -->
<script>
import { register, activeRoute } from './Router.svelte';
// page.js catch all handler
export let path = '*';
export let component = null;
register({ path, component, middleware: [] });
</script>
{#if $activeRoute.path === path}
<svelte:component this="{component}" />
<slot />
{/if}
And here what our App.svelte
looks now with style omitted.
<script>
import { Router, Route, NotFound, redirect } from './pager';
import Login from './pages/Login.svelte';
import Home from './pages/Home.svelte';
import About from './pages/About.svelte';
import Profile from './pages/Profile.svelte';
const data = { foo: 'bar', custom: true };
const guard = (ctx, next) => {
// check for example if user is authenticated
if (true) {
redirect('/login');
} else {
// go to the next callback in the chain
next();
}
};
</script>
<main>
<nav>
<a href="/">home</a>
<a href="/about">about</a>
<a href="/profile/joe">profile</a>
<a href="/news">news</a>
<a href="/login">login</a>
</nav>
<Router>
<Route path="/" component="{Home}" {data} />
<Route path="/about" component="{About}" />
<Route path="/login" component="{Login}" />
<Route path="/profile/:username" let:params>
<h2>Hello {params.username}!</h2>
<p>Here is your profile</p>
</Route>
<Route path="/news" middleware="{[guard]}">
<h2>Latest News</h2>
<p>Finally some good news!</p>
</Route>
<NotFound>
<h2>Sorry. Page not found.</h2>
</NotFound>
</Router>
</main>
The app file looks a little different now, but that's because I've added some bells and whistles to it. You can find the whole project here.
Conclusion
This wraps everything up. We've now created fully declarative router for Svelte based on page.js. It's not feature complete, but you can easily adjust it to your own requirements. It's hard to build libraries that cover every possible corner case, kudos to those who try!
I hope that I showed you that it's actually not that hard to build something in Svelte that fits just your requirements, while also keeping control of the code. I also hope that you picked up some knowledge on the way of how Svelte works.
Top comments (3)
What an excellent article, thanks for sharing!
Many thanks ๐
Your tutorial is excellent, but it would be interesting to indicate precisely which code goes on which page.
The ideal, for a novice like me, would be to have the final result of the four pages at the end of part 1 and also at the end of part 2. Not to copy / paste stupidly, but to analyse it more simply.
Thanks! Note taken! I usually put the end result on Github, but it's hard to do for multi-article series. Maybe keep the parts in different git branches or something? If you have any suggestions, I am all ears!