useFormDirty
Tracks unsaved changes and stops the reader losing them — through the app's own dialog on a route change, and the browser's on a tab close.
Import
ts
import { useFormDirty } from '@core/schema/useFormDirty'Signature
ts
function useFormDirty<T>(
initial: T,
current: Ref<T>,
options?: UseFormDirtyOptions,
): UseFormDirtyReturn
interface UseFormDirtyOptions {
confirmMessage?: string
confirmDescription?: string
}Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
initial | T | required | The baseline. Snapshotted once, at the call. |
current | Ref<T> | required | The live values. |
options.confirmMessage | string | 'common.unsavedChangesLeave' | The dialog's title. A translation key or a literal. |
options.confirmDescription | string | 'common.unsavedChangesLost' | The line under it. |
Returns
| Member | Type | Description |
|---|---|---|
isDirty | Readonly<Ref<boolean>> | Whether current differs from the baseline, by deep comparison. |
markPristine | () => void | Re-snapshots from current. Call it after a successful save. |
Example
vue
<script setup lang="ts">
import { useFormDirty } from '@core/schema/useFormDirty'
import { ref } from 'vue'
interface Profile {
name: string
title: string
}
const initial: Profile = { name: 'Amina Haddad', title: 'Operations lead' }
const form = ref<Profile>({ ...initial })
const { isDirty, markPristine } = useFormDirty(initial, form)
async function save() {
await fetch('/api/profile', { method: 'PATCH', body: JSON.stringify(form.value) })
markPristine()
}
</script>
<template>
<form class="space-y-3" @submit.prevent="save()">
<input v-model="form.name" type="text">
<input v-model="form.title" type="text">
<button type="submit" :disabled="!isDirty">
{{ isDirty ? 'Save changes' : 'Saved' }}
</button>
</form>
</template>Notes
- The route guard is async, and that is the whole point. vue-router awaits a promise returned from a leave hook, so the question can be asked through the app's own confirm dialog instead of the browser's gray box. It uses the
defaultvariant, because the title is already a question. - The
beforeunloaddialog cannot be styled. Closing a tab happens outside the app's lifetime, so the browser draws its own and ignores your text. A non-emptyreturnValueis still what triggers it. - The baseline is a structural clone. Without it, mutating the object you passed as
initialwould shift the baseline andisDirtywould read false no matter how much was edited. - The snapshot is taken once. After a save, either remount the form or call
markPristine(), or the guard keeps firing. - Comparison is deep, so key order and nesting do not produce false positives.
Related
useFormDraft— keeping the work rather than warning about it.useConfirm— the dialog this asks through.- Forms — the form engine.