Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 8 additions & 43 deletions packages/react/src/runtime/use-island-form.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
import { useState, useRef, useCallback, useMemo } from 'react';
import {
VoidActionError,
categorizeActionError,
getValidationErrors,
isEqualFormValue,
} from 'void/pages-client';

async function readActionErrorBody(response: Response): Promise<unknown> {
const contentType = response.headers.get('content-type') || '';
try {
if (contentType.includes('application/json')) {
return await response.json();
}
const text = await response.text();
return text || null;
} catch {
return null;
}
}
import { isEqualFormValue, submitIslandAction, type VoidActionError } from 'void/pages-client';

export type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
data: T;
Expand Down Expand Up @@ -106,42 +88,25 @@ export function useIslandForm<T extends Record<string, unknown>>(defaults: T): I
setError(null);

try {
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const result = await submitIslandAction(url, method, data);

if (response.redirected) {
window.location.href = response.url;
if (result.kind === 'redirect') {
window.location.href = result.url;
return;
}

if (response.ok) {
if (result.kind === 'success') {
setWasSuccessful(true);
setRecentlySuccessful(true);
const nextDefaults = { ...data };
defaultsRef.current = nextDefaults;
setDefaultsState(nextDefaults);
successTimeoutRef.current = setTimeout(() => setRecentlySuccessful(false), 2000);
window.location.reload();
} else if (result.kind === 'validation') {
setErrors(result.errors);
} else {
const body = await readActionErrorBody(response);
const actionError = new VoidActionError({
body,
status: response.status,
statusText: response.statusText,
url,
});
const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
if (validationErrors) {
setErrors(validationErrors);
return;
}
if (categorizeActionError(response.status) === 'boundary') {
throw actionError;
}
setError(actionError);
setError(result.error);
}
} finally {
setPending(false);
Expand Down
51 changes: 8 additions & 43 deletions packages/solid/src/runtime/use-island-form.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { createSignal, createMemo } from 'solid-js';
import { createStore, reconcile } from 'solid-js/store';
import {
VoidActionError,
categorizeActionError,
getValidationErrors,
isEqualFormValue,
} from 'void/pages-client';

async function readActionErrorBody(response: Response): Promise<unknown> {
const contentType = response.headers.get('content-type') || '';
try {
if (contentType.includes('application/json')) {
return await response.json();
}
const text = await response.text();
return text || null;
} catch {
return null;
}
}
import { isEqualFormValue, submitIslandAction, type VoidActionError } from 'void/pages-client';

export type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
data: T;
Expand Down Expand Up @@ -100,40 +82,23 @@ export function useIslandForm<T extends Record<string, unknown>>(defaults: T): I
clearError();

try {
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const result = await submitIslandAction(url, method, data);

if (response.redirected) {
window.location.href = response.url;
if (result.kind === 'redirect') {
window.location.href = result.url;
return;
}

if (response.ok) {
if (result.kind === 'success') {
setWasSuccessful(true);
setRecentlySuccessful(true);
defaults_ = { ...data } as typeof defaults_;
successTimeout = setTimeout(() => setRecentlySuccessful(false), 2000);
window.location.reload();
} else if (result.kind === 'validation') {
setErrors(result.errors);
} else {
const body = await readActionErrorBody(response);
const actionError = new VoidActionError({
body,
status: response.status,
statusText: response.statusText,
url,
});
const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
if (validationErrors) {
setErrors(validationErrors);
return;
}
if (categorizeActionError(response.status) === 'boundary') {
throw actionError;
}
setError(actionError);
setError(result.error);
}
} finally {
setPending(false);
Expand Down
51 changes: 8 additions & 43 deletions packages/svelte/src/runtime/use-island-form.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
VoidActionError,
categorizeActionError,
getValidationErrors,
isEqualFormValue,
} from 'void/pages-client';
import { isEqualFormValue, submitIslandAction, type VoidActionError } from 'void/pages-client';

type UntypedIslandForm = {
readonly data: Record<string, unknown>;
Expand All @@ -23,19 +18,6 @@ type UntypedIslandForm = {
clearError(): void;
};

async function readActionErrorBody(response: Response): Promise<unknown> {
const contentType = response.headers.get('content-type') || '';
try {
if (contentType.includes('application/json')) {
return await response.json();
}
const text = await response.text();
return text || null;
} catch {
return null;
}
}

/**
* Form composable for island pages. Uses fetch + page reload instead of the
* Void Router (which is not available in island mode).
Expand Down Expand Up @@ -90,42 +72,25 @@ export function useIslandForm(defaults: Record<string, unknown>): UntypedIslandF
clearError();

try {
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const result = await submitIslandAction(url, method, data);

if (response.redirected) {
window.location.href = response.url;
if (result.kind === 'redirect') {
window.location.href = result.url;
return;
}

if (response.ok) {
if (result.kind === 'success') {
wasSuccessful = true;
recentlySuccessful = true;
defaults_ = { ...data };
successTimeout = setTimeout(() => {
recentlySuccessful = false;
}, 2000);
window.location.reload();
} else if (result.kind === 'validation') {
Object.assign(errors, result.errors);
} else {
const body = await readActionErrorBody(response);
const actionError = new VoidActionError({
body,
status: response.status,
statusText: response.statusText,
url,
});
const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
if (validationErrors) {
Object.assign(errors, validationErrors);
return;
}
if (categorizeActionError(response.status) === 'boundary') {
throw actionError;
}
error = actionError;
error = result.error;
}
} finally {
pending = false;
Expand Down
1 change: 1 addition & 0 deletions packages/void/src/pages/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export {
isCallSiteActionError,
} from './action-result.ts';
export type { ActionErrorHandlingScope, ActionResult } from './action-result.ts';
export { submitIslandAction, type IslandActionResult } from './island-action.ts';
export { isEqualFormValue } from './form-value-equality.ts';
export {
cloneFormValue,
Expand Down
41 changes: 41 additions & 0 deletions packages/void/src/pages/island-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
categorizeActionError,
createActionErrorFromResponse,
type VoidActionError,
} from './action-result.ts';
import { getValidationErrors } from './adapter-helpers.ts';

export type IslandActionResult =
| { kind: 'redirect'; url: string }
| { kind: 'success' }
| { kind: 'validation'; errors: Record<string, string> }
| { kind: 'error'; error: VoidActionError };

export async function submitIslandAction(
url: string,
method: string,
data: Record<string, unknown>,
): Promise<IslandActionResult> {
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});

if (response.redirected) {
return { kind: 'redirect', url: response.url };
}
if (response.ok) {
return { kind: 'success' };
}

const error = await createActionErrorFromResponse(response, url);
const errors = response.status === 422 ? getValidationErrors(error.body) : null;
if (errors) {
return { kind: 'validation', errors };
}
if (categorizeActionError(response.status) === 'boundary') {
throw error;
}
return { kind: 'error', error };
}
Loading