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
5 changes: 5 additions & 0 deletions .changeset/paths-node-base.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/router": patch
---

Under a `base`, `navigate(paths.about)` and `useLinkState(paths.about)` no longer add the base twice. A paths node already includes the base, so with `base: "/app"` they gave `/app/app/about`. They now use the node's path as-is and give `/app/about`. `useLinkState(paths.about)` also works under `hashHistory`, where it never reported the link as current before.
15 changes: 11 additions & 4 deletions src/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,13 @@ export const useLinkState = (
): LinkState => {
const router = useRouter();
const location = router.location;
const to = useResolvedPath(() => String(href()));
const route = useRoute();
// a paths node's brand already holds the base; resolving would add it again
const to = createMemo(() => {
const h = href();
const brand = typeof h !== "string" ? ((h as any)[HREF] as string | undefined) : undefined;
return brand !== undefined ? brand : route.resolvePath(String(h));
});
const base = router.base.path();
const state = createMemo(() => linkMatcher(location, base, options.end)(to()));
return {
Expand Down Expand Up @@ -1085,9 +1091,10 @@ export function createRouterContext(
};
resolvedTo = compose(headed);
} else {
if (typeof to !== "string") to = href || to.toString();
resolvedTo = resolveTarget(to);
if (resolvedTo === undefined) throw unroutable(to);
// the brand already holds the base, so it skips resolution
if (typeof to !== "string" && href === undefined) to = to.toString();
resolvedTo = href !== undefined ? href : resolveTarget(to as string);
if (resolvedTo === undefined) throw unroutable(to as string);
}

const navigationDepth =
Expand Down
154 changes: 154 additions & 0 deletions test/paths-base.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { render } from "@solidjs/web";
import { vi } from "vitest";
import {
createRouter,
hashHistory,
memoryHistory,
useLinkState,
useNavigate,
type LinkState,
type Navigator
} from "../src/index.js";

const settle = () => new Promise(r => setTimeout(r, 30));

describe("paths nodes under a base", () => {
const originalScrollTo = window.scrollTo;
beforeEach(() => {
window.scrollTo = vi.fn() as any;
});
afterAll(() => {
window.scrollTo = originalScrollTo;
});

const routes = [
{ path: "/", component: () => <p>home</p> },
{ path: "/about", component: () => <p>about</p> }
] as const;

test("navigate(paths.about) lands on /app/about", async () => {
const history = memoryHistory("/app");
const Router = createRouter({ routes, base: "/app", history });
let navigate!: Navigator;
const div = document.createElement("div");
const dispose = render(
() => <Router>{p => ((navigate = useNavigate()), p.children)}</Router>,
div
);
try {
await settle();
expect(String(Router.paths.about)).toBe("/app/about");
navigate(Router.paths.about);
await settle();
expect(history.get()).toBe("/app/about");
expect(div.textContent).toBe("about");
} finally {
dispose();
}
});

test("useLinkState(paths.about) is current on /app/about, like the base-relative string", async () => {
const history = memoryHistory("/app/about");
const Router = createRouter({ routes, base: "/app", history });
let node!: LinkState;
let relative!: LinkState;
let root!: LinkState;
const div = document.createElement("div");
const dispose = render(
() => (
<Router>
{p => {
node = useLinkState(() => Router.paths.about);
relative = useLinkState(() => "/about");
root = useLinkState(() => Router.paths);
return p.children;
}}
</Router>
),
div
);
try {
await settle();
expect(div.textContent).toBe("about");
expect([node.active(), node.current()]).toEqual([true, true]);
expect([relative.active(), relative.current()]).toEqual([true, true]);
expect([root.active(), root.current()]).toEqual([false, false]);
} finally {
dispose();
}
});
});

describe("paths nodes under hashHistory", () => {
const originalScrollTo = window.scrollTo;
beforeEach(() => {
window.scrollTo = vi.fn() as any;
window.history.replaceState(null, "", "/#/about");
});
afterEach(() => {
window.history.replaceState(null, "", "/");
});
afterAll(() => {
window.scrollTo = originalScrollTo;
});

test("useLinkState(paths.about) reads the node's route, not its display href", async () => {
const Router = createRouter({
routes: [
{ path: "/", component: () => <p>home</p> },
{ path: "/about", component: () => <p>about</p> }
] as const,
history: hashHistory()
});
let link!: LinkState;
const div = document.createElement("div");
const dispose = render(
() => <Router>{p => ((link = useLinkState(() => Router.paths.about)), p.children)}</Router>,
div
);
try {
await settle();
expect(String(Router.paths.about)).toBe("#/about");
expect(div.textContent).toBe("about");
expect([link.active(), link.current()]).toEqual([true, true]);
} finally {
dispose();
}
});

test("navigate(paths.about) and useLinkState(paths.about) under a base", async () => {
const Router = createRouter({
routes: [
{ path: "/", component: () => <p>home</p> },
{ path: "/about", component: () => <p>about</p> }
] as const,
base: "/app",
history: hashHistory()
});
window.history.replaceState(null, "", "/#/app");
let navigate!: Navigator;
let link!: LinkState;
const div = document.createElement("div");
const dispose = render(
() => (
<Router>
{p => (
(navigate = useNavigate()), (link = useLinkState(() => Router.paths.about)), p.children
)}
</Router>
),
div
);
try {
await settle();
expect(div.textContent).toBe("home");
navigate(Router.paths.about);
await settle();
expect(window.location.hash).toBe("#/app/about");
expect(div.textContent).toBe("about");
expect([link.active(), link.current()]).toEqual([true, true]);
} finally {
dispose();
}
});
});
Loading