svelte5-spa-router
Version:
A simple, flexible, and lightweight SPA router specifically designed for Svelte 5 with runes support (dev/non-stable, not production ready)
839 lines (623 loc) โข 22.4 kB
Markdown
# ๐ v1.1.8 & v1.1.9: Reactive locationStore for Layout Control
## New: `locationStore` for Layout Reactivity
You can now use a reactive Svelte store to track the current location (pathname, search, hash) for advanced layout logic (e.g. sidebar, header, breadcrumbs) in your app:
```js
import { locationStore } from 'svelte5-spa-router';
$: $locationStore.pathname; // Reacts to path changes
```
**locationStore** is always up-to-date with browser navigation, pushState, replaceState, and popstate events.
## Example: Hide Sidebar on Login Page
```svelte
<script>
import { locationStore } from 'svelte5-spa-router';
$: hideSidebar = $locationStore.pathname === '/login';
</script>
{#if !hideSidebar}
<Sidebar />
{/if}
```
---
## Changelog (Recent)
## v1.2.2 Changelog & Migration
### ๐ What's New in v1.2.2
- **Multi-level & Nested Route Support:**
- Flat route definitions for deeply nested paths (e.g. `/multi/:parentId/child/:childId/grandchild/:grandId`).
- **Improved Route Guards:**
- Role-based, async, and parameterized guards with `beforeEnter`.
- **Cypress E2E Testing:**
- Full coverage for all routes, including nested and guarded endpoints.
- **Demo & Docs Update:**
- README now includes explicit examples for nested/multi routes and guard usage.
- **Bug Fixes & Stability:**
- Improved param parsing, fallback handling, and route matching.
### โก Migration Notes
- **Flat Route Definitions:**
- Replace nested/children arrays with flat `path` strings for all routes.
- Example:
```js
// Old (nested)
{ path: '/multi/:parentId', children: [ ... ] }
// New (flat)
{ path: '/multi/:parentId/child/:childId', component: MultiChild }
```
- **Route Guards:**
- Use `beforeEnter` on each route for sync/async/role checks.
- **Testing:**
- Use Cypress for browser-based E2E tests (`cypress/e2e/routes.integration.cy.js`).
- **No Breaking Changes:**
- Existing array-based config and imperative `router.addRoute` still supported.
See the updated README and demo for usage patterns and migration examples.
---
# Svelte 5 SPA Router โ Universal Routing Example
## ๐ Quick Start
> **Important for Universal SPA:**
> To ensure routing works on all paths (e.g. `/login`, `/about`), make sure your static server rewrites all requests to `index.html` (see Troubleshooting below).
### 1. Install
```bash
npm install svelte5-spa-router
```
### 2. Definisikan Route dan Komponen (REKOMENDASI UTAMA)
### 2. Define Routes and Components (RECOMMENDED)
```svelte
<script>
import Router from 'svelte5-spa-router/Router.svelte';
import Link from 'svelte5-spa-router/Link.svelte';
import { goto, routeParams, queryParams } from 'svelte5-spa-router';
import Home from './Home.svelte';
import About from './About.svelte';
import Blog from './Blog.svelte';
import BlogPost from './BlogPost.svelte';
import UserProfile from './UserProfile.svelte';
import Search from './Search.svelte';
import NotFound from './NotFound.svelte';
// Array-based config (recommended for all universal SPAs)
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/blog', component: Blog },
{ path: '/blog/:id', component: BlogPost },
{ path: '/user/:id', component: UserProfile },
{ path: '/search/:query?', component: Search }
];
function navigateToBlog() {
goto('/blog/my-first-post');
}
function navigateWithQuery() {
goto('/search', { q: 'svelte', category: 'frontend' });
}
function searchBlog() {
goto('/blog', { search: 'router' });
}
</script>
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
<Link href="/user/123">User Profile</Link>
<Link href="/search">Search</Link>
<button on:click={navigateToBlog}>Go to Blog Post</button>
<button on:click={navigateWithQuery}>Search with Query</button>
<button on:click={searchBlog}>Search Blog</button>
</nav>
<Router {routes} fallback={NotFound} />
<!-- Access params in your component -->
<p>Route Params: {JSON.stringify($routeParams)}</p>
<p>Query Params: {JSON.stringify($queryParams)}</p>
```
---
#### Note:
- **The array-based config above is the default and most recommended for universal SPAs.**
- The imperative way (`router.addRoute`) is only for advanced use-cases (e.g. dynamic/plugin route injection), not needed for most apps.
---
- Always import from the package (`svelte5-spa-router`), not from `src/lib/`.
- To access params, use `$routeParams` and `$queryParams` in your template.
- No SvelteKit required, works directly with Vite + Svelte 5.
---
# ๐ Svelte 5 SPA Router on SvelteKit runes support
[](https://badge.fury.io/js/svelte5-spa-router)
[](https://opensource.org/licenses/MIT)
A simple, flexible, and lightweight SPA router specifically designed for **Svelte 5** with **runes** support.
## โจ Features
- ๐ฏ **Svelte 5 Native**: Built from ground up for Svelte 5 with runes
- ๐ฃ๏ธ **Dynamic Routing**: Support for parameters (`:id`), optional parameters (`:id?`), and wildcards (`/*`)
- โ **Query Parameters**: Full support for URL query strings and hash fragments
- ๐ **Programmatic Navigation**: Navigate with `goto()` function and reactive stores
- ๐ฑ **Browser History**: Full back/forward button support with automatic link interception
- ๐๏ธ **SSR Compatible**: Works perfectly with SvelteKit and server-side rendering
- ๐ฆ **TypeScript Ready**: Fully typed for better developer experience
- ๐ชถ **Lightweight**: Zero external dependencies, minimal bundle size
## ๐ฆ Installation
```bash
npm install svelte5-spa-router
# or
yarn add svelte5-spa-router
# or
pnpm add svelte5-spa-router
```
## ๐ฏ Quick Start
### Basic Setup
```svelte
<!-- App.svelte -->
<script>
import Router from 'svelte5-spa-router/Router.svelte';
import Link from 'svelte5-spa-router/Link.svelte';
import { router } from 'svelte5-spa-router';
import Home from './routes/Home.svelte';
import About from './routes/About.svelte';
import UserProfile from './routes/UserProfile.svelte';
import NotFound from './routes/NotFound.svelte';
// Setup routes
router.addRoute('/', Home);
router.addRoute('/about', About);
router.addRoute('/user/:id', UserProfile);
router.setFallback(NotFound);
</script>
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/user/123">User Profile</Link>
</nav>
<Router />
```
### Available Imports
```javascript
// Components
import Router from 'svelte5-spa-router/Router.svelte';
import Link from 'svelte5-spa-router/Link.svelte';
// Router instance and functions
import {
router, // Main router instance
goto, // Programmatic navigation
getQueryParam, // Get query parameter
updateQueryParams // Update query params
} from 'svelte5-spa-router';
// Reactive stores
import {
currentRoute, // Current route info
routeParams, // Route parameters
queryParams, // Query parameters
hashFragment // Hash fragment
} from 'svelte5-spa-router';
```
## ๐ฃ๏ธ Route Setup
### Setting Up Routes
```javascript
import { router } from 'svelte5-spa-router';
import Home from './components/Home.svelte';
import About from './components/About.svelte';
import UserProfile from './components/UserProfile.svelte';
import BlogPost from './components/BlogPost.svelte';
import Search from './components/Search.svelte';
import AdminPanel from './components/AdminPanel.svelte';
import NotFound from './components/NotFound.svelte';
// Static Routes
router.addRoute('/', Home);
router.addRoute('/about', About);
// Dynamic Routes with Parameters
router.addRoute('/user/:id', UserProfile);
router.addRoute('/blog/:slug', BlogPost);
router.addRoute('/category/:type/item/:id', ItemDetail);
// Optional Parameters
router.addRoute('/search/:query?', Search);
// Wildcard Routes
router.addRoute('/admin/*', AdminPanel);
// Set fallback for 404
router.setFallback(NotFound);
```
## ๐งญ Navigation
### Using Link Component
```svelte
<script>
import Link from 'svelte5-spa-router/Link.svelte';
</script>
<Link href="/about">About Us</Link>
<Link href="/user/123">User Profile</Link>
<Link href="/search?q=svelte">Search Svelte</Link>
<Link href="/docs#introduction">Documentation</Link>
```
### Programmatic Navigation
```javascript
import { goto } from 'svelte5-spa-router';
// Simple navigation
goto('/about');
// With query parameters
goto('/search', { q: 'svelte', page: '1' });
// With hash fragment
goto('/docs', {}, 'introduction');
// Combined
goto('/search', { q: 'svelte', category: 'frontend' }, 'results');
```
## ๐ Accessing Route Data
### Route Parameters
```svelte
<script>
import { routeParams } from 'svelte5-spa-router';
// Access route parameters reactively
const userId = $derived($routeParams.id);
const allParams = $derived($routeParams);
</script>
<h1>User Profile: {userId}</h1><p>All params: {JSON.stringify(allParams)}</p>
```
### Query Parameters
```svelte
<script>
import { queryParams, getQueryParam, updateQueryParams } from 'svelte5-spa-router';
// Get single parameter with default
const searchQuery = $derived(getQueryParam('q', ''));
// Get all parameters
const allQueryParams = $derived($queryParams);
// Update query parameters
function updateSearch(newQuery) {
updateQueryParams({ q: newQuery });
}
// Replace all query parameters
function setFilters() {
updateQueryParams({ category: 'tech', sort: 'date' }, true);
}
</script>
<input bind:value={searchQuery} onchange={() => updateSearch(searchQuery)} />
<p>Current query: {searchQuery}</p>
<p>All params: {JSON.stringify(allQueryParams)}</p>
```
### Hash Fragments
```svelte
<script>
import { hashFragment } from 'svelte5-spa-router';
const currentHash = $derived($hashFragment);
</script>
<p>Current hash: {currentHash}</p>
```
## ๐ง API Reference
### Components
### Components
#### `<Router>`
Main router component that renders the current route based on the URL.
**Usage:**
```svelte
<script>
import Router from 'svelte5-spa-router/Router.svelte';
import { router } from 'svelte5-spa-router';
// Setup your routes first
router.addRoute('/', HomeComponent);
router.setFallback(NotFoundComponent);
</script>
<Router />
```
#### `<Link>`
Link component with automatic active state handling and proper navigation.
**Props:**
- `href` (string): Target URL
- `class` (string, optional): CSS class for the link
**Usage:**
```svelte
<script>
import Link from 'svelte5-spa-router/Link.svelte';
</script>
<Link href="/about" class="nav-link">About</Link>
```
### Functions
#### `goto(path, queryParams?, hash?)`
Navigate programmatically.
- `path`: Target path
- `queryParams`: Object of query parameters
- `hash`: Hash fragment
#### `getQueryParam(key, defaultValue?)`
Get a specific query parameter.
#### `updateQueryParams(params, replace?)`
Update URL query parameters without navigation.
### Stores
All stores are reactive and can be used with `$` syntax:
- `currentRoute`: Current route information `{ path, component, params }`
- `routeParams`: Parameters from current route
- `queryParams`: Current query parameters object
- `hashFragment`: Current hash fragment string
## ๐จ Styling Links
```svelte
<script>
import Link from 'svelte5-spa-router/Link.svelte';
</script>
<Link href="/" class="nav-link">Home</Link>
<style>
:global(.nav-link) {
text-decoration: none;
color: #007acc;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.2s;
}
:global(.nav-link:hover) {
background-color: #f0f0f0;
}
</style>
```
## ๐ Route Guards (Custom Implementation)
```svelte
<!-- App.svelte -->
<script>
import Router from 'svelte5-spa-router/Router.svelte';
import { currentRoute, goto, router } from 'svelte5-spa-router';
const protectedRoutes = ['/dashboard', '/profile'];
// Route guard
$effect(() => {
if ($currentRoute && protectedRoutes.includes($currentRoute.path)) {
if (!isAuthenticated()) {
goto('/login');
}
}
});
function isAuthenticated() {
// Your authentication logic
return localStorage.getItem('token') !== null;
}
</script>
<Router />
```
## ๐ Route Guards: Auth, Async, Role-based (beforeEnter)
Svelte5 SPA Router supports function-based route guards using `beforeEnter` on each route. You can create guards for authentication, async checks, or role-based access (admin/user).
### Contoh Penggunaan
```svelte
<script>
import Router from 'svelte5-spa-router/Router.svelte';
import Link from 'svelte5-spa-router/Link.svelte';
import { goto } from 'svelte5-spa-router';
import ProtectedPage from './ProtectedPage.svelte';
import AdminPanel from './AdminPanel.svelte';
import Home from './Home.svelte';
// Guard: hanya user login
function authGuard(to, from) {
const isAuthenticated = localStorage.getItem('user') !== null;
if (!isAuthenticated) {
alert('Access denied! Please login first.');
return false;
}
return true;
}
// Guard: async (misal cek token ke server)
async function asyncAuthGuard(to, from) {
return new Promise((resolve) => {
setTimeout(() => {
const isAuthenticated = localStorage.getItem('user') !== null;
resolve(isAuthenticated);
}, 300);
});
}
// Guard: hanya admin
function roleGuard(to, from) {
const user = JSON.parse(localStorage.getItem('user') || '{}');
if (user.role !== 'admin') {
alert('Only admin can access this route!');
return false;
}
return true;
}
const routes = [
{ path: '/', component: Home },
{ path: '/protected', component: ProtectedPage, beforeEnter: authGuard },
{ path: '/admin/:id?', component: ProtectedPage, beforeEnter: asyncAuthGuard },
{ path: '/admin-panel', component: AdminPanel, beforeEnter: roleGuard }
];
function simulateLogin(role = 'user') {
const name = role === 'admin' ? 'John Admin' : 'Jane User';
localStorage.setItem('user', JSON.stringify({ name, role }));
alert(`Login as ${role} successful!`);
}
function simulateLogout() {
localStorage.removeItem('user');
alert('Logged out!');
goto('/');
}
</script>
<div>
<button on:click={() => simulateLogin('admin')}>Login as Admin</button>
<button on:click={() => simulateLogin('user')}>Login as User</button>
<button on:click={simulateLogout}>Logout</button>
<nav>
<Link href="/">Home</Link>
<Link href="/protected">Protected</Link>
<Link href="/admin/123">Admin Async</Link>
<Link href="/admin-panel">Admin Panel</Link>
</nav>
<Router {routes} />
</div>
// ProtectedPage.svelte dan AdminPanel.svelte bisa berupa halaman biasa.
```
#### Penjelasan
#### Explanation
`beforeEnter`: Function (sync/async) executed before accessing the route. Return `true` to proceed, `false` to block.
`authGuard`: Only logged-in users can access.
`asyncAuthGuard`: Example of async guard (e.g., check token to server).
`roleGuard`: Only users with `role: 'admin'` can access.
Simulate login/logout using localStorage.
#### Demo Result
Login as **user**: Can access `/protected` and `/admin/123`, cannot access `/admin-panel`.
Login as **admin**: All routes can be accessed.
See the file `src/routes/demo.svelte` for a complete demo.
Lihat file `src/routes/demo.svelte` untuk demo lengkap.
## ๐งช Testing
```javascript
// vitest example
import { render, fireEvent } from '@testing-library/svelte';
import { goto, router } from 'svelte5-spa-router';
import Home from '../components/Home.svelte';
import About from '../components/About.svelte';
import App from '../App.svelte';
beforeEach(() => {
// Setup routes for testing
router.clearRoutes();
router.addRoute('/', Home);
router.addRoute('/about', About);
});
test('should navigate to about page', async () => {
const { getByText } = render(App);
await fireEvent.click(getByText('About'));
expect(getByText('About Page')).toBeInTheDocument();
});
test('should handle dynamic routes', async () => {
router.addRoute('/user/:id', UserProfile);
goto('/user/123');
const { getByText } = render(App);
expect(getByText('User ID: 123')).toBeInTheDocument();
});
```
## ๐ Migration from Other Routers
### From svelte-spa-router
```diff
- import router from 'svelte-spa-router'
+ import Router from 'svelte5-spa-router/Router.svelte'
+ import { router } from 'svelte5-spa-router'
- <Router {routes} />
+ // Setup routes first
+ router.addRoute('/', HomeComponent);
+ router.setFallback(NotFoundComponent);
+ <Router />
```
### From @roxi/routify
```diff
- import { router } from '@roxi/routify'
+ import { goto } from 'svelte5-spa-router'
- $router.goto('/path')
+ goto('/path')
```
## ๐๏ธ SvelteKit Integration
This router works perfectly with SvelteKit for client-side routing:
```svelte
<!-- src/app.html or main component -->
<script>
import Router from 'svelte5-spa-router/Router.svelte';
import { router } from 'svelte5-spa-router';
import Home from './routes/Home.svelte';
import About from './routes/About.svelte';
import NotFound from './routes/NotFound.svelte';
// Setup routes
router.addRoute('/', Home);
router.addRoute('/about', About);
router.setFallback(NotFound);
</script>
<Router />
```
# Cypress E2E Testing
This project includes comprehensive Cypress end-to-end tests for all SPA router routes, including nested, parameterized, and guarded routes.
## Tested Routes
- `/` (Home)
- `/about` (About)
- `/blog` (Blog)
- `/blog/123` (BlogPost)
- `/search?query=router` (Search)
- `/user/tanto` (UserProfile)
- `/admin-panel` (Admin Panel, requires authentication)
- `/multi/123` (MultiParent)
- `/multi/123/child/abc` (MultiChild)
- `/multi/123/child/abc/grandchild/foo` (MultiGrandchild)
- `/nested` (NestedParent)
- `/nested/child` (NestedChild)
- Unknown route (NotFound)
## Example: Nested & Multi-level Routes
You can define deeply nested and multi-level routes using flat path patterns:
```js
import MultiParent from './MultiParent.svelte';
import MultiChild from './MultiChild.svelte';
import MultiGrandchild from './MultiGrandchild.svelte';
import NestedParent from './NestedParent.svelte';
import NestedChild from './NestedChild.svelte';
const routes = [
{ path: '/multi/:parentId', component: MultiParent },
{ path: '/multi/:parentId/child/:childId', component: MultiChild },
{ path: '/multi/:parentId/child/:childId/grandchild/:grandId', component: MultiGrandchild },
{ path: '/nested', component: NestedParent },
{ path: '/nested/child', component: NestedChild }
];
// Usage in Svelte:
<Router {routes} />
// Access params in your component:
<script>
import { routeParams } from 'svelte5-spa-router';
// $routeParams.parentId, $routeParams.childId, $routeParams.grandId
</script>
```
Navigation example:
```svelte
<Link href="/multi/123">MultiParent</Link>
<Link href="/multi/123/child/abc">MultiChild</Link>
<Link href="/multi/123/child/abc/grandchild/foo">MultiGrandchild</Link>
<Link href="/nested">NestedParent</Link>
<Link href="/nested/child">NestedChild</Link>
```
## Admin Route Guard
The `/admin-panel` route is protected by a role-based guard. Cypress sets the required user object in `localStorage` before navigation:
```js
cy.visit('http://localhost:5174/admin-panel', {
onBeforeLoad(win) {
win.localStorage.setItem('user', JSON.stringify({ role: 'admin', name: 'cypress' }));
}
});
```
## Running Cypress Tests
1. Start the dev server:
```bash
npm run dev
```
2. Run Cypress in interactive mode:
```bash
npx cypress open
```
Or run all tests headlessly:
```bash
npx cypress run --spec cypress/e2e/routes.integration.cy.js
```
All tests are located in `cypress/e2e/routes.integration.cy.js`.
## ๐ Troubleshooting
### Link Not Working When Clicked
1. Make sure to import Link like this:
```svelte
import Link from 'svelte5-spa-router/Link.svelte';
```
2. Use `<Link href="/about">About</Link>`, not a regular `<a>` tag.
3. Ensure there are no other elements (overlay/z-index) covering the Link.
4. Check the browser console for JS errors.
### SSR Issues
Make sure you're importing from the correct path and the router handles SSR automatically.
### Route Not Matching
Check your route patterns and ensure they match the URL structure exactly.
### Active Links Not Working
Ensure you're using the `Link` component and not regular `<a>` tags.
## ๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## ๐ License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## ๐ Acknowledgments
- Built for the amazing Svelte 5 and its new runes system
- Inspired by various SPA routers in the ecosystem
- Thanks to the Svelte community for feedback and suggestions
---
**Made with โค๏ธ for the Svelte community**
[Report Bug](https://github.com/tantojos4/svelte5-spa-router/issues) โข [Request Feature](https://github.com/tantojos4/svelte5-spa-router/issues) โข [Documentation](https://github.com/tantojos4/svelte5-spa-router#readme)
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
## Building
To build your library:
```sh
npm run package
```
To create a production version of your showcase app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
## Publishing
Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
To publish your library to [npm](https://www.npmjs.com):
```sh
npm publish
```