form-dirty 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Everything Frontend
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# 📝 form-dirty
|
|
2
|
+
|
|
3
|
+
> Unsaved changes detection. Snapshot form state, expose `isDirty` + `changedFields`, handle `beforeunload` — in 3 lines. Zero dependencies.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/form-dirty)
|
|
6
|
+
[](https://www.npmjs.com/package/form-dirty)
|
|
7
|
+
[](https://bundlephobia.com/package/form-dirty)
|
|
8
|
+
[](https://github.com/everything-frontend/form-dirty/blob/main/LICENSE)
|
|
9
|
+
[](https://www.npmjs.com/package/form-dirty)
|
|
10
|
+
[](https://www.npmjs.com/package/form-dirty)
|
|
11
|
+
|
|
12
|
+
**[Live Demo →](https://www.everythingfrontend.com/form-dirty)**
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
"You have unsaved changes — are you sure you want to leave?" gets asked on every StackOverflow forum. Detecting form dirtiness across native and controlled forms, handling `beforeunload`, figuring out *which* fields changed — it's consistently painful.
|
|
19
|
+
|
|
20
|
+
Full form libraries like react-hook-form handle it internally, but you have to buy into the entire system. There's no tiny, standalone "is this form dirty?" utility.
|
|
21
|
+
|
|
22
|
+
**form-dirty is that utility.** ~0.9kB gzipped. Zero dependencies. Works with DOM forms and controlled state (React, Vue, Svelte, anything).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install form-dirty
|
|
30
|
+
# or
|
|
31
|
+
yarn add form-dirty
|
|
32
|
+
# or
|
|
33
|
+
pnpm add form-dirty
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
### DOM form
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import FormDirty from 'form-dirty';
|
|
44
|
+
|
|
45
|
+
const fd = new FormDirty({
|
|
46
|
+
form: '#my-form',
|
|
47
|
+
beforeUnload: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Check anytime
|
|
51
|
+
console.log(fd.isDirty); // true / false
|
|
52
|
+
console.log(fd.changedFields); // [{ name: 'email', original: '', current: 'hi@me.com' }]
|
|
53
|
+
|
|
54
|
+
// After saving, re-baseline
|
|
55
|
+
fd.snapshot();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Controlled form (React / Vue / Svelte)
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import FormDirty from 'form-dirty';
|
|
62
|
+
|
|
63
|
+
const fd = new FormDirty({
|
|
64
|
+
fields: { name: '', email: '', bio: '' },
|
|
65
|
+
beforeUnload: true,
|
|
66
|
+
onDirtyChange: (dirty) => setHasUnsavedChanges(dirty),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Whenever state changes
|
|
70
|
+
fd.update({ name: 'Ada', email: '', bio: '' });
|
|
71
|
+
|
|
72
|
+
console.log(fd.isDirty); // true
|
|
73
|
+
console.log(fd.changedFields); // [{ name: 'name', original: '', current: 'Ada' }]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Features
|
|
79
|
+
|
|
80
|
+
- **Zero dependencies** — pure TypeScript, no external packages
|
|
81
|
+
- **DOM mode** — pass a form element or selector, tracks `<input>`, `<select>`, `<textarea>` automatically
|
|
82
|
+
- **Controlled mode** — pass initial fields object, call `update()` when state changes
|
|
83
|
+
- **`isDirty`** — single boolean, always up to date
|
|
84
|
+
- **`changedFields`** — array of `{ name, original, current }` for every changed field
|
|
85
|
+
- **`beforeunload` guard** — one option to prevent accidental navigation
|
|
86
|
+
- **`snapshot()`** — re-baseline after save
|
|
87
|
+
- **`onDirtyChange` callback** — fires only when dirty state transitions
|
|
88
|
+
- **Handles edge cases** — checkboxes, radio buttons, multi-selects, nested objects
|
|
89
|
+
- **SSR safe** — guards all DOM access behind `typeof window`
|
|
90
|
+
- **~0.9kB minified + gzipped**
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## API
|
|
95
|
+
|
|
96
|
+
### `new FormDirty(options?)`
|
|
97
|
+
|
|
98
|
+
| Option | Type | Default | Description |
|
|
99
|
+
|--------|------|---------|-------------|
|
|
100
|
+
| `form` | `HTMLFormElement \| string` | — | DOM form element or CSS selector |
|
|
101
|
+
| `fields` | `Record<string, unknown>` | — | Initial field values for controlled mode |
|
|
102
|
+
| `beforeUnload` | `boolean` | `false` | Auto-attach `beforeunload` guard |
|
|
103
|
+
| `onDirtyChange` | `(dirty: boolean) => void` | — | Called when dirty state changes |
|
|
104
|
+
|
|
105
|
+
### Instance properties
|
|
106
|
+
|
|
107
|
+
| Property | Type | Description |
|
|
108
|
+
|----------|------|-------------|
|
|
109
|
+
| `.isDirty` | `boolean` | Whether any field differs from the baseline |
|
|
110
|
+
| `.changedFields` | `ChangedField[]` | Array of changed fields with original and current values |
|
|
111
|
+
|
|
112
|
+
### Instance methods
|
|
113
|
+
|
|
114
|
+
| Method | Returns | Description |
|
|
115
|
+
|--------|---------|-------------|
|
|
116
|
+
| `.snapshot()` | `void` | Capture current state as the new clean baseline |
|
|
117
|
+
| `.update(fields)` | `void` | Push new field values (controlled mode) |
|
|
118
|
+
| `.guard(enable?)` | `void` | Toggle the `beforeunload` guard on/off |
|
|
119
|
+
| `.destroy()` | `void` | Remove all listeners and clean up |
|
|
120
|
+
|
|
121
|
+
### `ChangedField`
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
interface ChangedField {
|
|
125
|
+
name: string;
|
|
126
|
+
original: unknown;
|
|
127
|
+
current: unknown;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Examples
|
|
134
|
+
|
|
135
|
+
### React hook
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
import { useEffect, useRef } from 'react';
|
|
139
|
+
import FormDirty from 'form-dirty';
|
|
140
|
+
|
|
141
|
+
function useFormDirty(fields: Record<string, unknown>) {
|
|
142
|
+
const fdRef = useRef<FormDirty | null>(null);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
fdRef.current = new FormDirty({
|
|
146
|
+
fields,
|
|
147
|
+
beforeUnload: true,
|
|
148
|
+
});
|
|
149
|
+
return () => fdRef.current?.destroy();
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
fdRef.current?.update(fields);
|
|
154
|
+
}, [fields]);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
get isDirty() { return fdRef.current?.isDirty ?? false; },
|
|
158
|
+
get changedFields() { return fdRef.current?.changedFields ?? []; },
|
|
159
|
+
snapshot: () => fdRef.current?.snapshot(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Vue composable
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { onMounted, onUnmounted, reactive } from 'vue';
|
|
168
|
+
import FormDirty from 'form-dirty';
|
|
169
|
+
|
|
170
|
+
export function useFormDirty(formSelector: string) {
|
|
171
|
+
let fd: FormDirty;
|
|
172
|
+
const state = reactive({ isDirty: false, changedFields: [] as any[] });
|
|
173
|
+
|
|
174
|
+
onMounted(() => {
|
|
175
|
+
fd = new FormDirty({
|
|
176
|
+
form: formSelector,
|
|
177
|
+
beforeUnload: true,
|
|
178
|
+
onDirtyChange: (dirty) => {
|
|
179
|
+
state.isDirty = dirty;
|
|
180
|
+
state.changedFields = fd.changedFields;
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
onUnmounted(() => fd?.destroy());
|
|
186
|
+
|
|
187
|
+
return { state, snapshot: () => fd?.snapshot() };
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Snapshot after save
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const fd = new FormDirty({ form: '#settings-form', beforeUnload: true });
|
|
195
|
+
|
|
196
|
+
async function handleSave() {
|
|
197
|
+
await fetch('/api/settings', { method: 'POST', body: getFormData() });
|
|
198
|
+
fd.snapshot(); // current state is now the new baseline
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### CDN (no build step)
|
|
203
|
+
|
|
204
|
+
```html
|
|
205
|
+
<script type="module">
|
|
206
|
+
import FormDirty from 'https://esm.sh/form-dirty';
|
|
207
|
+
|
|
208
|
+
const fd = new FormDirty({
|
|
209
|
+
form: '#contact-form',
|
|
210
|
+
beforeUnload: true,
|
|
211
|
+
onDirtyChange: (dirty) => {
|
|
212
|
+
document.getElementById('save-btn').disabled = !dirty;
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
</script>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
[MIT](https://github.com/everything-frontend/form-dirty/blob/main/LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var a=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var c=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var m=(t,e)=>{for(var i in e)a(t,i,{get:e[i],enumerable:!0})},v=(t,e,i,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of c(e))!p.call(t,n)&&n!==i&&a(t,n,{get:()=>e[n],enumerable:!(r=h(e,n))||r.enumerable});return t};var y=t=>v(a({},"__esModule",{value:!0}),t);var g={};m(g,{FormDirty:()=>u,default:()=>E});module.exports=y(g);function b(t){return typeof t=="string"?document.querySelector(t):t}function o(t){let e={},i=t.elements;for(let r=0;r<i.length;r++){let n=i[r],s=n.getAttribute("name");if(s)if(n instanceof HTMLInputElement)if(n.type==="checkbox")e[s]=n.checked;else if(n.type==="radio")n.checked?e[s]=n.value:s in e||(e[s]="");else{if(n.type==="file")continue;e[s]=n.value}else n instanceof HTMLSelectElement?n.multiple?e[s]=Array.from(n.selectedOptions).map(d=>d.value):e[s]=n.value:n instanceof HTMLTextAreaElement&&(e[s]=n.value)}return e}function l(t){let e={};for(let i in t){let r=t[i];e[i]=typeof r=="object"&&r!==null?JSON.parse(JSON.stringify(r)):r}return e}function f(t,e){return t===e?!0:typeof t!=typeof e?!1:typeof t=="object"&&t!==null&&e!==null?JSON.stringify(t)===JSON.stringify(e):!1}var u=class{constructor(e={}){this.formEl=null;this.baseline={};this.current={};this.wasDirty=!1;this.guardActive=!1;this.boundOnInput=null;this.boundBeforeUnload=null;this.opts=e,e.form?(this.formEl=typeof window<"u"?b(e.form):null,this.formEl&&(this.baseline=o(this.formEl),this.current=o(this.formEl),this.boundOnInput=()=>this.handleInput(),this.formEl.addEventListener("input",this.boundOnInput),this.formEl.addEventListener("change",this.boundOnInput))):e.fields&&(this.baseline=l(e.fields),this.current=l(e.fields)),e.beforeUnload&&this.guard(!0)}get isDirty(){let e=new Set([...Object.keys(this.baseline),...Object.keys(this.current)]);for(let i of e)if(!f(this.baseline[i],this.current[i]))return!0;return!1}get changedFields(){let e=[],i=new Set([...Object.keys(this.baseline),...Object.keys(this.current)]);for(let r of i)f(this.baseline[r],this.current[r])||e.push({name:r,original:this.baseline[r],current:this.current[r]});return e}snapshot(){this.formEl?(this.baseline=o(this.formEl),this.current=o(this.formEl)):this.baseline=l(this.current),this.notify()}update(e){this.current=l(e),this.notify()}guard(e=!0){typeof window>"u"||(e&&!this.guardActive?(this.boundBeforeUnload=i=>{this.isDirty&&i.preventDefault()},window.addEventListener("beforeunload",this.boundBeforeUnload),this.guardActive=!0):!e&&this.guardActive&&(this.boundBeforeUnload&&(window.removeEventListener("beforeunload",this.boundBeforeUnload),this.boundBeforeUnload=null),this.guardActive=!1))}destroy(){this.formEl&&this.boundOnInput&&(this.formEl.removeEventListener("input",this.boundOnInput),this.formEl.removeEventListener("change",this.boundOnInput)),this.guard(!1),this.formEl=null,this.baseline={},this.current={}}handleInput(){this.formEl&&(this.current=o(this.formEl)),this.notify()}notify(){let e=this.isDirty;e!==this.wasDirty&&(this.wasDirty=e,this.opts.onDirtyChange?.(e))}},E=u;0&&(module.exports={FormDirty});
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export interface FormDirtyOptions {\n form?: HTMLFormElement | string;\n fields?: Record<string, unknown>;\n beforeUnload?: boolean;\n onDirtyChange?: (dirty: boolean) => void;\n}\n\nexport interface ChangedField {\n name: string;\n original: unknown;\n current: unknown;\n}\n\ntype FieldMap = Record<string, unknown>;\n\nfunction resolveForm(form: HTMLFormElement | string): HTMLFormElement | null {\n if (typeof form === \"string\") {\n return document.querySelector<HTMLFormElement>(form);\n }\n return form;\n}\n\nfunction readFormValues(form: HTMLFormElement): FieldMap {\n const values: FieldMap = {};\n const elements = form.elements;\n\n for (let i = 0; i < elements.length; i++) {\n const el = elements[i] as\n | HTMLInputElement\n | HTMLSelectElement\n | HTMLTextAreaElement;\n const name = el.getAttribute(\"name\");\n if (!name) continue;\n\n if (el instanceof HTMLInputElement) {\n if (el.type === \"checkbox\") {\n values[name] = el.checked;\n } else if (el.type === \"radio\") {\n if (el.checked) values[name] = el.value;\n else if (!(name in values)) values[name] = \"\";\n } else if (el.type === \"file\") {\n continue;\n } else {\n values[name] = el.value;\n }\n } else if (el instanceof HTMLSelectElement) {\n if (el.multiple) {\n values[name] = Array.from(el.selectedOptions).map((o) => o.value);\n } else {\n values[name] = el.value;\n }\n } else if (el instanceof HTMLTextAreaElement) {\n values[name] = el.value;\n }\n }\n\n return values;\n}\n\nfunction cloneFields(fields: FieldMap): FieldMap {\n const out: FieldMap = {};\n for (const key in fields) {\n const v = fields[key];\n out[key] =\n typeof v === \"object\" && v !== null ? JSON.parse(JSON.stringify(v)) : v;\n }\n return out;\n}\n\nfunction eq(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (typeof a === \"object\" && a !== null && b !== null) {\n return JSON.stringify(a) === JSON.stringify(b);\n }\n return false;\n}\n\nexport class FormDirty {\n private formEl: HTMLFormElement | null = null;\n private baseline: FieldMap = {};\n private current: FieldMap = {};\n private wasDirty = false;\n private guardActive = false;\n private opts: FormDirtyOptions;\n private boundOnInput: (() => void) | null = null;\n private boundBeforeUnload: ((e: BeforeUnloadEvent) => void) | null = null;\n\n constructor(options: FormDirtyOptions = {}) {\n this.opts = options;\n\n if (options.form) {\n this.formEl =\n typeof window !== \"undefined\" ? resolveForm(options.form) : null;\n if (this.formEl) {\n this.baseline = readFormValues(this.formEl);\n this.current = readFormValues(this.formEl);\n this.boundOnInput = () => this.handleInput();\n this.formEl.addEventListener(\"input\", this.boundOnInput);\n this.formEl.addEventListener(\"change\", this.boundOnInput);\n }\n } else if (options.fields) {\n this.baseline = cloneFields(options.fields);\n this.current = cloneFields(options.fields);\n }\n\n if (options.beforeUnload) {\n this.guard(true);\n }\n }\n\n get isDirty(): boolean {\n const allKeys = new Set([\n ...Object.keys(this.baseline),\n ...Object.keys(this.current),\n ]);\n for (const key of allKeys) {\n if (!eq(this.baseline[key], this.current[key])) return true;\n }\n return false;\n }\n\n get changedFields(): ChangedField[] {\n const changed: ChangedField[] = [];\n const allKeys = new Set([\n ...Object.keys(this.baseline),\n ...Object.keys(this.current),\n ]);\n for (const key of allKeys) {\n if (!eq(this.baseline[key], this.current[key])) {\n changed.push({\n name: key,\n original: this.baseline[key],\n current: this.current[key],\n });\n }\n }\n return changed;\n }\n\n snapshot(): void {\n if (this.formEl) {\n this.baseline = readFormValues(this.formEl);\n this.current = readFormValues(this.formEl);\n } else {\n this.baseline = cloneFields(this.current);\n }\n this.notify();\n }\n\n update(fields: FieldMap): void {\n this.current = cloneFields(fields);\n this.notify();\n }\n\n guard(enable = true): void {\n if (typeof window === \"undefined\") return;\n\n if (enable && !this.guardActive) {\n this.boundBeforeUnload = (e: BeforeUnloadEvent) => {\n if (this.isDirty) {\n e.preventDefault();\n }\n };\n window.addEventListener(\"beforeunload\", this.boundBeforeUnload);\n this.guardActive = true;\n } else if (!enable && this.guardActive) {\n if (this.boundBeforeUnload) {\n window.removeEventListener(\"beforeunload\", this.boundBeforeUnload);\n this.boundBeforeUnload = null;\n }\n this.guardActive = false;\n }\n }\n\n destroy(): void {\n if (this.formEl && this.boundOnInput) {\n this.formEl.removeEventListener(\"input\", this.boundOnInput);\n this.formEl.removeEventListener(\"change\", this.boundOnInput);\n }\n this.guard(false);\n this.formEl = null;\n this.baseline = {};\n this.current = {};\n }\n\n private handleInput(): void {\n if (this.formEl) {\n this.current = readFormValues(this.formEl);\n }\n this.notify();\n }\n\n private notify(): void {\n const dirty = this.isDirty;\n if (dirty !== this.wasDirty) {\n this.wasDirty = dirty;\n this.opts.onDirtyChange?.(dirty);\n }\n }\n}\n\nexport default FormDirty;\n"],"mappings":"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","names":["index_exports","__export","FormDirty","index_default","__toCommonJS","resolveForm","form","readFormValues","values","elements","i","el","name","o","cloneFields","fields","out","key","v","eq","a","b","options","allKeys","changed","enable","e","dirty"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
interface FormDirtyOptions {
|
|
2
|
+
form?: HTMLFormElement | string;
|
|
3
|
+
fields?: Record<string, unknown>;
|
|
4
|
+
beforeUnload?: boolean;
|
|
5
|
+
onDirtyChange?: (dirty: boolean) => void;
|
|
6
|
+
}
|
|
7
|
+
interface ChangedField {
|
|
8
|
+
name: string;
|
|
9
|
+
original: unknown;
|
|
10
|
+
current: unknown;
|
|
11
|
+
}
|
|
12
|
+
type FieldMap = Record<string, unknown>;
|
|
13
|
+
declare class FormDirty {
|
|
14
|
+
private formEl;
|
|
15
|
+
private baseline;
|
|
16
|
+
private current;
|
|
17
|
+
private wasDirty;
|
|
18
|
+
private guardActive;
|
|
19
|
+
private opts;
|
|
20
|
+
private boundOnInput;
|
|
21
|
+
private boundBeforeUnload;
|
|
22
|
+
constructor(options?: FormDirtyOptions);
|
|
23
|
+
get isDirty(): boolean;
|
|
24
|
+
get changedFields(): ChangedField[];
|
|
25
|
+
snapshot(): void;
|
|
26
|
+
update(fields: FieldMap): void;
|
|
27
|
+
guard(enable?: boolean): void;
|
|
28
|
+
destroy(): void;
|
|
29
|
+
private handleInput;
|
|
30
|
+
private notify;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { type ChangedField, FormDirty, type FormDirtyOptions, FormDirty as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
interface FormDirtyOptions {
|
|
2
|
+
form?: HTMLFormElement | string;
|
|
3
|
+
fields?: Record<string, unknown>;
|
|
4
|
+
beforeUnload?: boolean;
|
|
5
|
+
onDirtyChange?: (dirty: boolean) => void;
|
|
6
|
+
}
|
|
7
|
+
interface ChangedField {
|
|
8
|
+
name: string;
|
|
9
|
+
original: unknown;
|
|
10
|
+
current: unknown;
|
|
11
|
+
}
|
|
12
|
+
type FieldMap = Record<string, unknown>;
|
|
13
|
+
declare class FormDirty {
|
|
14
|
+
private formEl;
|
|
15
|
+
private baseline;
|
|
16
|
+
private current;
|
|
17
|
+
private wasDirty;
|
|
18
|
+
private guardActive;
|
|
19
|
+
private opts;
|
|
20
|
+
private boundOnInput;
|
|
21
|
+
private boundBeforeUnload;
|
|
22
|
+
constructor(options?: FormDirtyOptions);
|
|
23
|
+
get isDirty(): boolean;
|
|
24
|
+
get changedFields(): ChangedField[];
|
|
25
|
+
snapshot(): void;
|
|
26
|
+
update(fields: FieldMap): void;
|
|
27
|
+
guard(enable?: boolean): void;
|
|
28
|
+
destroy(): void;
|
|
29
|
+
private handleInput;
|
|
30
|
+
private notify;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { type ChangedField, FormDirty, type FormDirtyOptions, FormDirty as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function d(t){return typeof t=="string"?document.querySelector(t):t}function o(t){let e={},r=t.elements;for(let i=0;i<r.length;i++){let n=r[i],s=n.getAttribute("name");if(s)if(n instanceof HTMLInputElement)if(n.type==="checkbox")e[s]=n.checked;else if(n.type==="radio")n.checked?e[s]=n.value:s in e||(e[s]="");else{if(n.type==="file")continue;e[s]=n.value}else n instanceof HTMLSelectElement?n.multiple?e[s]=Array.from(n.selectedOptions).map(f=>f.value):e[s]=n.value:n instanceof HTMLTextAreaElement&&(e[s]=n.value)}return e}function l(t){let e={};for(let r in t){let i=t[r];e[r]=typeof i=="object"&&i!==null?JSON.parse(JSON.stringify(i)):i}return e}function a(t,e){return t===e?!0:typeof t!=typeof e?!1:typeof t=="object"&&t!==null&&e!==null?JSON.stringify(t)===JSON.stringify(e):!1}var u=class{constructor(e={}){this.formEl=null;this.baseline={};this.current={};this.wasDirty=!1;this.guardActive=!1;this.boundOnInput=null;this.boundBeforeUnload=null;this.opts=e,e.form?(this.formEl=typeof window<"u"?d(e.form):null,this.formEl&&(this.baseline=o(this.formEl),this.current=o(this.formEl),this.boundOnInput=()=>this.handleInput(),this.formEl.addEventListener("input",this.boundOnInput),this.formEl.addEventListener("change",this.boundOnInput))):e.fields&&(this.baseline=l(e.fields),this.current=l(e.fields)),e.beforeUnload&&this.guard(!0)}get isDirty(){let e=new Set([...Object.keys(this.baseline),...Object.keys(this.current)]);for(let r of e)if(!a(this.baseline[r],this.current[r]))return!0;return!1}get changedFields(){let e=[],r=new Set([...Object.keys(this.baseline),...Object.keys(this.current)]);for(let i of r)a(this.baseline[i],this.current[i])||e.push({name:i,original:this.baseline[i],current:this.current[i]});return e}snapshot(){this.formEl?(this.baseline=o(this.formEl),this.current=o(this.formEl)):this.baseline=l(this.current),this.notify()}update(e){this.current=l(e),this.notify()}guard(e=!0){typeof window>"u"||(e&&!this.guardActive?(this.boundBeforeUnload=r=>{this.isDirty&&r.preventDefault()},window.addEventListener("beforeunload",this.boundBeforeUnload),this.guardActive=!0):!e&&this.guardActive&&(this.boundBeforeUnload&&(window.removeEventListener("beforeunload",this.boundBeforeUnload),this.boundBeforeUnload=null),this.guardActive=!1))}destroy(){this.formEl&&this.boundOnInput&&(this.formEl.removeEventListener("input",this.boundOnInput),this.formEl.removeEventListener("change",this.boundOnInput)),this.guard(!1),this.formEl=null,this.baseline={},this.current={}}handleInput(){this.formEl&&(this.current=o(this.formEl)),this.notify()}notify(){let e=this.isDirty;e!==this.wasDirty&&(this.wasDirty=e,this.opts.onDirtyChange?.(e))}},h=u;export{u as FormDirty,h as default};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export interface FormDirtyOptions {\n form?: HTMLFormElement | string;\n fields?: Record<string, unknown>;\n beforeUnload?: boolean;\n onDirtyChange?: (dirty: boolean) => void;\n}\n\nexport interface ChangedField {\n name: string;\n original: unknown;\n current: unknown;\n}\n\ntype FieldMap = Record<string, unknown>;\n\nfunction resolveForm(form: HTMLFormElement | string): HTMLFormElement | null {\n if (typeof form === \"string\") {\n return document.querySelector<HTMLFormElement>(form);\n }\n return form;\n}\n\nfunction readFormValues(form: HTMLFormElement): FieldMap {\n const values: FieldMap = {};\n const elements = form.elements;\n\n for (let i = 0; i < elements.length; i++) {\n const el = elements[i] as\n | HTMLInputElement\n | HTMLSelectElement\n | HTMLTextAreaElement;\n const name = el.getAttribute(\"name\");\n if (!name) continue;\n\n if (el instanceof HTMLInputElement) {\n if (el.type === \"checkbox\") {\n values[name] = el.checked;\n } else if (el.type === \"radio\") {\n if (el.checked) values[name] = el.value;\n else if (!(name in values)) values[name] = \"\";\n } else if (el.type === \"file\") {\n continue;\n } else {\n values[name] = el.value;\n }\n } else if (el instanceof HTMLSelectElement) {\n if (el.multiple) {\n values[name] = Array.from(el.selectedOptions).map((o) => o.value);\n } else {\n values[name] = el.value;\n }\n } else if (el instanceof HTMLTextAreaElement) {\n values[name] = el.value;\n }\n }\n\n return values;\n}\n\nfunction cloneFields(fields: FieldMap): FieldMap {\n const out: FieldMap = {};\n for (const key in fields) {\n const v = fields[key];\n out[key] =\n typeof v === \"object\" && v !== null ? JSON.parse(JSON.stringify(v)) : v;\n }\n return out;\n}\n\nfunction eq(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (typeof a === \"object\" && a !== null && b !== null) {\n return JSON.stringify(a) === JSON.stringify(b);\n }\n return false;\n}\n\nexport class FormDirty {\n private formEl: HTMLFormElement | null = null;\n private baseline: FieldMap = {};\n private current: FieldMap = {};\n private wasDirty = false;\n private guardActive = false;\n private opts: FormDirtyOptions;\n private boundOnInput: (() => void) | null = null;\n private boundBeforeUnload: ((e: BeforeUnloadEvent) => void) | null = null;\n\n constructor(options: FormDirtyOptions = {}) {\n this.opts = options;\n\n if (options.form) {\n this.formEl =\n typeof window !== \"undefined\" ? resolveForm(options.form) : null;\n if (this.formEl) {\n this.baseline = readFormValues(this.formEl);\n this.current = readFormValues(this.formEl);\n this.boundOnInput = () => this.handleInput();\n this.formEl.addEventListener(\"input\", this.boundOnInput);\n this.formEl.addEventListener(\"change\", this.boundOnInput);\n }\n } else if (options.fields) {\n this.baseline = cloneFields(options.fields);\n this.current = cloneFields(options.fields);\n }\n\n if (options.beforeUnload) {\n this.guard(true);\n }\n }\n\n get isDirty(): boolean {\n const allKeys = new Set([\n ...Object.keys(this.baseline),\n ...Object.keys(this.current),\n ]);\n for (const key of allKeys) {\n if (!eq(this.baseline[key], this.current[key])) return true;\n }\n return false;\n }\n\n get changedFields(): ChangedField[] {\n const changed: ChangedField[] = [];\n const allKeys = new Set([\n ...Object.keys(this.baseline),\n ...Object.keys(this.current),\n ]);\n for (const key of allKeys) {\n if (!eq(this.baseline[key], this.current[key])) {\n changed.push({\n name: key,\n original: this.baseline[key],\n current: this.current[key],\n });\n }\n }\n return changed;\n }\n\n snapshot(): void {\n if (this.formEl) {\n this.baseline = readFormValues(this.formEl);\n this.current = readFormValues(this.formEl);\n } else {\n this.baseline = cloneFields(this.current);\n }\n this.notify();\n }\n\n update(fields: FieldMap): void {\n this.current = cloneFields(fields);\n this.notify();\n }\n\n guard(enable = true): void {\n if (typeof window === \"undefined\") return;\n\n if (enable && !this.guardActive) {\n this.boundBeforeUnload = (e: BeforeUnloadEvent) => {\n if (this.isDirty) {\n e.preventDefault();\n }\n };\n window.addEventListener(\"beforeunload\", this.boundBeforeUnload);\n this.guardActive = true;\n } else if (!enable && this.guardActive) {\n if (this.boundBeforeUnload) {\n window.removeEventListener(\"beforeunload\", this.boundBeforeUnload);\n this.boundBeforeUnload = null;\n }\n this.guardActive = false;\n }\n }\n\n destroy(): void {\n if (this.formEl && this.boundOnInput) {\n this.formEl.removeEventListener(\"input\", this.boundOnInput);\n this.formEl.removeEventListener(\"change\", this.boundOnInput);\n }\n this.guard(false);\n this.formEl = null;\n this.baseline = {};\n this.current = {};\n }\n\n private handleInput(): void {\n if (this.formEl) {\n this.current = readFormValues(this.formEl);\n }\n this.notify();\n }\n\n private notify(): void {\n const dirty = this.isDirty;\n if (dirty !== this.wasDirty) {\n this.wasDirty = dirty;\n this.opts.onDirtyChange?.(dirty);\n }\n }\n}\n\nexport default FormDirty;\n"],"mappings":"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","names":["resolveForm","form","readFormValues","values","elements","el","name","o","cloneFields","fields","out","key","v","eq","a","b","FormDirty","options","allKeys","changed","enable","e","dirty","index_default"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "form-dirty",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Unsaved changes detection. Snapshot form state, expose isDirty + changedFields, handle beforeunload. Framework agnostic. Zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"form",
|
|
31
|
+
"dirty",
|
|
32
|
+
"unsaved-changes",
|
|
33
|
+
"beforeunload",
|
|
34
|
+
"changed-fields",
|
|
35
|
+
"zero-dependency"
|
|
36
|
+
],
|
|
37
|
+
"sideEffects": false,
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/everything-frontend/form-dirty"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"tsup": "^8.5.1",
|
|
45
|
+
"typescript": "^5.9.3"
|
|
46
|
+
}
|
|
47
|
+
}
|