/** * Progressive-enhancement form actions. * * Binds a `
` to a server action (an endpoint URL or a function). The form * posts natively when JavaScript is unavailable, and is progressively enhanced * to a `fetch`-based submit with reactive pending/error/result state — and * optional optimistic updates — when JS is present. Composes with the existing * validation pipeline and with the `server` module's `csrf()` middleware. * * @module bquery/forms */ import type { ReadonlySignalHandle, Signal } from '../reactive/index'; import type { OptimisticHandle } from './optimistic'; /** Context passed to a function-style action target and available to hooks. */ export interface FormActionContext { /** The `` element that triggered the submit, when invoked via `enhance()`. */ form?: HTMLFormElement; } /** * A form-action target: an endpoint URL (native-fallback capable) or a function * that receives the submitted `FormData` and returns the result. */ export type FormActionTarget = string | ((formData: FormData, context: FormActionContext) => Promise | R); /** * Error thrown by {@link formAction} when a string target responds with a * non-OK HTTP status. Carries the `status` and the raw {@link Response} so * `onError` handlers can branch on them. */ export declare class FormActionError extends Error { readonly status: number; readonly response: Response; constructor(response: Response); } /** Options for {@link formAction}. */ export interface FormActionOptions { /** * HTTP method for the enhanced fetch submit. Default `'POST'`. Native form * fallback only supports `GET`/`POST`, so non-GET methods degrade to a native * `POST` (use a server-side method-override convention if you need the verb). */ method?: 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'GET'; /** Custom `fetch` implementation (defaults to the global `fetch`). */ fetch?: typeof fetch; /** Extra headers merged into the enhanced fetch submit. */ headers?: Record; /** * CSRF token to send with the enhanced submit (as a header) and to inject as a * hidden field on `enhance()` so the native no-JS POST carries it too. Pairs * with the `server` module's `csrf()` middleware. */ csrf?: string | (() => string | null | undefined); /** Header carrying the CSRF token. Default `'x-csrf-token'`. */ csrfHeader?: string; /** Hidden-field name carrying the CSRF token for native fallback. Default `'_csrf'`. */ csrfField?: string; /** * Parse a `Response` into the action result. Defaults to JSON when the * response's content-type is JSON, otherwise text. */ parseResponse?: (response: Response) => Promise | R; /** * Apply an optimistic overlay for the duration of the submit. Return the * handle from an {@link OptimisticController.add} call (or nothing); it is * removed automatically when the submit settles. * * @example * ```ts * const list = optimistic(todos, (cur, draft: string) => [...cur, draft]); * formAction('/todos', { * optimistic: (fd) => list.add(String(fd.get('title') ?? '')), * }); * ``` */ optimistic?: (formData: FormData) => OptimisticHandle | void; /** Called after a successful submit. */ onSuccess?: (result: R, formData: FormData) => void | Promise; /** Called when the submit throws, rejects, or returns a non-OK response. */ onError?: (error: unknown, formData: FormData) => void | Promise; /** Reset the source `` after a successful enhanced submit. Default `false`. */ resetOnSuccess?: boolean; } /** Options for {@link FormAction.enhance}. */ export interface EnhanceOptions { /** Override `resetOnSuccess` for this binding. */ resetOnSuccess?: boolean; } /** A reactive form-action handle returned by {@link formAction}. */ export interface FormAction { /** `true` while a submit is in flight. */ pending: Signal; /** The error from the most recent failed submit, or `null`. */ error: Signal; /** The result of the most recent successful submit, or `undefined`. */ result: Signal; /** Number of submits started. */ submitCount: Signal; /** Timestamp (ms since epoch) of the most recent successful submit, or `null`. */ submittedAt: Signal; /** * Progressively enhance a ``: ensure the native `action`/`method` (and * optional hidden CSRF field) are present so the form POSTs without JS, then * intercept `submit` to run a fetch-based, optimistic-aware submit when JS is * available. Returns a cleanup function that detaches the listener and removes * any injected hidden field. */ enhance: (form: HTMLFormElement, options?: EnhanceOptions) => () => void; /** Programmatically run the action with explicit `FormData`. */ submit: (formData?: FormData, context?: FormActionContext) => Promise; /** Clear `error` / `result` and reset `pending`. */ reset: () => void; } /** * Bind a form to a server action with progressive enhancement and reactive * submission state. * * @example * ```ts * import { formAction, useFormStatus } from '@bquery/bquery/forms'; * * // Binds to a server action; falls back to a native POST without JS. * const submit = formAction('/todos', { method: 'POST' }); * const { pending } = useFormStatus(submit); * * const cleanup = submit.enhance(document.querySelector('form')!); * ``` */ export declare const formAction: (target: FormActionTarget, options?: FormActionOptions) => FormAction; /** Read-only reactive view of a {@link FormAction}'s submission state. */ export interface FormStatus { /** `true` while a submit is in flight. */ pending: ReadonlySignalHandle; /** The error from the most recent failed submit, or `null`. */ error: ReadonlySignalHandle; /** The result of the most recent successful submit, or `undefined`. */ result: ReadonlySignalHandle; /** Number of submits started. */ submitCount: ReadonlySignalHandle; /** Timestamp (ms since epoch) of the most recent successful submit, or `null`. */ submittedAt: ReadonlySignalHandle; } /** * Read-only reactive status for a {@link formAction}, mirroring React 19's * `useFormStatus`. Returns `readonly()` wrappers so consumers can observe but * not mutate the action's signals. * * @example * ```ts * const submit = formAction('/todos'); * const { pending, error } = useFormStatus(submit); * effect(() => { saveButton.disabled = pending.value; }); * ``` */ export declare const useFormStatus: (action: FormAction) => FormStatus; //# sourceMappingURL=action.d.ts.map