Created
September 22, 2025 07:24
-
-
Save gordonbrander/1e7f915ac26d1f14e8294674b471d204 to your computer and use it in GitHub Desktop.
URLPattern Router
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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