Snapshot form state, check isDirty,
list changedFields,
and guard beforeunload
— in 3 lines. No form library required.
Single boolean property. Always up to date. No manual comparisons, no deep-equal headaches.
Array of { name, original, current } for every field that differs. Show users exactly what changed.
One option: beforeUnload: true. Prevents accidental navigation when the form is dirty. That's it.
Re-baseline current state with one call. After your API save succeeds, the form is "clean" again.
No React, no Lodash, no form library. Pure TypeScript class you drop into any project.
Works with native HTML forms and controlled state (React, Vue, Svelte). Same API, same result.
This form is wired up with form-dirty. Change any field to see it in action.
npm install form-dirty
yarn add form-dirty
pnpm add form-dirty
<script type="module"> import FormDirty from 'https://esm.sh/form-dirty'; </script>
import FormDirty from 'form-dirty'; const fd = new FormDirty({ form: '#my-form', beforeUnload: true, }); // Check anytime console.log(fd.isDirty); // true / false console.log(fd.changedFields); // [{ name, original, current }]
import FormDirty from 'form-dirty'; const fd = new FormDirty({ fields: { name: '', email: '', bio: '' }, beforeUnload: true, onDirtyChange: (dirty) => setBanner(dirty), }); // Whenever your state changes fd.update({ name: 'Ada', email: '', bio: '' }); console.log(fd.isDirty); // true
import { useEffect, useRef } from 'react'; import FormDirty from 'form-dirty'; function useFormDirty(fields) { const ref = useRef(null); useEffect(() => { ref.current = new FormDirty({ fields, beforeUnload: true, }); return () => ref.current?.destroy(); }, []); useEffect(() => { ref.current?.update(fields); }, [fields]); return { get isDirty() { return ref.current?.isDirty ?? false; }, get changedFields() { return ref.current?.changedFields ?? []; }, snapshot: () => ref.current?.snapshot(), }; }
const fd = new FormDirty({ form: '#settings-form', beforeUnload: true, }); async function handleSave() { await fetch('/api/settings', { method: 'POST', body: getFormData(), }); fd.snapshot(); // current state = new baseline console.log(fd.isDirty); // false }
| Option | Type | Default | Description |
|---|---|---|---|
| form | HTMLFormElement | string | — | DOM form element or CSS selector. Enables DOM mode. |
| fields | Record<string, unknown> | — | Initial field values. Enables controlled mode. |
| beforeUnload | boolean | false | Auto-attach beforeunload guard when form is dirty. |
| onDirtyChange | (dirty: boolean) => void | — | Callback fired when dirty state transitions. |
| Member | Type | Description |
|---|---|---|
| .isDirty | boolean | Whether any field differs from baseline. |
| .changedFields | ChangedField[] | Array of { name, original, current } for changed fields. |
| .snapshot() | void | Capture current state as the new clean baseline. |
| .update(fields) | void | Push new field values (controlled mode). |
| .guard(enable?) | void | Toggle the beforeunload guard on or off. |
| .destroy() | void | Remove all listeners. Call on unmount / cleanup. |