Skip to content

Commit 24a26cc

Browse files
committed
Remove navLink API; add when API
1 parent 34bd506 commit 24a26cc

5 files changed

Lines changed: 35 additions & 90 deletions

File tree

examples/remix/contacts/src/components/SearchBar.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function SearchBar(this: Remix.Handle) {
1111
let query: string | undefined;
1212

1313
return () => {
14-
const searching = Boolean(router.navigating.to.url?.searchParams.has("q"));
14+
const searching = router.navigating.to.url?.searchParams.has("q");
1515
const currentQuery = router.url.searchParams.get("q") ?? undefined;
1616

1717
if (query !== currentQuery) {
@@ -22,11 +22,11 @@ export function SearchBar(this: Remix.Handle) {
2222
<RestfulForm id="search-form">
2323
<input
2424
aria-label="Search contacts"
25-
class={searching ? "loading" : ""}
25+
class={searching ? "loading" : undefined}
2626
defaultValue={query ?? ""}
2727
id="q"
2828
name="q"
29-
on={dom.input<HTMLInputElement>(async event => {
29+
on={dom.input(async event => {
3030
// Remove empty query params when value is empty
3131
if (!event.currentTarget.value) {
3232
router.navigate(router.location.pathname + router.location.hash);

examples/remix/contacts/src/components/Sidebar.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ export function Sidebar(this: Remix.Handle) {
1616
<nav>
1717
{contacts.length ? (
1818
<ul>
19-
{contacts.map(contact => (
20-
<SidebarItem contact={contact} key={contact.id} />
21-
))}
19+
{contacts
20+
// .toSorted((a, b) => a.first?.localeCompare(b.first ?? "") ?? -1)
21+
.map(contact => (
22+
<SidebarItem contact={contact} key={contact.id} />
23+
))}
2224
</ul>
2325
) : (
2426
<p>
@@ -30,29 +32,26 @@ export function Sidebar(this: Remix.Handle) {
3032
};
3133
}
3234

33-
function SidebarItem(this: Remix.Handle, props: { contact: ContactRecord }) {
35+
function SidebarItem(this: Remix.Handle) {
3436
const router = this.context.get(App);
3537

36-
return () => {
37-
const href =
38+
return ({ contact }: { contact: ContactRecord }) => {
39+
const link =
3840
routes.contact.show.href({
39-
contactId: String(props.contact.id),
41+
contactId: String(contact.id),
4042
}) + router.location.search;
4143

4244
return (
4345
<li>
44-
<a
45-
href={href}
46-
on={router.navLink({ activeClass: "active", pendingClass: "pending" })}
47-
>
48-
{props.contact.first || props.contact.last ? (
46+
<a href={link} class={router.when(link, { active: "active", pending: "pending" })}>
47+
{contact.first || contact.last ? (
4948
<>
50-
{props.contact.first} {props.contact.last}
49+
{contact.first} {contact.last}
5150
</>
5251
) : (
5352
<i>No Name</i>
5453
)}
55-
{props.contact.favorite && <span></span>}
54+
{contact.favorite && <span></span>}
5655
</a>
5756
</li>
5857
);

packages/router/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
"LICENSE"
4040
],
4141
"dependencies": {
42-
"@remix-run/dom": "catalog:remix",
4342
"@remix-run/events": "catalog:remix",
4443
"@remix-run/fetch-router": "catalog:remix"
4544
},

packages/router/src/router.ts

Lines changed: 19 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
events,
99
win,
1010
} from "@remix-run/events";
11-
import { connect } from "@remix-run/dom";
1211
import { AppStorage, type RouteHandlers, type RouteMap } from "@remix-run/fetch-router";
1312
import type {
1413
FormEncType,
@@ -794,7 +793,8 @@ export class Router<Renderable> extends EventTarget {
794793
* @param path - The path to check
795794
* @param exact - If true, requires exact match. Default is false (partial match)
796795
*/
797-
isActive(path: string | URL | Path, exact = false): boolean {
796+
isActive(path: string | URL | Path | undefined, exact = false): boolean {
797+
if (!path) return false;
798798
const pathname = this.#pathToString(path);
799799
const currentPath = this.#location.pathname;
800800

@@ -816,7 +816,8 @@ export class Router<Renderable> extends EventTarget {
816816
* @param path - The path to check
817817
* @param exact - If true, requires exact match. Default is false (partial match)
818818
*/
819-
isPending(path: string | URL | Path, exact = false): boolean {
819+
isPending(path: string | URL | Path | undefined, exact = false): boolean {
820+
if (!path) return false;
820821
if (this.#navigating.to.state === "idle") {
821822
return false;
822823
}
@@ -838,6 +839,21 @@ export class Router<Renderable> extends EventTarget {
838839
return pendingPath === pathname || pendingPath.startsWith(`${pathname}/`);
839840
}
840841

842+
when<T, U>(
843+
path: string | URL | Path | undefined,
844+
options: { active: T; pending: U }
845+
): T | U | undefined {
846+
if (this.isActive(path)) {
847+
return options.active;
848+
}
849+
850+
if (this.isPending(path)) {
851+
return options.pending;
852+
}
853+
854+
return undefined;
855+
}
856+
841857
/**
842858
* Submit event handler for forms that routes submissions through the router.
843859
* Use this with event listeners for forms when you need manual control.
@@ -985,54 +1001,6 @@ export class Router<Renderable> extends EventTarget {
9851001
}, options);
9861002
}
9871003

988-
/**
989-
* Create an event descriptor that manages active and pending classes on a navigation link.
990-
* Unlike {@link enhanceLink}, this method applies classes immediately on mount and keeps
991-
* them in sync with router state changes.
992-
*
993-
* @example
994-
* ```tsx
995-
* <a href="/about" on={router.navLink({ activeClass: 'active', pendingClass: 'loading' })}>
996-
* About
997-
* </a>
998-
* ```
999-
*
1000-
* @param styles - Configuration object with activeClass and/or pendingClass
1001-
* @param options - Optional event listener options (e.g., signal for cleanup)
1002-
*/
1003-
navLink(
1004-
styles: { activeClass?: string; pendingClass?: string },
1005-
options: AddEventListenerOptions = {}
1006-
): EventDescriptor<HTMLAnchorElement> {
1007-
return connect(event => {
1008-
const anchor = event.currentTarget as HTMLAnchorElement;
1009-
const targetPath = anchor.pathname + anchor.search + anchor.hash;
1010-
1011-
// Helper to update classes based on current state
1012-
const updateClasses = () => {
1013-
const active = this.isActive(targetPath);
1014-
const pending = !active && this.isPending(targetPath);
1015-
1016-
const { activeClass, pendingClass } = styles ?? {};
1017-
1018-
if (activeClass) anchor.classList.toggle(activeClass, active);
1019-
if (pendingClass) anchor.classList.toggle(pendingClass, pending);
1020-
1021-
// If neither state applies, both toggles above remove their classes.
1022-
// Drop the attribute entirely if no classes remain.
1023-
if (!anchor.classList.length) {
1024-
anchor.removeAttribute("class");
1025-
}
1026-
};
1027-
1028-
// Apply initial classes
1029-
updateClasses();
1030-
1031-
// Listen to router updates and return cleanup function
1032-
return events(this, [Router.update(updateClasses)]);
1033-
}, options);
1034-
}
1035-
10361004
/**
10371005
* Wrap a form submission handler to emit optimistic updates while the mutation is pending.
10381006
*

pnpm-lock.yaml

Lines changed: 0 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)