UNPKG

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
# ๐Ÿ†• 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 [![npm version](https://badge.fury.io/js/svelte5-spa-router.svg)](https://badge.fury.io/js/svelte5-spa-router) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 ```