/** * A utility hook that is a drop in replacement for the useForm hook from * Inertia.js, for use with general API endpoints. Any page updates or * navigation needs to be handled manually via onSuccess callback. * * Based on a combination of * - https://gist.github.com/mohitmamoria/91da6f30d9610b211248225c9e52bebe * - https://gist.github.com/mattiasghodsian/9b4ee07e805547aa13795dc3a28a206d * * Changes: * - Rewritten to use React from Vue * - Added TypeScript types */ import { useForm } from "@inertiajs/react"; import type { InertiaFormProps } from "@inertiajs/react"; import cloneDeep from "lodash.clonedeep"; import axios, { AxiosRequestConfig, AxiosProgressEvent } from "axios"; import { FormDataConvertible, FormDataKeys, Method, VisitOptions, } from "@inertiajs/core"; import type { Response } from "@inertiajs/core/types/response"; type HttpMethod = "get" | "post" | "put" | "patch" | "delete"; type FormDataType = Record; type FormOptions = Omit; interface SubmitOptions { headers?: Record; onBefore?: () => void; onStart?: () => void; onProgress?: (event: AxiosProgressEvent) => void; onSuccess?: (response: TResponse) => void; onError?: (error: unknown) => void; onFinish?: () => void; } type APIForm = InertiaFormProps & { transform: (callback: (data: TForm) => object) => void; submit: ( ...args: | [Method, string, FormOptions?] | [ { url: string; method: Method; }, FormOptions?, ] ) => void; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function hasFiles(data: any): boolean { return ( data instanceof File || data instanceof Blob || (data instanceof FileList && data.length > 0) || (data instanceof FormData && Array.from(data.values()).some((value) => hasFiles(value))) || (typeof data === "object" && data !== null && Object.values(data).some((value) => hasFiles(value))) ); } export function useAPIForm( rememberKeyOrInitialValues: string | TForm, initialValues?: TForm, ): APIForm { const form = initialValues === undefined ? useForm(rememberKeyOrInitialValues as TForm) : useForm( rememberKeyOrInitialValues as string, initialValues, ); let transformFn = (data: TForm): object => data; let recentlySuccessfulTimeoutId: ReturnType | null = null; const submit = ( method: HttpMethod, url: string, options: SubmitOptions = {}, ) => { form.wasSuccessful = false; form.recentlySuccessful = false; form.clearErrors(); if (recentlySuccessfulTimeoutId) { clearTimeout(recentlySuccessfulTimeoutId); } options.onBefore?.(); form.processing = true; options.onStart?.(); const rawData = transformFn(form.data); const config: AxiosRequestConfig = { method, url, headers: { ...options.headers, "Content-Type": hasFiles(rawData) ? "multipart/form-data" : "application/json", }, onUploadProgress: (event) => { form.progress = event; options.onProgress?.(event); }, }; if (method === "get") { config.params = rawData; } else { config.data = rawData; } axios(config) .then((response) => { form.processing = false; form.progress = null; form.clearErrors(); form.wasSuccessful = true; form.recentlySuccessful = true; recentlySuccessfulTimeoutId = setTimeout(() => { form.recentlySuccessful = false; }, 2000); options.onSuccess?.(response.data); form.setDefaults(cloneDeep(form.data)); form.isDirty = false; }) .catch((error) => { form.processing = false; form.progress = null; form.clearErrors(); if ( axios.isAxiosError(error) && error.response?.status === 422 && error.response.data.errors ) { const errors = error.response.data.errors; (Object.keys(errors) as Array>).forEach( (key) => { form.setError(key, errors[key][0]); }, ); } options.onError?.(error); }) .finally(() => { form.processing = false; form.progress = null; options.onFinish?.(); }); }; const overriders = { transform: (receiver: (data: TForm) => object) => (callback: (data: TForm) => object) => { transformFn = callback; return receiver; }, submit: () => submit, post: () => submit.bind(null, "post"), get: () => submit.bind(null, "get"), put: () => submit.bind(null, "put"), patch: () => submit.bind(null, "patch"), delete: () => submit.bind(null, "delete"), }; return new Proxy(form, { get: (target, prop, receiver) => prop in overriders ? overriders[prop as keyof typeof overriders](receiver) : (target as APIForm)[prop as keyof APIForm], }) as APIForm; }