Skip to content

Instantly share code, notes, and snippets.

@gordonbrander
Created September 22, 2025 07:24
Show Gist options
  • Save gordonbrander/1e7f915ac26d1f14e8294674b471d204 to your computer and use it in GitHub Desktop.
Save gordonbrander/1e7f915ac26d1f14e8294674b471d204 to your computer and use it in GitHub Desktop.
URLPattern Router
export type RouteHandler = (context: URLPatternResult) => void;
export type Cancel = () => void;
type Matcher = (route: string) => URLPatternResult | undefined;
const matcher = (pathname: string, baseUrl?: string): Matcher => {
const pattern = new URLPattern({ pathname, baseURL: baseUrl });
return (route: string) => pattern.exec(route) ?? undefined;
};
export type Route = {
match: Matcher;
handler: RouteHandler;
};
export class Router {
#baseUrl: string | undefined;
#routes: Route[] = [];
constructor(baseUrl?: string) {
this.#baseUrl = baseUrl;
}
#match(location: string): void {
for (const route of this.#routes) {
const match = route.match(location);
if (match) {
route.handler(match);
return;
}
}
}
start(): Cancel {
// Match against initial location
this.#match(globalThis.location.toString());
const onRouteChange = () => {
this.#match(globalThis.location.toString());
};
globalThis.addEventListener("popstate", onRouteChange);
globalThis.addEventListener("router-pushstate", onRouteChange);
return () => {
globalThis.removeEventListener("popstate", onRouteChange);
globalThis.removeEventListener("router-pushstate", onRouteChange);
};
}
on(route: string, handler: RouteHandler) {
const match = matcher(route, this.#baseUrl);
this.#routes.push({
match,
handler,
});
return this;
}
}
export type State = Record<string, unknown>;
export class RouterPushStateEvent extends Event {
url: URL;
state: State;
constructor(
url: URL,
state: State,
) {
super("router-pushstate");
this.url = url;
this.state = state;
}
}
/**
* Navigates to a new route, updating the browser history and dispatching a router push state event.
*
* @param path - The path to navigate to, can be relative or absolute
* @param state - Optional state object to associate with the history entry
* @param query - Optional query parameters to append to the URL
*/
export const navigate = (
path: string,
{
state = {},
query,
}: {
state?: State;
query?: Record<string, string>;
} = {},
) => {
const url = new URL(path, globalThis.location.href);
if (query) {
url.search = new URLSearchParams(query).toString();
}
history.pushState(state, "", url);
globalThis.dispatchEvent(new RouterPushStateEvent(url, state));
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment