preact-hashish-router 0.0.17 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  - Error handling integration with `ErrorRoute`.
8
8
  - Fully typed.
9
9
  - Ultra lightweight.
10
- - No external dependencies.
10
+ - Minimal external dependencies.
11
11
 
12
12
  ## Installation
13
13
 
@@ -22,16 +22,16 @@ npm install preact-hashish-router@latest
22
22
  First, ensure your application is wrapped within the router context. This will allow you to access routes and related functions.
23
23
 
24
24
  ```tsx
25
- import { ErrorRoute, Route, Router, RouterErrorBoundary } from "preact-hashish-router";
26
- import _404 from "./routes/404";
25
+ import { Route, Router, RouterErrorBoundary } from "preact-hashish-router";
27
26
  import AboutPage from "./routes/About";
28
27
  import HomePage from "./routes/Home";
29
28
  import ProductPage from "./routes/Product";
30
29
 
31
30
  export default function App() {
32
31
  return (
33
- <Router type="hash"> {/* <-- or browser */}
34
- <RouterErrorBoundary>
32
+ // or hash for hash-based routing
33
+ <RouterErrorBoundary>
34
+ <Router type="browser">
35
35
  <Route path="/">
36
36
  <HomePage />
37
37
  </Route>
@@ -43,12 +43,8 @@ export default function App() {
43
43
  <Route path="/product/:id">
44
44
  <ProductPage />
45
45
  </Route>
46
-
47
- <ErrorRoute>
48
- <_404 />
49
- </ErrorRoute>
50
- </RouterErrorBoundary>
51
- </Router>
46
+ </Router>
47
+ </RouterErrorBoundary>
52
48
  );
53
49
  }
54
50
  ```
@@ -61,10 +57,10 @@ The `useRouter` hook gives you access to the router context to programmatically
61
57
  import { useRouter } from "preact-hashish-router";
62
58
 
63
59
  function HomePage() {
64
- const router = useRouter();
60
+ const { go, params, path, searchParams } = useRouter();
65
61
 
66
62
  function goToAbout() {
67
- router.go("/about");
63
+ go("/about");
68
64
  }
69
65
 
70
66
  return (
@@ -93,26 +89,6 @@ export default function Header() {
93
89
  }
94
90
  ```
95
91
 
96
- ### `<Redirect />` Component
97
-
98
- ```tsx
99
- import { Redirect } from "preact-hashish-router";
100
-
101
- export default function ProductPage() {
102
- return (
103
- <>
104
- <header>
105
- <nav>
106
- <A href="/">Home</A>
107
- <A href="/about">About</A>
108
- </nav>
109
- </header>
110
- <Redirect to="/" />
111
- </>
112
- );
113
- }
114
- ```
115
-
116
92
  ## Development
117
93
 
118
94
  If you have any improvements or find any issues, feel free to contribute or open an issue in the associated repository.
package/dist/A.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { AnchorHTMLAttributes, PropsWithChildren } from "preact/compat";
2
- export type AProps = PropsWithChildren & AnchorHTMLAttributes;
1
+ import { ComponentProps } from "preact/compat";
2
+ export type AProps = Omit<ComponentProps<"a">, "href"> & {
3
+ href: string;
4
+ };
3
5
  export declare const A: import("preact").FunctionalComponent<import("preact/compat").PropsWithoutRef<AProps> & {
4
6
  ref?: import("preact").Ref<HTMLAnchorElement> | undefined;
5
7
  }>;
package/dist/A.js CHANGED
@@ -1,16 +1,13 @@
1
1
  import { jsx as _jsx } from "preact/jsx-runtime";
2
2
  import { forwardRef } from "preact/compat";
3
- import { useMemo } from "preact/hooks";
4
- import { useInternalRouter } from "./useInternalRouter";
5
- export const A = forwardRef(({ href, className, ...props }) => {
6
- const router = useInternalRouter();
7
- const isActive = useMemo(() => router.path === href?.split("?")[0], [router.path, href]);
8
- const browserRouterClickAnchorHandler = (e) => {
9
- e.preventDefault();
10
- e.stopPropagation();
11
- if (!href)
12
- return;
13
- router.go(href.toString());
14
- };
15
- return (_jsx("a", { href: router.type === "browser" ? href : `#${href}`, className: className, "data-route-active": isActive, ...props, onClick: router.type === "browser" ? browserRouterClickAnchorHandler : undefined }));
3
+ import { useHashisherContext } from "./context";
4
+ export const A = forwardRef(({ href, ...props }, forwardedRef) => {
5
+ const { go } = useHashisherContext();
6
+ if (!href) {
7
+ throw new Error("A: href must be defined");
8
+ }
9
+ return (_jsx("a", { ref: forwardedRef, href: href, onClick: (event) => {
10
+ event.preventDefault();
11
+ go(href);
12
+ }, ...props }));
16
13
  });
@@ -0,0 +1,3 @@
1
+ import { VNode } from "preact";
2
+ export declare const RenderMatchedRoute: () => VNode<any>;
3
+ export declare const set_not_found_element: (el: VNode<any>) => void;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ import { Suspense } from "preact/compat";
3
+ import { useHashisherContext } from "./context";
4
+ export const RenderMatchedRoute = () => {
5
+ const { active_route_data } = useHashisherContext();
6
+ if (!active_route_data)
7
+ return not_found_element;
8
+ if (active_route_data.component === null) {
9
+ return not_found_element;
10
+ }
11
+ if (active_route_data.lazy) {
12
+ return _jsx(Suspense, { fallback: active_route_data.fallback, children: active_route_data.component });
13
+ }
14
+ return active_route_data.component;
15
+ };
16
+ let not_found_element = _jsx("div", { children: "404 Not Found" });
17
+ export const set_not_found_element = (el) => {
18
+ not_found_element = el;
19
+ };
package/dist/Route.d.ts CHANGED
@@ -1,9 +1,17 @@
1
1
  import { VNode } from "preact";
2
- import { PropsWithChildren } from "preact/compat";
3
- export type RouteProps = PropsWithChildren & {
2
+ export type RouteProps = {
3
+ /** The route path matcher
4
+ * @example "/"
5
+ * "/product/:id"
6
+ * "/paper/*"
7
+ * "/docs/**"
8
+ */
4
9
  path: string;
5
- exact?: boolean;
10
+ /** The node that will be rendered if match */
11
+ element: VNode<any>;
12
+ /** Shoud be wrapped in a \<Suspense /> tag */
6
13
  lazy?: boolean;
14
+ /** Fallback to display when the element is loading when lazy is true */
7
15
  fallback?: VNode;
8
16
  };
9
- export declare function Route(props: RouteProps): import("preact").ComponentChildren;
17
+ export declare function Route(props: RouteProps): import("preact").JSX.Element;
package/dist/Route.js CHANGED
@@ -1,31 +1,6 @@
1
- import { jsx as _jsx } from "preact/jsx-runtime";
2
- import { Suspense, useLayoutEffect, useState } from "preact/compat";
3
- import { matchRoute } from "./match";
4
- import { useInternalRouter } from "./useInternalRouter";
1
+ import { Fragment as _Fragment, jsx as _jsx } from "preact/jsx-runtime";
2
+ import { add_route_to_matcher } from "./router/matcher";
5
3
  export function Route(props) {
6
- const router = useInternalRouter();
7
- const [render, setRender] = useState(false);
8
- useLayoutEffect(() => {
9
- setRender(false);
10
- if (props.exact === undefined) {
11
- props.exact = false;
12
- }
13
- if (props.exact === true && router.path !== props.path) {
14
- return;
15
- }
16
- const matches = matchRoute(router.path || "/", props.path);
17
- if (props.exact === false && matches === undefined) {
18
- return;
19
- }
20
- router.setParams(matches?.params || {});
21
- router.setRest(matches?.rest);
22
- router.setItMatch(true);
23
- setRender(true);
24
- }, [router.path]);
25
- if (!render)
26
- return null;
27
- if (props.lazy) {
28
- return _jsx(Suspense, { fallback: props.fallback ?? _jsx("div", { children: "Loading..." }), children: props.children });
29
- }
30
- return props.children;
4
+ add_route_to_matcher(props.path, props);
5
+ return _jsx(_Fragment, {});
31
6
  }
package/dist/Router.d.ts CHANGED
@@ -1,16 +1,9 @@
1
+ import { VNode } from "preact";
1
2
  import { PropsWithChildren } from "preact/compat";
2
- import { RouterContext } from "./context";
3
- type RouterProps = PropsWithChildren & {
4
- type: RouterContext["type"];
5
- /**
6
- * Only for `hash` routers.
7
- *
8
- * Decide if the initial pathname will be rewrite as the initial hash.
9
- *
10
- * `Caution`: This will replace the initial url hash
11
- * @default false
12
- */
13
- redirect_path_to_hash?: boolean;
14
- };
3
+ export type RouterProps = PropsWithChildren<{
4
+ type: "browser";
5
+ }>;
15
6
  export declare const Router: (props: RouterProps) => import("preact").JSX.Element;
16
- export {};
7
+ export declare const NotFound: (props: {
8
+ element: VNode<any>;
9
+ }) => import("preact").JSX.Element;
package/dist/Router.js CHANGED
@@ -1,101 +1,66 @@
1
- import { jsx as _jsx } from "preact/jsx-runtime";
2
- import { useLayoutEffect, useMemo, useState } from "preact/hooks";
3
- import { router_context } from "./context";
4
- const get_hash_route = () => location.hash.slice(1) || "/";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
+ import { useCallback, useLayoutEffect, useState } from "preact/hooks";
3
+ import { findRoute } from "rou3";
4
+ import { parseURL } from "ufo";
5
+ import { HashisherContext } from "./context";
6
+ import { RenderMatchedRoute, set_not_found_element } from "./RenderMatchedRoute";
7
+ import { Matcher } from "./router/matcher";
5
8
  export const Router = (props) => {
6
- const [path, setPath] = useState("");
7
- const [query, setQuery] = useState("");
8
- const [params, setParams] = useState({});
9
- const [rest, setRest] = useState("");
10
- const [itMatch, setItMatch] = useState(false);
11
- const router_type = useMemo(() => {
12
- return props.type;
13
- }, [props.type]);
14
- const hashEffectHandler = {
15
- listener: () => {
16
- const [newPath, query] = get_hash_route().split("?");
17
- setQuery(query || "");
18
- setPath(newPath);
19
- if (newPath !== path) {
20
- setItMatch(false);
9
+ const [active_path, set_active_path] = useState(() => {
10
+ if (typeof window !== "undefined")
11
+ return window.location.pathname;
12
+ return null;
13
+ });
14
+ const [params, setParams] = useState(undefined);
15
+ const [searchParams, setSearchParams] = useState(new URLSearchParams());
16
+ const [active_route_data, set_active_route_data] = useState(null);
17
+ const execute_path_change = useCallback((raw_path) => {
18
+ const url = parseURL(window.location.href);
19
+ const newPath = raw_path === null ? url.pathname : raw_path;
20
+ const route_data = findRoute(Matcher, undefined, newPath);
21
+ if (!route_data) {
22
+ set_active_path(newPath);
23
+ setSearchParams(new URLSearchParams(url.search));
24
+ set_active_route_data(null);
25
+ setParams(undefined);
26
+ if (props.type === "browser") {
27
+ window.history.pushState(null, "", newPath);
21
28
  }
22
- },
23
- effect: () => {
24
- window.addEventListener("hashchange", hashEffectHandler.listener);
25
- },
26
- cleanUp: () => {
27
- window.removeEventListener("hashchange", hashEffectHandler.listener);
28
- },
29
- };
30
- const browserEffectHandler = {
31
- listener: () => {
32
- setPath(location.pathname);
33
- setQuery(location.search.split("?")[1] || "");
34
- if (path !== location.pathname) {
35
- setItMatch(false);
36
- }
37
- },
38
- effect: () => {
39
- window.addEventListener("popstate", browserEffectHandler.listener);
40
- },
41
- cleanUp: () => {
42
- window.removeEventListener("popstate", browserEffectHandler.listener);
43
- },
44
- };
45
- useLayoutEffect(() => {
46
- if (router_type !== "hash")
47
29
  return;
48
- const [path, query] = get_hash_route().split("?");
49
- setQuery(query || "");
50
- setPath(path);
51
- if (props.redirect_path_to_hash === true) {
52
- if (location.pathname !== "/") {
53
- location.hash = location.pathname;
54
- location.pathname = "";
55
- }
56
30
  }
57
- hashEffectHandler.effect();
58
- return () => hashEffectHandler.cleanUp();
31
+ set_active_path(newPath);
32
+ setSearchParams(new URLSearchParams(url.search));
33
+ setParams({ ...route_data.params });
34
+ set_active_route_data({ ...route_data.data });
35
+ if (props.type === "browser") {
36
+ window.history.pushState(null, "", newPath);
37
+ }
59
38
  }, []);
60
39
  useLayoutEffect(() => {
61
- if (router_type !== "browser")
40
+ if (props.type !== "browser")
62
41
  return;
63
- setPath(location.pathname);
64
- setQuery(location.search.split("?")[1] || "");
65
- browserEffectHandler.effect();
66
- return () => browserEffectHandler.cleanUp();
42
+ const listener = () => {
43
+ execute_path_change(null);
44
+ };
45
+ window.addEventListener("popstate", listener);
46
+ listener();
47
+ return () => {
48
+ window.removeEventListener("popstate", listener);
49
+ };
67
50
  }, []);
68
- const handlerManualRouteChange = (newPath) => {
69
- const [np, nq] = newPath.split("?");
70
- if (path === np && query === nq)
71
- return;
72
- if (router_type === "hash") {
73
- location.hash = newPath;
74
- return;
75
- }
76
- if (router_type === "browser") {
77
- setPath(np);
78
- setQuery(nq || "");
79
- if (path !== np) {
80
- setItMatch(false);
81
- }
82
- history.pushState(null, "", new URL(newPath, location.origin));
83
- return;
84
- }
51
+ const go_imperative = (newPath) => {
52
+ const pathname = parseURL(newPath).pathname;
53
+ execute_path_change(pathname);
85
54
  };
86
- const ProviderValue = useMemo(() => {
87
- return {
88
- path,
89
- go: handlerManualRouteChange,
90
- itMatch,
91
- setItMatch,
92
- type: router_type,
93
- query,
55
+ return (_jsxs(HashisherContext.Provider, { value: {
56
+ active_path,
57
+ searchParams,
94
58
  params,
95
- rest,
96
- setParams,
97
- setRest,
98
- };
99
- }, [path, handlerManualRouteChange, itMatch, setItMatch, router_type]);
100
- return _jsx(router_context.Provider, { value: ProviderValue, children: props.children });
59
+ active_route_data,
60
+ go: go_imperative,
61
+ }, children: [props.children, _jsx(RenderMatchedRoute, {})] }));
62
+ };
63
+ export const NotFound = (props) => {
64
+ set_not_found_element(props.element);
65
+ return _jsx(_Fragment, {});
101
66
  };
@@ -1,15 +1,11 @@
1
1
  import { jsxs as _jsxs } from "preact/jsx-runtime";
2
2
  import { Component } from "preact";
3
3
  export class RouterErrorBoundary extends Component {
4
- constructor() {
5
- super(...arguments);
6
- this.state = { error: null };
7
- }
4
+ state = { error: null };
8
5
  static getDerivedStateFromError(error) {
9
6
  return { error: error.message };
10
7
  }
11
8
  componentDidCatch(error) {
12
- console.error(error);
13
9
  this.setState({ error: error.message });
14
10
  }
15
11
  render() {
package/dist/context.d.ts CHANGED
@@ -1,14 +1,22 @@
1
- import { Matches } from "./match";
2
- export type RouterContext = {
3
- type: "hash" | "browser";
4
- path: string;
5
- query: string;
6
- go: (r: string) => void;
7
- itMatch: boolean;
8
- setItMatch: (r: boolean) => void;
9
- params: Matches["params"];
10
- setParams: (p: Matches["params"]) => void;
11
- rest: Matches["rest"];
12
- setRest: (r: Matches["rest"]) => void;
13
- };
14
- export declare const router_context: import("preact").Context<RouterContext | null>;
1
+ import { RouteMatched } from "./router/matcher";
2
+ export type HashisherContextVal = {
3
+ active_path: string | null;
4
+ params: RouteMatched["params"];
5
+ active_route_data: RouteMatched["data"] | null;
6
+ searchParams: URLSearchParams;
7
+ };
8
+ export type HashisherContextMethods = {
9
+ go(newPath: string): void;
10
+ };
11
+ export declare const HashisherContext: import("preact").Context<HashisherContextVal & HashisherContextMethods>;
12
+ export declare const useHashisherContext: () => HashisherContextVal & HashisherContextMethods;
13
+ export declare function useParams<T extends Record<string, string>>(): T & {
14
+ _?: string;
15
+ };
16
+ export declare function useSearchParams(): URLSearchParams;
17
+ export declare const useRouter: () => {
18
+ path: string | null;
19
+ params: Record<string, string> | undefined;
20
+ go: (newPath: string) => void;
21
+ searchParams: URLSearchParams;
22
+ };
package/dist/context.js CHANGED
@@ -1,2 +1,38 @@
1
1
  import { createContext } from "preact";
2
- export const router_context = createContext(null);
2
+ import { useContext } from "preact/hooks";
3
+ export const HashisherContext = createContext({
4
+ active_path: "",
5
+ active_route_data: null,
6
+ params: undefined,
7
+ searchParams: new URLSearchParams(),
8
+ go() { },
9
+ });
10
+ export const useHashisherContext = () => {
11
+ const c = useContext(HashisherContext);
12
+ if (!c)
13
+ throw new Error("useHashisherContext should be inside a HashisherContext provider");
14
+ return c;
15
+ };
16
+ export function useParams() {
17
+ const c = useContext(HashisherContext);
18
+ if (!c)
19
+ throw new Error("useParams should be inside a HashisherContext provider");
20
+ return c.params;
21
+ }
22
+ export function useSearchParams() {
23
+ const c = useContext(HashisherContext);
24
+ if (!c)
25
+ throw new Error("useSearchParams should be inside a HashisherContext provider");
26
+ return c.searchParams;
27
+ }
28
+ export const useRouter = () => {
29
+ const c = useContext(HashisherContext);
30
+ if (!c)
31
+ throw new Error("useRouter should be inside a HashisherContext provider");
32
+ return {
33
+ path: c.active_path,
34
+ params: c.params,
35
+ go: c.go,
36
+ searchParams: c.searchParams,
37
+ };
38
+ };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  export * from "./A";
2
- export * from "./ErrorRoute";
3
- export * from "./Redirect";
2
+ export * from "./context";
4
3
  export * from "./Route";
5
4
  export * from "./Router";
6
5
  export * from "./RouterErrorBoundary";
7
- export * from "./useRouter";
package/dist/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  export * from "./A";
2
- export * from "./ErrorRoute";
3
- export * from "./Redirect";
2
+ export * from "./context";
4
3
  export * from "./Route";
5
4
  export * from "./Router";
6
5
  export * from "./RouterErrorBoundary";
7
- export * from "./useRouter";
@@ -0,0 +1,11 @@
1
+ import { VNode } from "preact";
2
+ import { MatchedRoute } from "rou3";
3
+ import { RouteProps } from "../Route";
4
+ export type MatcherPayload = {
5
+ component: VNode<any>;
6
+ lazy: boolean;
7
+ fallback: VNode<any> | null;
8
+ };
9
+ export type RouteMatched = MatchedRoute<MatcherPayload>;
10
+ export declare const Matcher: import("rou3").RouterContext<MatcherPayload>;
11
+ export declare const add_route_to_matcher: (path: string, data: Omit<RouteProps, "path">) => void;
@@ -0,0 +1,9 @@
1
+ import { addRoute, createRouter } from "rou3";
2
+ export const Matcher = createRouter();
3
+ export const add_route_to_matcher = (path, data) => {
4
+ addRoute(Matcher, undefined, path, {
5
+ component: data.element,
6
+ fallback: data.fallback || null,
7
+ lazy: Boolean(data.lazy),
8
+ });
9
+ };
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "preact-hashish-router",
3
- "version": "0.0.17",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "module": "dist/index.js",
6
6
  "description": "A simple router for preact",
7
7
  "scripts": {
8
8
  "build": "tsc -p ./tsconfig.lib.json",
9
- "format": "prettier --write src app --ignore-unknown",
9
+ "format": "prettier --write src app --ignore-unknown --cache",
10
10
  "app:dev": "vite",
11
- "prepublishOnly": "npm run build && npm run format",
11
+ "prepublishOnly": "npm run build",
12
12
  "push": "npm version patch && git push",
13
13
  "push-minor": "npm version minor && git push",
14
14
  "push-major": "npm version major && git push"
@@ -16,9 +16,6 @@
16
16
  "exports": {
17
17
  ".": "./dist/index.js"
18
18
  },
19
- "peerDependencies": {
20
- "preact": "^10.26.2"
21
- },
22
19
  "keywords": [
23
20
  "preact",
24
21
  "router",
@@ -37,17 +34,25 @@
37
34
  "url": "https://github.com/LiasCode/preact-hashish-router"
38
35
  },
39
36
  "devDependencies": {
40
- "@preact/preset-vite": "^2.10.1",
41
- "@types/node": "^22.13.4",
42
- "prettier": "^3.5.1",
43
- "prettier-plugin-organize-imports": "^4.1.0",
44
- "typescript": "^5.7.3",
45
- "vite": "^6.1.0"
37
+ "@preact/preset-vite": "^2.10.2",
38
+ "@types/node": "^22.17.1",
39
+ "preact-render-to-string": "^6.5.13",
40
+ "prettier": "^3.6.2",
41
+ "prettier-plugin-organize-imports": "^4.2.0",
42
+ "typescript": "^5.9.2",
43
+ "vite": "^6.3.5"
46
44
  },
47
45
  "files": [
48
46
  "dist",
49
47
  "LICENSE",
50
48
  "package.json",
51
49
  "README.md"
52
- ]
50
+ ],
51
+ "peerDependencies": {
52
+ "preact": "^10.27.0"
53
+ },
54
+ "dependencies": {
55
+ "rou3": "^0.7.3",
56
+ "ufo": "^1.6.1"
57
+ }
53
58
  }
@@ -1 +0,0 @@
1
- export declare function useInternalRouter(): import("./context").RouterContext;
@@ -1,9 +0,0 @@
1
- import { useContext } from "preact/hooks";
2
- import { router_context } from "./context";
3
- export function useInternalRouter() {
4
- const context = useContext(router_context);
5
- if (!context) {
6
- throw new Error("useInternalRouter should be used within a Router");
7
- }
8
- return context;
9
- }