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
30 changes: 15 additions & 15 deletions docs/guide/pages-routing/actions-and-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,21 +149,21 @@ The types are inferred from your action's `withValidator()` schema in the compan

`useForm` returns a reactive object with:

| Property / Method | Purpose |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data` / `setData` | Current form values, typed to match the action's body schema. |
| `errors` | Field-level validation errors, keys typed to body field names. |
| `error` | Action error, or `null`. |
| `post`, `put`, `patch`, `delete` | Submit the form with that method. In React these are native form action callbacks. |
| `pending` | `true` while the submission is in flight. |
| `hasChanges` | `true` if form data differs from the current defaults. After a successful submit, defaults update to the submitted values so `hasChanges` becomes `false`. |
| `wasSuccessful` | `true` after a successful submission. Stays `true` until the next submission. |
| `recentlySuccessful` | `true` for 2 seconds after a successful submission. Useful for flash messages. |
| `reset(...fields)` | Reset form data to the current defaults (last submitted values after success). Field names autocomplete. Pass `{ initial: true }` first to reset to the values passed to `useForm`. |
| `resetOnSuccess` (option) | When `true`, a successful submit resets both form data and defaults to the mount-time values. Useful for create forms that should clear after save. |
| `setError(field, message)` | Set a client-side validation error on a field. |
| `clearErrors(...fields)` | Clear validation errors. Field names autocomplete. |
| `clearError()` | Clear `error`. |
| Property / Method | Purpose |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data` / `setData` | Current form values, typed to match the action's body schema. |
| `errors` | Field-level validation errors, keys typed to body field names. |
| `error` | Action error, or `null`. |
| `post`, `put`, `patch`, `delete` | Submit the form with that method. In React these are native form action callbacks. |
| `pending` | `true` while the submission is in flight. |
| `hasChanges` | `true` if form data differs from the current defaults. After a successful submit, defaults update to the submitted values, so `hasChanges` becomes `false` unless the form was edited during the submit. |
| `wasSuccessful` | `true` after a successful submission. Stays `true` until the next submission. |
| `recentlySuccessful` | `true` for 2 seconds after a successful submission. Useful for flash messages. |
| `reset(...fields)` | Reset form data to the current defaults (last submitted values after success). Field names autocomplete. Pass `{ initial: true }` first to reset to the values passed to `useForm`. |
| `resetOnSuccess` (option) | When `true`, a successful submit resets defaults to the mount-time values, and form data too unless it was edited during the submit. Useful for create forms that should clear after save. |
| `setError(field, message)` | Set a client-side validation error on a field. |
| `clearErrors(...fields)` | Clear validation errors. Field names autocomplete. |
| `clearError()` | Clear `error`. |

Use `setError()` for client-side validation. Editing a field clears its error automatically.

Expand Down
20 changes: 16 additions & 4 deletions packages/solid/src/runtime/use-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function useForm(

const [defaults_, setDefaults] = createSignal<Record<string, unknown>>(initialDefaults);
let successTimeout: ReturnType<typeof setTimeout> | null = null;
let latestRequestId = 0;

const hasChanges = createMemo(() => {
const current = defaults_();
Expand Down Expand Up @@ -123,6 +124,9 @@ export function useForm(
}

async function submit(method: string, opts: FormSubmitOptions = {}) {
const requestId = ++latestRequestId;
// Edits made while this request is in flight stay unsaved.
const submittedData = cloneFormValue(data);
setPending(true);
setWasSuccessful(false);
setRecentlySuccessful(false);
Expand All @@ -141,9 +145,13 @@ export function useForm(
// request path (such as "..") fails this submit, not the page render.
const result = await submitAction(router, interpolateRouteParams(url, options?.params), {
method,
data: { ...data },
data: submittedData,
...opts,
});
// A newer submit owns the form's state, even if this one still completed.
if (requestId !== latestRequestId) {
return;
}

if (!result.ok) {
const validationErrors = getValidationErrors(result.error.body);
Expand All @@ -158,10 +166,12 @@ export function useForm(
setWasSuccessful(true);
setRecentlySuccessful(true);
if (options?.resetOnSuccess) {
setDataStore(reconcile(cloneFormValue(initialDefaults)));
if (isEqualFormValue(data, submittedData)) {
setDataStore(reconcile(cloneFormValue(initialDefaults)));
}
setDefaults(cloneFormValue(initialDefaults));
} else {
setDefaults(cloneFormValue(data));
setDefaults(cloneFormValue(submittedData));
}
successTimeout = setTimeout(() => {
setRecentlySuccessful(false);
Expand All @@ -172,7 +182,9 @@ export function useForm(
}
throw submitError;
} finally {
setPending(false);
if (requestId === latestRequestId) {
setPending(false);
}
}
}

Expand Down
17 changes: 14 additions & 3 deletions packages/solid/src/runtime/use-island-form.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSignal, createMemo } from 'solid-js';
import { createStore, reconcile } from 'solid-js/store';
import {
cloneFormValue,
isEqualFormValue,
submitIslandAction,
type IslandFormState,
Expand All @@ -25,6 +26,7 @@ export function useIslandForm<T extends Record<string, unknown>>(defaults: T): I

let defaults_ = { ...defaults };
let successTimeout: ReturnType<typeof setTimeout> | null = null;
let latestRequestId = 0;

const hasChanges = createMemo(() => {
return Object.keys(defaults_).some((key) => !isEqualFormValue(data[key], defaults_[key]));
Expand Down Expand Up @@ -63,6 +65,9 @@ export function useIslandForm<T extends Record<string, unknown>>(defaults: T): I
}

async function submit(url: string, method: string) {
const requestId = ++latestRequestId;
// Edits made while this request is in flight stay unsaved.
const submittedData = cloneFormValue(data);
setPending(true);
setWasSuccessful(false);
setRecentlySuccessful(false);
Expand All @@ -73,7 +78,11 @@ export function useIslandForm<T extends Record<string, unknown>>(defaults: T): I
clearError();

try {
const result = await submitIslandAction(url, method, data);
const result = await submitIslandAction(url, method, submittedData);
// A newer submit owns the form's state and decides whether the page changes.
if (requestId !== latestRequestId) {
return;
}

if (result.kind === 'redirect') {
window.location.href = result.url;
Expand All @@ -83,7 +92,7 @@ export function useIslandForm<T extends Record<string, unknown>>(defaults: T): I
if (result.kind === 'success') {
setWasSuccessful(true);
setRecentlySuccessful(true);
defaults_ = { ...data } as typeof defaults_;
defaults_ = submittedData as typeof defaults_;
successTimeout = setTimeout(() => setRecentlySuccessful(false), 2000);
window.location.reload();
} else if (result.kind === 'validation') {
Expand All @@ -92,7 +101,9 @@ export function useIslandForm<T extends Record<string, unknown>>(defaults: T): I
setError(result.error);
}
} finally {
setPending(false);
if (requestId === latestRequestId) {
setPending(false);
}
}
}

Expand Down
Loading
Loading