|
import { useApi } from '@/composables/useApi'; |
|
import { FormDataConvertible } from '@inertiajs/core/types'; |
|
import { InertiaFormProps, useForm } from '@inertiajs/vue3'; |
|
import { AxiosProgressEvent } from 'axios'; |
|
import cloneDeep from 'lodash.clonedeep'; |
|
|
|
const api = useApi(); |
|
|
|
interface FormOptions { |
|
onBefore?: () => void; |
|
onStart?: () => void; |
|
onProgress?: (event: AxiosProgressEvent) => void; |
|
onSuccess?: (data: any) => void; |
|
onError?: (error: { |
|
status: number; |
|
}) => void; |
|
onFinish?: () => void; |
|
} |
|
|
|
export type FormDataType = Record<string, FormDataConvertible>; |
|
export type InertiaForm<TForm extends FormDataType> = TForm & InertiaFormProps<TForm>; |
|
|
|
export function useApiForm<TForm extends FormDataType>(initialData?: TForm | (() => TForm)): InertiaForm<TForm> { |
|
const form = useForm(initialData || {}); |
|
|
|
let transform = (data: TForm) => data; |
|
let recentlySuccessfulTimeoutId: number | null = null; |
|
|
|
const overriders = { |
|
transform: (receiver: any) => (callback: (data: TForm) => any) => { |
|
transform = callback; |
|
return receiver; |
|
}, |
|
submit: |
|
() => |
|
(method: string, url: string, options: FormOptions = {}) => { |
|
form.wasSuccessful = false; |
|
form.recentlySuccessful = false; |
|
form.clearErrors(); |
|
if (recentlySuccessfulTimeoutId) clearTimeout(recentlySuccessfulTimeoutId); |
|
if (options.onBefore) { |
|
options.onBefore(); |
|
} |
|
|
|
form.processing = true; |
|
if (options.onStart) { |
|
options.onStart(); |
|
} |
|
|
|
const data = transform(form.data() as any); |
|
// @ts-expect-error Error in accessing method using square brackets |
|
api[method](url, data, { |
|
headers: { |
|
'Content-Type': hasFiles(data) ? 'multipart/form-data' : 'application/json', |
|
}, |
|
onUploadProgress: (event: any) => { |
|
form.progress = event; |
|
if (options.onProgress) { |
|
options.onProgress(event); |
|
} |
|
}, |
|
}) |
|
.then((response: any) => { |
|
form.processing = false; |
|
form.progress = null; |
|
form.clearErrors(); |
|
form.wasSuccessful = true; |
|
form.recentlySuccessful = true; |
|
recentlySuccessfulTimeoutId = setTimeout(() => (form.recentlySuccessful = false), 2000); |
|
|
|
if (options.onSuccess) { |
|
options.onSuccess(response.data); |
|
} |
|
|
|
form.defaults(cloneDeep(form.data())); |
|
form.isDirty = false; |
|
}) |
|
.catch((error: any) => { |
|
form.processing = false; |
|
form.progress = null; |
|
|
|
form.clearErrors(); |
|
if (error.response?.status === 422) { |
|
Object.keys(error.response.data.errors).forEach((key) => { |
|
// @ts-expect-error types mismatch |
|
form.setError(key, error.response.data.errors[key][0]); |
|
}); |
|
} |
|
|
|
if (options.onError) { |
|
options.onError(error); |
|
} |
|
}) |
|
.finally(() => { |
|
form.processing = false; |
|
form.progress = null; |
|
|
|
if (options.onFinish) { |
|
options.onFinish(); |
|
} |
|
}); |
|
}, |
|
}; |
|
|
|
return new Proxy(form, { |
|
get: (target, prop, receiver) => { |
|
if (Object.keys(overriders).indexOf(prop.toString()) < 0) { |
|
// @ts-expect-error Using prop |
|
return target[prop]; |
|
} |
|
|
|
return overriders[prop as keyof typeof overriders](receiver); |
|
}, |
|
}) as any; |
|
} |
|
|
|
function hasFiles(data: any): boolean { |
|
return data instanceof FormData; |
|
} |
Sure - no worries. I forgot to include the simple wrapper around the Axios instance that I'm using to hit the API endpoint. I've now added that to the gist, and also updated the useApiForm just in case there was anything else that changed.
The issue with @inertiajs/core/types may be a red-herring - check if it is just an issue with your editor, rather than the actual TS. In other words, check that it's not just a red underline in your editor. If it compiles when you run npm run dev or npm run build, then the problem may lay in your editor not being able to resolve the imports - Typescript can be a pain in the neck for this. If you already have inertia installed, then the type def for the FormDataConvertible should be resolvable - assuming that the @ alias in configured in your vite.config.ts (which it should be by default if you're using Laravel with Vue starter kit.
Here is a full but simple component that uses a HTTP GET API endpoint to update an 'online' status. In this example, there is no generics on the form itself, as the API endpoint doesn't take any form data. Notice that we can use the 'apiForm.processing' prop to indicate that the API call is actively underway. You also get type-hinting for the form props - for example, with setting the 'onSuccess' callback prop.