115 lines
3.5 KiB
TypeScript
115 lines
3.5 KiB
TypeScript
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<T extends object>(
|
|
form: InertiaForm<T>,
|
|
options: { isDirty?: () => boolean } = {},
|
|
) {
|
|
const failure = ref<FormFeedbackState | null>(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<FormFeedbackState>(() => {
|
|
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';
|
|
}
|
|
}),
|
|
);
|
|
});
|
|
|
|
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;
|
|
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);
|
|
},
|
|
});
|
|
}
|
|
|
|
return { state, submit, confirmLeave };
|
|
}
|