Svelte routing with page.js, Part 2

codechips

Ilia Mikhailov

Posted on April 5, 2020

Svelte routing with page.js, Part 2

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>
Enter fullscreen mode Exit fullscreen mode

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);
});

Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

šŸ’– šŸ’Ŗ šŸ™… šŸš©
codechips
Ilia Mikhailov

Posted on April 5, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related