@void/react 0.27.0 → 1.0.3

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.
@@ -1,468 +0,0 @@
1
- import { a as SharedContext, i as RouterContext, n as NavigationContext, r as ParamsContext } from "./context-B0GL7p5m.mjs";
2
- import "./action-4EvQppia.mjs";
3
- import { useActionState, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
- import { VoidActionError, categorizeActionError, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, scheduleFormRetry, ssrProxy, submitAction } from "void/pages-client";
5
- import { jsx, jsxs } from "react/jsx-runtime";
6
- //#region src/runtime/form.tsx
7
- function Form({ form, method = "post", renderError, children, ...props }) {
8
- const [now, setNow] = useState(Date.now);
9
- useEffect(() => scheduleFormRetry(form.error, () => setNow(Date.now())), [form.error]);
10
- const message = formErrorMessage(form);
11
- const retry = formRetryMessage(form.error);
12
- const blocked = formSubmissionBlocked(form, now);
13
- return /* @__PURE__ */ jsxs("form", {
14
- ...props,
15
- action: form[method],
16
- "aria-busy": form.pending,
17
- onSubmit: (event) => {
18
- if (blocked) event.preventDefault();
19
- },
20
- children: [message && /* @__PURE__ */ jsxs("div", {
21
- role: "alert",
22
- "data-void-form-error": true,
23
- children: [form.error && renderError ? renderError(form.error) : message, retry && /* @__PURE__ */ jsx("p", { children: retry })]
24
- }), children]
25
- });
26
- }
27
- //#endregion
28
- //#region src/runtime/link.tsx
29
- function hasPrefetch(prefetch) {
30
- return prefetch !== false;
31
- }
32
- function Link({ href, method = "GET", data, preserveScroll = false, preserveState = false, replace = false, reloadDocument = false, viewTransition, prefetch: prefetchProp = false, cacheFor, onNavigate, children, style, onClick: onClickProp, onMouseEnter: onMouseEnterProp, onMouseLeave: onMouseLeaveProp, onFocus: onFocusProp, onMouseDown: onMouseDownProp, onTouchStart: onTouchStartProp, ...rest }) {
33
- const router = useContext(RouterContext);
34
- const elementRef = useRef(null);
35
- const hoverTimerRef = useRef(null);
36
- const normalizedMethod = useMemo(() => method.toUpperCase(), [method]);
37
- const isGet = normalizedMethod === "GET";
38
- const hrefWithData = useMemo(() => isGet ? mergeDataIntoHref(href, data) : href, [
39
- data,
40
- href,
41
- isGet
42
- ]);
43
- const strategies = useMemo(() => {
44
- if (prefetchProp === false || reloadDocument) return [];
45
- if (prefetchProp === true) return ["hover"];
46
- if (typeof prefetchProp === "string") return [prefetchProp];
47
- return prefetchProp;
48
- }, [prefetchProp, reloadDocument]);
49
- const hoverDelay = router?._hoverDelay ?? 75;
50
- if (hasPrefetch(prefetchProp) && !isGet) throw new Error("Link: prefetch only supports GET links. Remove `prefetch` or use method=\"GET\".");
51
- if (reloadDocument && !isGet) throw new Error("Link: reloadDocument only supports GET links.");
52
- const doPrefetch = useCallback(() => {
53
- if (!router?.prefetch) return;
54
- const current = window.location.pathname + window.location.search;
55
- if (hrefWithData === current) return;
56
- const options = {};
57
- if (cacheFor !== void 0) options.cacheFor = cacheFor;
58
- if (normalizedMethod) options.method = normalizedMethod;
59
- router.prefetch(hrefWithData, options);
60
- }, [
61
- cacheFor,
62
- hrefWithData,
63
- normalizedMethod,
64
- router
65
- ]);
66
- const shouldHandleClick = useCallback((event) => {
67
- if (reloadDocument || !router || event.defaultPrevented || isModifiedEvent(event)) return false;
68
- if (event.currentTarget.hasAttribute("download")) return false;
69
- if (new URL(hrefWithData, window.location.origin).origin !== window.location.origin) return false;
70
- return true;
71
- }, [
72
- hrefWithData,
73
- reloadDocument,
74
- router
75
- ]);
76
- const onClick = useCallback((event) => {
77
- onClickProp?.(event);
78
- if (!shouldHandleClick(event)) return;
79
- let navigatePrevented = false;
80
- onNavigate?.({ preventDefault() {
81
- navigatePrevented = true;
82
- } });
83
- if (navigatePrevented) {
84
- event.preventDefault();
85
- return;
86
- }
87
- event.preventDefault();
88
- router?.visit(hrefWithData, {
89
- method: normalizedMethod,
90
- data: isGet ? void 0 : data,
91
- preserveScroll,
92
- preserveState,
93
- replace,
94
- viewTransition
95
- });
96
- }, [
97
- data,
98
- hrefWithData,
99
- isGet,
100
- normalizedMethod,
101
- onClickProp,
102
- onNavigate,
103
- preserveScroll,
104
- preserveState,
105
- replace,
106
- router,
107
- shouldHandleClick,
108
- viewTransition
109
- ]);
110
- const onMouseEnter = useCallback((event) => {
111
- onMouseEnterProp?.(event);
112
- if (event.defaultPrevented) return;
113
- if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
114
- hoverTimerRef.current = setTimeout(doPrefetch, hoverDelay);
115
- }, [
116
- doPrefetch,
117
- hoverDelay,
118
- onMouseEnterProp
119
- ]);
120
- const onMouseLeave = useCallback((event) => {
121
- onMouseLeaveProp?.(event);
122
- if (hoverTimerRef.current) {
123
- clearTimeout(hoverTimerRef.current);
124
- hoverTimerRef.current = null;
125
- }
126
- }, [onMouseLeaveProp]);
127
- const onFocus = useCallback((event) => {
128
- onFocusProp?.(event);
129
- if (!event.defaultPrevented) doPrefetch();
130
- }, [doPrefetch, onFocusProp]);
131
- const onMouseDown = useCallback((event) => {
132
- onMouseDownProp?.(event);
133
- if (!event.defaultPrevented) doPrefetch();
134
- }, [doPrefetch, onMouseDownProp]);
135
- const onTouchStart = useCallback((event) => {
136
- onTouchStartProp?.(event);
137
- if (!event.defaultPrevented) doPrefetch();
138
- }, [doPrefetch, onTouchStartProp]);
139
- useEffect(() => {
140
- if (strategies.length === 0) return;
141
- if (strategies.includes("mount")) doPrefetch();
142
- let observer = null;
143
- if (strategies.includes("visible") && elementRef.current) {
144
- observer = new IntersectionObserver((entries) => {
145
- if (entries.some((e) => e.isIntersecting)) {
146
- doPrefetch();
147
- observer?.disconnect();
148
- observer = null;
149
- }
150
- }, { rootMargin: "0px" });
151
- observer.observe(elementRef.current);
152
- }
153
- return () => {
154
- if (hoverTimerRef.current) {
155
- clearTimeout(hoverTimerRef.current);
156
- hoverTimerRef.current = null;
157
- }
158
- if (observer) observer.disconnect();
159
- };
160
- }, [doPrefetch, strategies]);
161
- const eventHandlers = useMemo(() => {
162
- const handlers = {
163
- onClick,
164
- onFocus: onFocusProp,
165
- onMouseDown: onMouseDownProp,
166
- onMouseEnter: onMouseEnterProp,
167
- onMouseLeave: onMouseLeaveProp,
168
- onTouchStart: onTouchStartProp
169
- };
170
- if (strategies.includes("hover")) {
171
- handlers.onMouseEnter = onMouseEnter;
172
- handlers.onMouseLeave = onMouseLeave;
173
- handlers.onFocus = onFocus;
174
- handlers.onTouchStart = onTouchStart;
175
- }
176
- if (strategies.includes("click")) {
177
- handlers.onMouseDown = onMouseDown;
178
- handlers.onTouchStart = onTouchStart;
179
- }
180
- return handlers;
181
- }, [
182
- onClick,
183
- onFocus,
184
- onFocusProp,
185
- onMouseDown,
186
- onMouseDownProp,
187
- onMouseEnter,
188
- onMouseEnterProp,
189
- onMouseLeave,
190
- onMouseLeaveProp,
191
- onTouchStart,
192
- onTouchStartProp,
193
- strategies
194
- ]);
195
- const resolvedStyle = useMemo(() => ({
196
- touchAction: "manipulation",
197
- ...style
198
- }), [style]);
199
- if (isGet) return /* @__PURE__ */ jsx("a", {
200
- ref: elementRef,
201
- href: hrefWithData,
202
- style: resolvedStyle,
203
- ...rest,
204
- ...eventHandlers,
205
- children
206
- });
207
- return /* @__PURE__ */ jsx("button", {
208
- ref: elementRef,
209
- type: "button",
210
- style: resolvedStyle,
211
- ...rest,
212
- ...eventHandlers,
213
- children
214
- });
215
- }
216
- //#endregion
217
- //#region src/runtime/use-router.ts
218
- function useRouter() {
219
- return useContext(RouterContext) ?? ssrProxy;
220
- }
221
- //#endregion
222
- //#region src/runtime/use-params.ts
223
- function useParams() {
224
- const params = useContext(ParamsContext);
225
- if (params === null) throw new Error("useParams(): must be used inside a Void page.");
226
- return params;
227
- }
228
- //#endregion
229
- //#region src/runtime/use-shared.ts
230
- function useShared() {
231
- const shared = useContext(SharedContext);
232
- if (!shared) throw new Error("useShared(): must be used inside a Void page.");
233
- return shared;
234
- }
235
- //#endregion
236
- //#region src/runtime/use-form.ts
237
- function useForm(url, defaults, options) {
238
- const router = useContext(RouterContext);
239
- const [data, setDataState] = useState(() => ({ ...defaults }));
240
- const [defaultsState, setDefaultsState] = useState(() => ({ ...defaults }));
241
- const [errors, setErrors] = useState({});
242
- const [error, setError] = useState(null);
243
- const [wasSuccessful, setWasSuccessful] = useState(false);
244
- const [recentlySuccessful, setRecentlySuccessful] = useState(false);
245
- const defaultsRef = useRef(defaultsState);
246
- const dataRef = useRef(data);
247
- const successTimeoutRef = useRef(null);
248
- const hasChanges = useMemo(() => {
249
- return Object.keys(defaultsState).some((key) => !isEqualFormValue(data[key], defaultsState[key]));
250
- }, [data, defaultsState]);
251
- const setData = useCallback((field, value) => {
252
- dataRef.current = {
253
- ...dataRef.current,
254
- [field]: value
255
- };
256
- setDataState(dataRef.current);
257
- }, []);
258
- const reset = useCallback((...fields) => {
259
- if (fields.length === 0) dataRef.current = { ...defaultsRef.current };
260
- else {
261
- const next = { ...dataRef.current };
262
- for (const field of fields) next[field] = defaultsRef.current[field];
263
- dataRef.current = next;
264
- }
265
- setDataState(dataRef.current);
266
- }, []);
267
- const clearErrors = useCallback((...fields) => {
268
- if (fields.length === 0) setErrors({});
269
- else setErrors((prev) => {
270
- const next = { ...prev };
271
- for (const field of fields) delete next[field];
272
- return next;
273
- });
274
- }, []);
275
- const clearError = useCallback(() => {
276
- setError(null);
277
- }, []);
278
- const [, dispatchAction, pending] = useActionState(async (previousSuccessVersion, payload) => {
279
- if (!router) throw new Error("useForm(): requires the Void Router.");
280
- const method = payload instanceof FormData ? "POST" : payload.method;
281
- setWasSuccessful(false);
282
- setRecentlySuccessful(false);
283
- if (successTimeoutRef.current) {
284
- clearTimeout(successTimeoutRef.current);
285
- successTimeoutRef.current = null;
286
- }
287
- setErrors({});
288
- setError(null);
289
- let result;
290
- try {
291
- result = await submitAction(router, interpolateRouteParams(url, options?.params), {
292
- method,
293
- data: { ...dataRef.current },
294
- _resolveOnShell: true
295
- });
296
- } catch (submitError) {
297
- if (isAbortError(submitError)) return previousSuccessVersion;
298
- throw submitError;
299
- }
300
- if (!result.ok) {
301
- const validationErrors = getValidationErrors(result.error.body);
302
- if (validationErrors) setErrors(validationErrors);
303
- else setError(result.error);
304
- return previousSuccessVersion;
305
- }
306
- const nextDefaults = { ...dataRef.current };
307
- defaultsRef.current = nextDefaults;
308
- setDefaultsState(nextDefaults);
309
- setWasSuccessful(true);
310
- setRecentlySuccessful(true);
311
- successTimeoutRef.current = setTimeout(() => {
312
- setRecentlySuccessful(false);
313
- successTimeoutRef.current = null;
314
- }, 2e3);
315
- return previousSuccessVersion + 1;
316
- }, 0);
317
- useEffect(() => {
318
- return () => {
319
- if (successTimeoutRef.current) {
320
- clearTimeout(successTimeoutRef.current);
321
- successTimeoutRef.current = null;
322
- }
323
- };
324
- }, []);
325
- const dispatchMethodAction = useCallback((method) => (_formData) => {
326
- dispatchAction({ method });
327
- }, [dispatchAction]);
328
- return {
329
- data,
330
- setData,
331
- errors,
332
- error,
333
- pending,
334
- hasChanges,
335
- wasSuccessful,
336
- recentlySuccessful,
337
- post: dispatchAction,
338
- put: useMemo(() => dispatchMethodAction("PUT"), [dispatchMethodAction]),
339
- patch: useMemo(() => dispatchMethodAction("PATCH"), [dispatchMethodAction]),
340
- delete: useMemo(() => dispatchMethodAction("DELETE"), [dispatchMethodAction]),
341
- reset,
342
- clearErrors,
343
- clearError
344
- };
345
- }
346
- //#endregion
347
- //#region src/runtime/use-navigation.ts
348
- function useNavigation() {
349
- return useContext(NavigationContext);
350
- }
351
- //#endregion
352
- //#region src/runtime/use-island-form.ts
353
- async function readActionErrorBody(response) {
354
- const contentType = response.headers.get("content-type") || "";
355
- try {
356
- if (contentType.includes("application/json")) return await response.json();
357
- return await response.text() || null;
358
- } catch {
359
- return null;
360
- }
361
- }
362
- /**
363
- * Form hook for island pages. Uses fetch + page reload instead of the
364
- * Void Router (which is not available in island mode).
365
- */
366
- function useIslandForm(defaults) {
367
- const [data, setDataState] = useState(() => ({ ...defaults }));
368
- const [defaultsState, setDefaultsState] = useState(() => ({ ...defaults }));
369
- const [errors, setErrors] = useState({});
370
- const [error, setError] = useState(null);
371
- const [pending, setPending] = useState(false);
372
- const [wasSuccessful, setWasSuccessful] = useState(false);
373
- const [recentlySuccessful, setRecentlySuccessful] = useState(false);
374
- const defaultsRef = useRef(defaultsState);
375
- const successTimeoutRef = useRef(null);
376
- const hasChanges = useMemo(() => {
377
- return Object.keys(defaultsState).some((key) => !isEqualFormValue(data[key], defaultsState[key]));
378
- }, [data, defaultsState]);
379
- const setData = useCallback((field, value) => {
380
- setDataState((prev) => ({
381
- ...prev,
382
- [field]: value
383
- }));
384
- }, []);
385
- const reset = useCallback((...fields) => {
386
- if (fields.length === 0) setDataState({ ...defaultsRef.current });
387
- else setDataState((prev) => {
388
- const next = { ...prev };
389
- for (const field of fields) next[field] = defaultsRef.current[field];
390
- return next;
391
- });
392
- }, []);
393
- const clearErrors = useCallback((...fields) => {
394
- if (fields.length === 0) setErrors({});
395
- else setErrors((prev) => {
396
- const next = { ...prev };
397
- for (const field of fields) delete next[field];
398
- return next;
399
- });
400
- }, []);
401
- const clearError = useCallback(() => {
402
- setError(null);
403
- }, []);
404
- const submit = useCallback(async (url, method) => {
405
- setPending(true);
406
- setWasSuccessful(false);
407
- setRecentlySuccessful(false);
408
- if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current);
409
- setErrors({});
410
- setError(null);
411
- try {
412
- const response = await fetch(url, {
413
- method,
414
- headers: { "Content-Type": "application/json" },
415
- body: JSON.stringify(data)
416
- });
417
- if (response.redirected) {
418
- window.location.href = response.url;
419
- return;
420
- }
421
- if (response.ok) {
422
- setWasSuccessful(true);
423
- setRecentlySuccessful(true);
424
- const nextDefaults = { ...data };
425
- defaultsRef.current = nextDefaults;
426
- setDefaultsState(nextDefaults);
427
- successTimeoutRef.current = setTimeout(() => setRecentlySuccessful(false), 2e3);
428
- window.location.reload();
429
- } else {
430
- const body = await readActionErrorBody(response);
431
- const actionError = new VoidActionError({
432
- body,
433
- status: response.status,
434
- statusText: response.statusText,
435
- url
436
- });
437
- const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
438
- if (validationErrors) {
439
- setErrors(validationErrors);
440
- return;
441
- }
442
- if (categorizeActionError(response.status) === "boundary") throw actionError;
443
- setError(actionError);
444
- }
445
- } finally {
446
- setPending(false);
447
- }
448
- }, [data]);
449
- return {
450
- data,
451
- setData,
452
- errors,
453
- error,
454
- pending,
455
- hasChanges,
456
- wasSuccessful,
457
- recentlySuccessful,
458
- reset,
459
- clearErrors,
460
- clearError,
461
- post: (url) => submit(url, "POST"),
462
- put: (url) => submit(url, "PUT"),
463
- patch: (url) => submit(url, "PATCH"),
464
- delete: (url) => submit(url, "DELETE")
465
- };
466
- }
467
- //#endregion
468
- export { useParams as a, Form as c, useShared as i, useNavigation as n, useRouter as o, useForm as r, Link as s, useIslandForm as t };