Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 23 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -44,10 +44,15 @@ route's key.

### `<Link>` Component

+ **Drop-in replacement**: Exchange `<a>` tags with `<Link>` tags and you're done.
+ **Drop-in replacement**: Exchange `<a>` tags with `<Link>` 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 `<Link>` components, remove
the pound sign (`#`) from the href.

### `<LinkContext>` Component

Expand Down Expand Up @@ -115,25 +120,23 @@ init({ implicitMode: 'hash' });
import UserView from "./lib/UserView.svelte";
</script>

<Route>
<Router>
<NavBar />
<div class="container">
<Router>
<!-- content outside routes is always rendered -->
<h1>Routing Demo</h1>
<Route key="users" path="/users">
<!-- content here -->
</Route>
<Route key="user" path="/users/:userId">
<!-- access parameters via the snippet parameter -->
{#snippet children(params)}
<UserView id={params.userId} /> <!-- Intellisense will work here!! -->
{/snippet}
</Route>
...
</Router>
<!-- content outside routes is always rendered -->
<h1>Routing Demo</h1>
<Route key="users" path="/users">
<!-- content here -->
</Route>
<Route key="user" path="/users/:userId">
<!-- access parameters via the snippet parameter -->
{#snippet children(params)}
<UserView id={params.userId} /> <!-- Intellisense will work here!! -->
{/snippet}
</Route>
...
</div>
</Route>
</Router>
```

### Navigation Links
Expand Down Expand Up @@ -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

Expand Down
22 changes: 12 additions & 10 deletions src/lib/Link/Link.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, RouteStatus> | undefined]>;
Expand Down Expand Up @@ -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;
Expand All @@ -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
});
Expand Down
25 changes: 25 additions & 0 deletions src/lib/core/LocationLite.svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
44 changes: 28 additions & 16 deletions src/lib/core/LocationLite.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand Down
6 changes: 4 additions & 2 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down