diff --git a/README.md b/README.md index 2b2aa82..f360e20 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ > [!NOTE] > #### Small and Unique! > -> + Less than **1.000** lines of code, including TypeScript typing. +> + Less than **1,050** lines of code, including TypeScript typing. > + Always-on path and hash routing. Simultaneous and independent routing modes. > + The router that invented multi hash routing. @@ -44,10 +44,15 @@ route's key. ### `` Component -+ **Drop-in replacement**: Exchange `` tags with `` tags and you're done. ++ **Drop-in replacement**: Exchange `` tags with `` tags and you're done.[^1] + **Specify state**: Set history state upon hyperlink click. + **Active state based on route key**: Automatically set active state and `aria-current` by specifying the route's key. + **Replace or push**: Select the method for pushing state. ++ **Preserve query string**: Opt in to preserve existing query string values. ++ **Shallow routing**: [This Sveltekit](https://svelte.dev/docs/kit/shallow-routing) document explains the concept. + +[^1]: For hyperlink components that only specify a hash and are converted to hash-routing `` components, remove +the pound sign (`#`) from the href. ### `` Component @@ -115,25 +120,23 @@ init({ implicitMode: 'hash' }); import UserView from "./lib/UserView.svelte"; - +
- - -

Routing Demo

- - - - - - {#snippet children(params)} - - {/snippet} - - ... -
+ +

Routing Demo

+ + + + + + {#snippet children(params)} + + {/snippet} + + ...
-
+ ``` ### Navigation Links @@ -217,8 +220,8 @@ Nothing prevents you to add transitions to anything. ``` > [!NOTE] -> This one item might be worthwhile revisting for the cases where synchronized transitions are desired. This, however, -> won't be looked at until Svelte attachments become a thing. +> This one item might be worthwhile revisiting for the cases where synchronized transitions are desired. This, +> however, won't be looked at until Svelte attachments become a thing. ### Guarded Routes diff --git a/src/lib/Link/Link.svelte b/src/lib/Link/Link.svelte index 0b7c4cb..202dd5e 100644 --- a/src/lib/Link/Link.svelte +++ b/src/lib/Link/Link.svelte @@ -59,9 +59,9 @@ activeState?: ActiveState; /** * Renders the children of the component. - * @param state The state object stored in in the window's History API for the universe the link is + * @param state The state object stored in in the window's History API for the universe the link is * associated to. - * @param routeStatus The router's route status data, if the `Link` component is within the context of a + * @param routeStatus The router's route status data, if the `Link` component is within the context of a * router. */ children?: Snippet<[any, Record | undefined]>; @@ -89,11 +89,13 @@ const calcPreserveQuery = $derived(preserveQuery ?? linkContext?.preserveQuery ?? false); const calcPrependBasePath = $derived(prependBasePath ?? linkContext?.prependBasePath ?? false); const isActive = $derived(!!router?.routeStatus[activeState?.key ?? '']?.match); - const calcHref = $derived(buildHref()); - - function buildHref() { + const calcHref = $derived.by(() => { + if (href === '') { + // Leave untouched for shallow routing. + return href; + } const pathname = calcPrependBasePath ? joinPaths(router?.basePath ?? '', href) : href; - if (hash || !calcPreserveQuery || !location.url.searchParams.size) { + if (resolvedHash || !calcPreserveQuery || !location.url.searchParams.size) { return pathname; } let searchParams: URLSearchParams; @@ -116,15 +118,15 @@ } } return `${pathname}?${searchParams}`; - } + }); function handleClick(event: MouseEvent) { event.preventDefault(); const newState = typeof state === 'function' ? state() : state; - if (typeof hash === 'string') { - location.navigate(calcHref, hash, { replace: calcReplace, state: newState }); + if (typeof resolvedHash === 'string') { + location.navigate(calcHref, resolvedHash, { replace: calcReplace, state: newState }); } else { - location.navigate(!!hash ? `#${calcHref}` : calcHref, { + location.navigate(resolvedHash && calcHref !== '' ? `#${calcHref}` : calcHref, { replace: calcReplace, state: newState }); diff --git a/src/lib/core/LocationLite.svelte.test.ts b/src/lib/core/LocationLite.svelte.test.ts index d68f61f..1463ba5 100644 --- a/src/lib/core/LocationLite.svelte.test.ts +++ b/src/lib/core/LocationLite.svelte.test.ts @@ -220,5 +220,30 @@ describe("LocationLite", () => { expect(location.url.hash).toBe(`#${hash}=${newPath}`); expect(location.getState(hash)).toBe(state); }); + test("Should update the state whenever shallow routing is used (path routing).", () => { + // Arrange. + const currentUrl = location.url.href; + const newState = 123; + + // Act. + location.navigate('', { state: newState }); + + // Assert. + expect(location.getState(false)).toBe(newState); + expect(location.url.href).toBe(currentUrl); + }); + test("Should update the state whenever shallow routing is used (multi hash routing).", () => { + // Arrange. + const currentUrl = location.url.href; + const newState = 123; + const pathName = 'p1'; + + // Act. + location.navigate('', pathName, { state: newState }); + + // Assert. + expect(location.getState(pathName)).toBe(newState); + expect(location.url.href).toBe(currentUrl); + }); }); }); diff --git a/src/lib/core/LocationLite.svelte.ts b/src/lib/core/LocationLite.svelte.ts index 8c3bbfc..aedec47 100644 --- a/src/lib/core/LocationLite.svelte.ts +++ b/src/lib/core/LocationLite.svelte.ts @@ -81,26 +81,38 @@ export class LocationLite implements Location { navigate(url: string, hashId: string, options?: NavigateOptions): void; navigate(url: string, hashIdOrOptions?: string | NavigateOptions, options?: NavigateOptions) { let newState: State; - if (typeof hashIdOrOptions === 'string') { - let idExists = false; - let finalUrl = ''; - for (let [id, path] of Object.entries(this.hashPaths)) { - if (id === hashIdOrOptions) { - idExists = true; - finalUrl += `;${id}=${url}`; - continue; - } - finalUrl += `;${id}=${path}`; + if (url === '') { + url = this.url.href; + if (typeof hashIdOrOptions === 'string') { + newState = this.#newState(hashIdOrOptions, options?.state); } - if (!idExists) { - finalUrl += `;${hashIdOrOptions}=${url}`; + else { + options = hashIdOrOptions; + newState = this.#newState(url.startsWith('#'), options?.state); } - url = '#' + finalUrl.substring(1); - newState = this.#newState(hashIdOrOptions, options?.state); } else { - options = hashIdOrOptions; - newState = this.#newState(url.startsWith('#'), options?.state); + if (typeof hashIdOrOptions === 'string') { + let idExists = false; + let finalUrl = ''; + for (let [id, path] of Object.entries(this.hashPaths)) { + if (id === hashIdOrOptions) { + idExists = true; + finalUrl += `;${id}=${url}`; + continue; + } + finalUrl += `;${id}=${path}`; + } + if (!idExists) { + finalUrl += `;${hashIdOrOptions}=${url}`; + } + url = '#' + finalUrl.substring(1); + newState = this.#newState(hashIdOrOptions, options?.state); + } + else { + options = hashIdOrOptions; + newState = this.#newState(url.startsWith('#'), options?.state); + } } (options?.replace ? globalThis.window?.history.replaceState : diff --git a/src/lib/types.ts b/src/lib/types.ts index ae29ba5..0b04482 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -143,13 +143,15 @@ export interface Location { * Navigates to the specified URL. * * It will push new URL's by default. To instead replace the current URL, set the `replace` option to `true`. - * @param url The URL to navigate to. + * @param url The URL to navigate to. Use an empty string (`""`) to navigate to the current URL, a. k. a., shallow + * routing. * @param options Options for navigation. */ navigate(url: string | URL, options?: NavigateOptions): void; /** * Navigates to the specified hash URL for the specified hash identifier. - * @param url The URL that will be saved as hash. + * @param url The URL that will be saved as hash. Use an empty string (`""`) to navigate to the current URL, + * a. k. a., shallow routing. * @param hashId The hash identifier for the route to set. * @param options Options for navigation. */