preact-missing-hooks 4.5.0 → 4.7.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/dist/react.js CHANGED
@@ -2008,13 +2008,168 @@ function useRBAC(options) {
2008
2008
  };
2009
2009
  }
2010
2010
 
2011
+ function prefetchDocument(url) {
2012
+ if (typeof document === "undefined")
2013
+ return;
2014
+ const links = document.querySelectorAll('link[rel="prefetch"]');
2015
+ for (let i = 0; i < links.length; i++) {
2016
+ if (links[i].href === url)
2017
+ return;
2018
+ }
2019
+ const link = document.createElement("link");
2020
+ link.rel = "prefetch";
2021
+ link.href = url;
2022
+ document.head.appendChild(link);
2023
+ }
2024
+ function prefetchFetch(url) {
2025
+ if (typeof fetch === "undefined")
2026
+ return;
2027
+ fetch(url, { method: "GET", mode: "cors" }).catch(() => {
2028
+ /* ignore; prefetch is best-effort */
2029
+ });
2030
+ }
2031
+ /**
2032
+ * A Preact hook that returns a stable prefetch function to preload URLs (documents or data)
2033
+ * so they are cached before the user navigates or needs them. Useful for link hover or
2034
+ * route preloading.
2035
+ *
2036
+ * @returns Object with prefetch(url, options?) and isPrefetched(url)
2037
+ *
2038
+ * @example
2039
+ * ```tsx
2040
+ * function NavLink({ href, children }) {
2041
+ * const { prefetch } = usePrefetch();
2042
+ * return (
2043
+ * <a
2044
+ * href={href}
2045
+ * onMouseEnter={() => prefetch(href)}
2046
+ * >
2047
+ * {children}
2048
+ * </a>
2049
+ * );
2050
+ * }
2051
+ * ```
2052
+ *
2053
+ * @example
2054
+ * ```tsx
2055
+ * // Prefetch API data
2056
+ * const { prefetch } = usePrefetch();
2057
+ * prefetch('/api/user', { as: 'fetch' });
2058
+ * ```
2059
+ */
2060
+ function usePrefetch() {
2061
+ const prefetchedRef = react.useRef(new Set());
2062
+ const [, setTick] = react.useState(0);
2063
+ const prefetch = react.useCallback((url, options) => {
2064
+ var _a;
2065
+ const trimmed = url === null || url === void 0 ? void 0 : url.trim();
2066
+ if (!trimmed)
2067
+ return;
2068
+ if (prefetchedRef.current.has(trimmed))
2069
+ return;
2070
+ const as = (_a = options === null || options === void 0 ? void 0 : options.as) !== null && _a !== void 0 ? _a : "document";
2071
+ if (as === "document") {
2072
+ prefetchDocument(trimmed);
2073
+ }
2074
+ else {
2075
+ prefetchFetch(trimmed);
2076
+ }
2077
+ prefetchedRef.current.add(trimmed);
2078
+ setTick((n) => n + 1);
2079
+ }, []);
2080
+ const isPrefetched = react.useCallback((url) => {
2081
+ var _a;
2082
+ return prefetchedRef.current.has((_a = url === null || url === void 0 ? void 0 : url.trim()) !== null && _a !== void 0 ? _a : "");
2083
+ }, []);
2084
+ return { prefetch, isPrefetched };
2085
+ }
2086
+
2087
+ /**
2088
+ * Polls an async function at a fixed interval until it returns { done: true, data? }.
2089
+ * Useful for waiting on a backend job, readiness checks, or until a condition is met.
2090
+ *
2091
+ * @param pollFn - Async function called each tick. Return { done: true, data? } to stop and set result.
2092
+ * @param options - intervalMs, immediate, enabled
2093
+ * @returns { data, done, error, pollCount, start, stop }
2094
+ *
2095
+ * @example
2096
+ * ```tsx
2097
+ * const { data, done, pollCount } = usePoll(
2098
+ * async () => {
2099
+ * const res = await fetch('/api/status');
2100
+ * const json = await res.json();
2101
+ * return json.ready ? { done: true, data: json } : { done: false };
2102
+ * },
2103
+ * { intervalMs: 500, immediate: true }
2104
+ * );
2105
+ * return done ? <div>Ready: {JSON.stringify(data)}</div> : <div>Polling… ({pollCount})</div>;
2106
+ * ```
2107
+ */
2108
+ function usePoll(pollFn, options = {}) {
2109
+ const { intervalMs = 1000, immediate = true, enabled = true } = options;
2110
+ const [data, setData] = react.useState(null);
2111
+ const [done, setDone] = react.useState(false);
2112
+ const [error, setError] = react.useState(null);
2113
+ const [pollCount, setPollCount] = react.useState(0);
2114
+ const [running, setRunning] = react.useState(false);
2115
+ const intervalRef = react.useRef(null);
2116
+ const pollFnRef = react.useRef(pollFn);
2117
+ pollFnRef.current = pollFn;
2118
+ const stop = react.useCallback(() => {
2119
+ if (intervalRef.current != null) {
2120
+ clearInterval(intervalRef.current);
2121
+ intervalRef.current = null;
2122
+ }
2123
+ setRunning(false);
2124
+ }, []);
2125
+ const tick = react.useCallback(() => __awaiter(this, void 0, void 0, function* () {
2126
+ try {
2127
+ setError(null);
2128
+ const result = yield pollFnRef.current();
2129
+ setPollCount((n) => n + 1);
2130
+ if (result.done) {
2131
+ stop();
2132
+ setDone(true);
2133
+ if (result.data !== undefined) {
2134
+ setData(result.data);
2135
+ }
2136
+ }
2137
+ }
2138
+ catch (e) {
2139
+ const err = e instanceof Error ? e : new Error(String(e));
2140
+ setError(err);
2141
+ stop();
2142
+ }
2143
+ }), [stop]);
2144
+ const start = react.useCallback(() => {
2145
+ if (!enabled || running)
2146
+ return;
2147
+ setRunning(true);
2148
+ if (immediate) {
2149
+ tick();
2150
+ }
2151
+ intervalRef.current = setInterval(tick, intervalMs);
2152
+ }, [enabled, immediate, intervalMs, running, tick]);
2153
+ react.useEffect(() => {
2154
+ if (enabled) {
2155
+ start();
2156
+ }
2157
+ return () => {
2158
+ stop();
2159
+ };
2160
+ }, [enabled]);
2161
+ return { data, done, error, pollCount, start, stop };
2162
+ }
2163
+
2011
2164
  exports.useClipboard = useClipboard;
2012
2165
  exports.useEventBus = useEventBus;
2013
2166
  exports.useIndexedDB = useIndexedDB;
2014
2167
  exports.useLLMMetadata = useLLMMetadata;
2015
2168
  exports.useMutationObserver = useMutationObserver;
2016
2169
  exports.useNetworkState = useNetworkState;
2170
+ exports.usePoll = usePoll;
2017
2171
  exports.usePreferredTheme = usePreferredTheme;
2172
+ exports.usePrefetch = usePrefetch;
2018
2173
  exports.useRBAC = useRBAC;
2019
2174
  exports.useRageClick = useRageClick;
2020
2175
  exports.useRefPrint = useRefPrint;
@@ -0,0 +1,47 @@
1
+ export interface UsePollOptions {
2
+ /** Polling interval in milliseconds. Default: 1000 */
3
+ intervalMs?: number;
4
+ /** Run the poll function immediately when the hook mounts. Default: true */
5
+ immediate?: boolean;
6
+ /** When false, do not start or continue polling. Default: true */
7
+ enabled?: boolean;
8
+ }
9
+ export interface UsePollResult<T> {
10
+ /** Last resolved data when poll returned done: true */
11
+ data: T | null;
12
+ /** True once the poll function returned { done: true } */
13
+ done: boolean;
14
+ /** Error from the last failed poll call */
15
+ error: Error | null;
16
+ /** Number of times the poll function has been invoked */
17
+ pollCount: number;
18
+ /** Manually start polling (e.g. after reset). Only has effect when not already polling. */
19
+ start: () => void;
20
+ /** Stop polling. */
21
+ stop: () => void;
22
+ }
23
+ /**
24
+ * Polls an async function at a fixed interval until it returns { done: true, data? }.
25
+ * Useful for waiting on a backend job, readiness checks, or until a condition is met.
26
+ *
27
+ * @param pollFn - Async function called each tick. Return { done: true, data? } to stop and set result.
28
+ * @param options - intervalMs, immediate, enabled
29
+ * @returns { data, done, error, pollCount, start, stop }
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * const { data, done, pollCount } = usePoll(
34
+ * async () => {
35
+ * const res = await fetch('/api/status');
36
+ * const json = await res.json();
37
+ * return json.ready ? { done: true, data: json } : { done: false };
38
+ * },
39
+ * { intervalMs: 500, immediate: true }
40
+ * );
41
+ * return done ? <div>Ready: {JSON.stringify(data)}</div> : <div>Polling… ({pollCount})</div>;
42
+ * ```
43
+ */
44
+ export declare function usePoll<T>(pollFn: () => Promise<{
45
+ done: boolean;
46
+ data?: T;
47
+ }>, options?: UsePollOptions): UsePollResult<T>;
@@ -0,0 +1,44 @@
1
+ export interface UsePrefetchOptions {
2
+ /**
3
+ * Resource type for prefetch.
4
+ * - "document": uses <link rel="prefetch"> (default, for next navigation)
5
+ * - "fetch": uses fetch() to warm the HTTP cache (e.g. for API or same-origin data)
6
+ */
7
+ as?: "document" | "fetch";
8
+ }
9
+ export interface UsePrefetchReturn {
10
+ /** Prefetch a URL so it is cached for later use. No-op if URL was already prefetched or empty. */
11
+ prefetch: (url: string, options?: UsePrefetchOptions) => void;
12
+ /** Check whether a URL has already been prefetched in this hook instance. */
13
+ isPrefetched: (url: string) => boolean;
14
+ }
15
+ /**
16
+ * A Preact hook that returns a stable prefetch function to preload URLs (documents or data)
17
+ * so they are cached before the user navigates or needs them. Useful for link hover or
18
+ * route preloading.
19
+ *
20
+ * @returns Object with prefetch(url, options?) and isPrefetched(url)
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * function NavLink({ href, children }) {
25
+ * const { prefetch } = usePrefetch();
26
+ * return (
27
+ * <a
28
+ * href={href}
29
+ * onMouseEnter={() => prefetch(href)}
30
+ * >
31
+ * {children}
32
+ * </a>
33
+ * );
34
+ * }
35
+ * ```
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * // Prefetch API data
40
+ * const { prefetch } = usePrefetch();
41
+ * prefetch('/api/user', { as: 'fetch' });
42
+ * ```
43
+ */
44
+ export declare function usePrefetch(): UsePrefetchReturn;
package/docs/README.md CHANGED
@@ -41,6 +41,8 @@ Open `docs/index.html` in a browser that supports ES modules and import maps. Th
41
41
  | **useWrappedChildren** | Children buttons get injected styles. |
42
42
  | **usePreferredTheme** | Shows light / dark / no-preference from system. |
43
43
  | **useNetworkState** | Online/offline and connection type. |
44
+ | **usePrefetch** | Hover or click to prefetch a URL (document or fetch); see prefetched status. |
45
+ | **usePoll** | Poll until done (3 ticks); see poll count and result. Stop button to cancel. |
44
46
  | **useClipboard** | Copy and paste; see “Copied!” and pasted text. |
45
47
  | **useRageClick** | Click the area 3+ times quickly; rage click count. |
46
48
  | **useThreadedWorker** | Run a task; see loading and result. |
@@ -48,6 +50,8 @@ Open `docs/index.html` in a browser that supports ES modules and import maps. Th
48
50
  | **useWebRTCIP** | Detect IP via WebRTC (may take a few seconds). |
49
51
  | **useWasmCompute** | Run WASM in a worker (needs `add.wasm` in docs). |
50
52
  | **useWorkerNotifications** | Run/fail tasks and queue updates; toasts show events. |
53
+ | **useRefPrint** | Bind a ref to a section and click “Print / Save as PDF”; only that section is printed via `@media print`. |
54
+ | **useRBAC** | Login as Admin / Editor / Viewer (localStorage or sessionStorage); see roles and capabilities; conditional UI by `can(...)`. |
51
55
  | **useLLMMetadata** | Change “route” with buttons; see injected script info in the Live panel and `<script data-llm="true">` in the document head. Safe with `null`/`undefined` config (minimal payload with `route: "/"`). |
52
56
 
53
57
  ---
@@ -88,6 +92,110 @@ function App() {
88
92
  }
89
93
  ```
90
94
 
95
+ ### useRefPrint example
96
+
97
+ Print only a specific section via the native print dialog (user can save as PDF):
98
+
99
+ ```js
100
+ import { h, render } from "preact";
101
+ import { useRef } from "preact/hooks";
102
+ import { useRefPrint } from "preact-missing-hooks";
103
+
104
+ function Report() {
105
+ const printRef = useRef(null);
106
+ const { print } = useRefPrint(printRef, {
107
+ documentTitle: "My Report",
108
+ downloadAsPdf: true,
109
+ });
110
+ return h(
111
+ "div",
112
+ {},
113
+ h(
114
+ "div",
115
+ { ref: printRef, style: { padding: "1rem", background: "#f5f5f5" } },
116
+ "Only this section is printed when you click the button."
117
+ ),
118
+ h("button", { onClick: print }, "Print / Save as PDF")
119
+ );
120
+ }
121
+ render(h(Report), document.getElementById("root"));
122
+ ```
123
+
124
+ ### useRBAC example
125
+
126
+ Frontend-only role-based access: define roles with conditions, assign capabilities per role, and read the current user from localStorage (or sessionStorage / API):
127
+
128
+ ```js
129
+ import { h, render } from "preact";
130
+ import { useRBAC } from "preact-missing-hooks";
131
+
132
+ const roleDefinitions = [
133
+ { role: "admin", condition: (u) => u && u.role === "admin" },
134
+ {
135
+ role: "editor",
136
+ condition: (u) => u && (u.role === "editor" || u.role === "admin"),
137
+ },
138
+ { role: "viewer", condition: (u) => u && u.id != null },
139
+ ];
140
+ const roleCapabilities = {
141
+ admin: ["*"],
142
+ editor: ["posts:edit", "posts:create", "posts:read"],
143
+ viewer: ["posts:read"],
144
+ };
145
+
146
+ function App() {
147
+ const { user, roles, can, setUserInStorage } = useRBAC({
148
+ userSource: { type: "localStorage", key: "app-user" },
149
+ roleDefinitions,
150
+ roleCapabilities,
151
+ });
152
+
153
+ if (!user) {
154
+ return h(
155
+ "div",
156
+ {},
157
+ h(
158
+ "button",
159
+ {
160
+ onClick: () =>
161
+ setUserInStorage(
162
+ { id: 1, role: "admin" },
163
+ "localStorage",
164
+ "app-user"
165
+ ),
166
+ },
167
+ "Login as Admin"
168
+ ),
169
+ h(
170
+ "button",
171
+ {
172
+ onClick: () =>
173
+ setUserInStorage(
174
+ { id: 2, role: "viewer" },
175
+ "localStorage",
176
+ "app-user"
177
+ ),
178
+ },
179
+ "Login as Viewer"
180
+ )
181
+ );
182
+ }
183
+ return h(
184
+ "div",
185
+ {},
186
+ h("p", {}, "Roles: " + roles.join(", ")),
187
+ can("posts:edit") && h("button", {}, "Edit post"),
188
+ can("*") && h("button", {}, "Admin panel"),
189
+ h(
190
+ "button",
191
+ { onClick: () => setUserInStorage(null, "localStorage", "app-user") },
192
+ "Logout"
193
+ )
194
+ );
195
+ }
196
+ render(h(App), document.getElementById("root"));
197
+ ```
198
+
91
199
  ### useLLMMetadata in the demo
92
200
 
93
201
  The **useLLMMetadata** Live panel simulates route changes. Click “Route: /”, “Route: /blog”, or “Route: /docs”. Each change:
package/docs/main.js CHANGED
@@ -21,6 +21,8 @@ const {
21
21
  useLLMMetadata,
22
22
  useRefPrint,
23
23
  useRBAC,
24
+ usePrefetch,
25
+ usePoll,
24
26
  } = await import(
25
27
  isLocal ? '../dist/index.module.js' : 'https://unpkg.com/preact-missing-hooks/dist/index.module.js'
26
28
  );
@@ -84,6 +86,55 @@ function DemoNetworkState() {
84
86
  );
85
87
  }
86
88
 
89
+ function DemoPrefetch() {
90
+ const { prefetch, isPrefetched } = usePrefetch();
91
+ const [lastUrl, setLastUrl] = useState('');
92
+ const demoUrl = 'https://example.com/prefetched-page';
93
+ const fetchUrl = 'https://httpbin.org/get';
94
+ const doPrefetchDoc = () => { prefetch(demoUrl); setLastUrl(demoUrl); };
95
+ const doPrefetchFetch = () => { prefetch(fetchUrl, { as: 'fetch' }); setLastUrl(fetchUrl); };
96
+ return h('div', {},
97
+ h('div', { style: { marginBottom: '0.5rem', fontSize: '0.85rem' } }, [
98
+ h('button', {
99
+ onClick: doPrefetchDoc,
100
+ onMouseEnter: doPrefetchDoc,
101
+ }, 'Prefetch document'),
102
+ ' ',
103
+ h('button', {
104
+ onClick: doPrefetchFetch,
105
+ onMouseEnter: doPrefetchFetch,
106
+ }, 'Prefetch (fetch)'),
107
+ ]),
108
+ lastUrl
109
+ ? h('span', { class: 'badge green', style: { marginLeft: '0.35rem' } },
110
+ 'Prefetched: ' + lastUrl + (isPrefetched(lastUrl) ? ' ✓' : ''))
111
+ : h('span', { class: 'status' }, 'Hover or click to prefetch a URL (document or fetch).')
112
+ );
113
+ }
114
+
115
+ function DemoPoll() {
116
+ const countRef = { current: 0 };
117
+ const { data, done, error, pollCount, start, stop } = usePoll(
118
+ async () => {
119
+ countRef.current += 1;
120
+ if (countRef.current >= 3) return { done: true, data: { message: 'Ready after ' + countRef.current + ' polls' } };
121
+ return { done: false };
122
+ },
123
+ { intervalMs: 700, immediate: true }
124
+ );
125
+ return h('div', {},
126
+ h('div', { style: { marginBottom: '0.5rem', fontSize: '0.85rem' } }, [
127
+ h('button', { onClick: start }, 'Start'),
128
+ ' ',
129
+ h('button', { onClick: stop }, 'Stop'),
130
+ ]),
131
+ error ? h('span', { class: 'badge', style: { background: 'var(--red)', color: '#fff' } }, error.message) : null,
132
+ done
133
+ ? h('span', { class: 'badge green', style: { marginLeft: '0.35rem' } }, data?.message ?? 'Done')
134
+ : h('span', { class: 'status' }, 'Polling… (' + pollCount + ' calls)')
135
+ );
136
+ }
137
+
87
138
  function DemoClipboard() {
88
139
  const { copy, paste, copied, error } = useClipboard();
89
140
  const [pasted, setPasted] = useState('');
@@ -511,6 +562,20 @@ const HOOKS = [
511
562
  code: `const state = useNetworkState();\n// state.online, state.effectiveType, ...`,
512
563
  Live: DemoNetworkState,
513
564
  },
565
+ {
566
+ name: 'usePrefetch',
567
+ flow: 'Component → usePrefetch() → prefetch(url, options?) → link rel=prefetch or fetch()',
568
+ summary: 'Preload URLs (documents or data) so they are cached before navigation or use. Ideal for link hover or route preloading.',
569
+ code: `const { prefetch, isPrefetched } = usePrefetch();\n<a onMouseEnter={() => prefetch(href)} href={href}>Link</a>\n// or prefetch(url, { as: 'fetch' }) for API`,
570
+ Live: DemoPrefetch,
571
+ },
572
+ {
573
+ name: 'usePoll',
574
+ flow: 'Component → usePoll(pollFn, { intervalMs, immediate }) → poll until done: true → data, done, pollCount',
575
+ summary: 'Polls an async function at a fixed interval until it returns { done: true, data? }. Stops on error. Good for readiness checks or waiting on a backend job.',
576
+ code: `const { data, done, error, pollCount, stop } = usePoll(\n async () => (await fetch('/api/status')).ok ? { done: true, data } : { done: false },\n { intervalMs: 1000, immediate: true }\n);`,
577
+ Live: DemoPoll,
578
+ },
514
579
  {
515
580
  name: 'useClipboard',
516
581
  flow: 'Component → useClipboard() → copy(text) / paste() → Clipboard API',