import type { PendingVisit, UrlMethodPair, UseFormSubmitOptions, } from '@inertiajs/core'; import { router } from '@inertiajs/vue3'; import type { InertiaForm } from '@inertiajs/vue3'; import { computed, onMounted, onUnmounted, ref } from 'vue'; import { useUnsavedChanges } from '@/composables/useUnsavedChanges'; import { formRequestHeader, isFormNetworkFailure, matchesFormRequest, } from '@/lib/form-feedback'; import type { FormFeedbackState } from '@/lib/form-feedback'; /** Pair with a keyed useForm/useRemember for restoration on browser Back/Forward. */ export function useFormProtection( form: InertiaForm, options: { isDirty?: () => boolean } = {}, ) { const failure = ref(null); const responseStatus = ref(null); let ownVisit: PendingVisit | null = null; let requestId: string | null = null; const removeListeners: Array<() => void> = []; const { confirmLeave } = useUnsavedChanges( () => form.processing || (options.isDirty?.() ?? form.isDirty), (visit) => visit === ownVisit, ); const state = computed(() => { if (form.processing) { return 'loading'; } if (failure.value) { return failure.value; } if (Object.keys(form.errors).length > 0) { return 'validation'; } return form.recentlySuccessful ? 'success' : 'idle'; }); onMounted(() => { removeListeners.push( router.on('exception', (event) => { if (isFormNetworkFailure(event.detail.exception, requestId)) { event.preventDefault(); failure.value = 'network'; } }), router.on('invalid', (event) => { if ( matchesFormRequest(event.detail.response.config, requestId) ) { event.preventDefault(); failure.value = 'server'; responseStatus.value = event.detail.response.status; } }), ); }); onUnmounted(() => removeListeners.forEach((remove) => remove())); function submit( target: UrlMethodPair, visitOptions: UseFormSubmitOptions = {}, ): void { if (form.processing) { return; } const nextRequestId = crypto.randomUUID(); form.submit(target, { ...visitOptions, preserveState: true, headers: { ...visitOptions.headers, [formRequestHeader]: nextRequestId, }, onBefore: (visit) => { if (visitOptions.onBefore?.(visit) === false) { return false; } ownVisit = visit; }, onStart: (visit) => { requestId = nextRequestId; failure.value = null; responseStatus.value = null; visitOptions.onStart?.(visit); }, onError: (errors) => { failure.value = 'validation'; return visitOptions.onError?.(errors); }, onSuccess: (page) => { failure.value = null; return visitOptions.onSuccess?.(page); }, onCancel: () => { failure.value = 'cancelled'; visitOptions.onCancel?.(); }, onFinish: (visit) => { requestId = null; ownVisit = null; visitOptions.onFinish?.(visit); }, }); } function clearFeedback(): void { failure.value = null; responseStatus.value = null; } return { state, responseStatus, submit, confirmLeave, clearFeedback }; }