Routing
In the Creating a New Project section, we can see that the route entries are defined within the routes
configuration item when instantiating the Router
. Each route entry contains two properties: path
and component
. Here, path
is the route path, and component
is the corresponding component for the route. Here’s an example:
import { Router } from 'svelte-pilot'
import About from './views/About.svelte'
import Home from './views/Home.svelte'
export default new Router({
routes: [
{
component: Home,
path: '/'
},
{
component: About,
path: '/about'
}
]
})
In the example above, we defined two route entries corresponding to the paths /
and /about
. When we navigate to /
, the <Home>
component is rendered on the page; navigating to /about
renders the <About>
component.
Dynamic Import
In the initial example, we directly passed the class of the component into the route entry. However, this approach results in all components being bundled into a single file. Users will download components they aren't accessing, leading to resource wastage and prolonged initial loading times. To mitigate these issues, we can employ dynamic loading for route components. For example:
import { Router } from 'svelte-pilot'
export default new Router({
routes: [
{
component: () => import('./views/Home.svelte'),
path: '/'
},
{
component: () => import('./views/About.svelte'),
path: '/about'
}
]
})
In this updated example, we’ve replaced the component class with a function. This function returns a Promise
that only invokes when a user accesses the route, loading the corresponding component. Each component is packaged into a separate file to enable load-on-demand.
Layouts
For scenarios where multiple route entries require the same layout, we can configure an entry without a path
attribute, referred to as a view. Its component
attribute points to the layout component, and route entries utilizing this layout are configured as its children. In fact, route entries with a path
attribute are a specialized type of view that maps the URL to the corresponding layout. The entire layout is assembled by traversing the nodes upon matching a route entry. For example:
import { Router } from 'svelte-pilot'
export default new Router({
routes: [
{
children: [
{
component: () => import('./views/Home.svelte'),
path: '/'
},
{
component: () => import('./views/About.svelte'),
path: '/about'
}
],
component: () => import('./views/Layout.svelte')
}
]
})
<script>
import { View } from 'svelte-pilot'
</script>
<header>My Awesome App</header>
<View />
<footer>App Footer</footer>
In the instance above, we defined a <Layout>
component that includes a <View>
component. The <View>
component renders the component specified by the current route entry’s component
attribute. Views can be nested to enable more flexible layout reuse.
Named Views
If we need to render multiple views in a layout, we can assign a name
attribute to the view entries, and then use the name
attribute of the <View>
component in the layout to specify the view to be rendered. For instance:
import { Router } from 'svelte-pilot'
export default new Router({
routes: [
{
children: [
{
component: () => import('./views/Ad.svelte'),
name: 'sidebar'
},
{
component: () => import('./views/Home.svelte'),
path: '/',
},
{
component: () => import('./views/About.svelte'),
path: '/about',
}
],
component: () => import('./views/Layout.svelte')
}
]
})
<script>
import { View } from 'svelte-pilot'
</script>
<header>My Awesome App</header>
<div class='container'>
<aside>
<View name='sidebar' />
</aside>
<main>
<View />
</main>
</div>
<footer>App Footer</footer>
In this example, the <Layout>
component includes two <View>
components; one default view and another named sidebar
. Upon accessing /
or /about
, the router matches the corresponding route entry, retrieving sibling view entries to render the appropriate <View>
components based on the name
attribute.
View Override
In the previous examples, if the <About>
component requires a different sidebar component, it, along with the required sidebar component, can be defined within a nested array, overriding the default sidebar component. For instance:
import { Router } from 'svelte-pilot'
export default new Router({
routes: [
{
children: [
{
component: () => import('./views/Ad.svelte'),
name: 'sidebar'
},
{
component: () => import('./views/Home.svelte'),
path: '/',
},
[
{
component: () => import('./views/AnotherSidebar.svelte'),
name: 'sidebar'
},
{
component: () => import('./views/About.svelte'),
path: '/about',
}
]
],
component: () => import('./views/Layout.svelte')
}
]
})
Component Props
Parameters can be passed to components by defining the props
object in the view configuration. Each attribute of the props
object is passed to the component. For example:
import { Router } from 'svelte-pilot'
export default new Router({
routes: [
{
component: () => import('./views/User.svelte'),
path: '/user/Alice',
props: {
user: 'Alice'
}
}
]
})
<script>
let { user } = $props()
</script>
<h1>Hello {user}!</h1>
In this example, we defined a route entry with the path /user/Alice
, and included a props
object containing a user
attribute. When navigating to /user/Alice
, the <User>
component is rendered, and the user
attribute value is passed into the component.
Extracting Parameters from Paths
We can define parameters in the path using the :parameterName
format and then retrieve the parameter values through the route.params object. route.params
is a StringCaster object, which contains the parameters extracted from the path. We then define the props
attribute as a function, where its parameter is the route object, and the return value will serve as the parameters for the component. For example:
import { Router } from 'svelte-pilot'
export default new Router({
routes: [
{
component: () => import('./views/User.svelte'),
path: '/user/:name',
props: route => ({
user: route.params.string('name')
})
}
]
})
Using Regular Expressions to Match Path Parameters
If path parameters must conform to a specific format, regular expressions can be employed to match them. For example:
import { Router } from 'svelte-pilot'
export default new Router({
routes: [
{
component: () => import('./views/User.svelte'),
path: '/user/:id(\\d+)',
props: route => ({
id: route.params.int('id')
})
},
{
component: () => import('./views/User.svelte'),
path: '/user/:name',
props: route => ({
name: route.params.string('name')
})
}
]
})
In this example, two route entries are defined. The id
parameter of the first entry must be numeric, while the name
parameter of the second can be any string. The router matches route entries based on the order of paths. So, when navigating to /user/123
, the first route entry matches, converting the id
parameter to an integer type before passing it to the <User>
component. Navigating to /user/Alice
matches the second entry, converting the name
parameter to a string before passing it to the `User” component.
In fact, path parameters without defined regular expressions are assumed to match [^/]+
by default, meaning they can match any character up to the next /
.
Unnamed Regular Expression Parameters
If there’s no need to extract the value of a parameter from the path, and the only requirement is to ascertain if the parameter satisfies a specific regular expression, unnamed regular expression parameters can be used. For instance:
import { Router } from 'svelte-pilot'
export default new Router({
routes: [
// ...
{
component: () => import('./views/NotFound.svelte'),
path: '(.*)'
}
]
})
In the example above, a route entry with the path (.*)
is defined, capable of matching any path. It’s matched when the accessed path doesn’t correspond to any other route entry.
Order of Route Entry Matching
- Pure string path route entries take precedence over those containing path parameters. Route entries containing path parameters are only considered when no match is found for those with pure string paths.
- Route entries with longer string prefixes take precedence. For instance,
/user/:id
takes precedence over(.*)
as the router strives to match the longer string path. - For route entries with equal string prefix lengths, the order of definition is considered. For example, between
/user/:id(\\d+)
and/user/:name
, since both have the/user/
prefix, the router matches based on the order of definition.
Generally, defining route entries from the most specific to the most general helps reduce cognitive load and prevent errors.
Extracting Parameters from Query Strings
Parameters from the query string can be obtained through the route.query
object. It’s also a StringCaster
object. Here’s an example:
import { Router } from 'svelte-pilot'
export default new Router({
routes: [
{
component: () => import('./views/User.svelte'),
path: '/user',
props: route => ({
name: route.query.string('name')
})
}
]
})
In this example, navigating to /user?name=Alice
sets the name
attribute to Alice
.
Cross-View Parameter Passing
The route.meta
object enables cross-view parameter passing. It contains custom data for the current route entry and is a regular object. Here's an example:
import { Router } from 'svelte-pilot'
export default new Router({
routes: [
{
children: [
{
component: () => import('./views/Home.svelte'),
meta: {
title: 'Home'
},
path: '/'
},
{
component: () => import('./views/About.svelte'),
meta: {
title: 'About'
},
path: '/about'
}
],
component: () => import('./views/Layout.svelte'),
props: route => ({
title: route.meta.title
})
}
]
})
In this example, the meta
object defined in the route entry can be passed to other view components via route.meta
. The meta
attribute can be an object or a function. If it’s a function, it accepts a route
object as a parameter and returns an object. The meta
objects of the entire view tree are merged, with attributes of child views overriding those of parent views.
Key
The key
attribute can be defined to control whether a component should be destroyed and recreated. The key
attribute is a function that accepts a route
object and returns a primitive type (typically a string or number). If the returned value changes, the component is destroyed and recreated. For example:
import { Router } from 'svelte-pilot'
export default new Router({
routes: [
{
component: () => import('./views/User.svelte'),
key: route => route.params.int('id'),
path: '/user/:id'
}
]
})
In this example, navigating from /user/123
to /user/456
results in the destruction and recreation of the <User>
component. If the key
attribute isn’t defined, the component is reused.
Route Guards
beforeEnter
The beforeEnter
attribute can be defined in the view configuration to set up a route guard before entering the route. The beforeEnter
attribute is a function that receives two parameters, from
and to
, both of which are Route
objects. If the function returns true
or undefined
, the route transition continues. If it returns false
, the transition is interrupted. If it returns a string or Location object, a redirection occurs. If a Promise
is returned, it’s processed based on its resolved value. When multiple beforeEnter
functions exist in the view tree, they’re invoked in parent-to-child order. If any function returns false
or a redirection path, subsequent functions aren’t invoked. Here’s an example:
import { Router } from 'svelte-pilot'
import auth from './auth'
export default new Router({
routes: [
{
beforeEnter: () => {
if (!auth.loggedIn) {
return '/login'
}
},
component: () => import('./views/User.svelte'),
path: '/user'
}
]
})
In this example, a route entry with the path /user
is defined. When navigating to /user
, the beforeEnter
function is invoked. If the user is logged in, the route transition continues. If not, a redirection to /login
occurs.
beforeLeave
The beforeLeave
attribute can be defined in the view configuration to set up a route guard before leaving the current route. Its usage is similar to beforeEnter
.