@provydon/vue-auto-save 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/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # useAutoSaveForm
2
+
3
+ A powerful Vue 3 composable that automatically saves form data with intelligent debouncing, field filtering, and lifecycle hooks.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@provydon/vue-auto-save.svg)](https://www.npmjs.com/package/@provydon/vue-auto-save)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## โœจ Features
9
+
10
+ - ๐Ÿš€ **Auto-save on change** with configurable debounce
11
+ - ๐ŸŽฏ **Smart field filtering** - skip Inertia helpers or custom fields
12
+ - ๐Ÿ›ก๏ธ **Blockable watchers** - pause auto-save during initialization
13
+ - ๐Ÿ”„ **Lifecycle hooks** - beforeSave, afterSave, onError callbacks
14
+ - ๐ŸŽ›๏ธ **Custom serialization** - support for circular references and functions
15
+ - ๐Ÿงช **Custom comparators** - shallow/deep equality without stringification
16
+ - ๐Ÿงน **Automatic cleanup** - no memory leaks on component unmount
17
+ - ๐Ÿ“ฆ **Framework agnostic** - works with Axios, Inertia, Fetch, etc.
18
+
19
+ ## ๐Ÿš€ Quick Start
20
+
21
+ ```bash
22
+ npm install @provydon/vue-auto-save
23
+ ```
24
+
25
+ ```vue
26
+ <script setup>
27
+ import { reactive } from 'vue'
28
+ import { useAutoSaveForm } from '@provydon/vue-auto-save'
29
+
30
+ const form = reactive({
31
+ title: '',
32
+ content: '',
33
+ tags: []
34
+ })
35
+
36
+ const { isAutoSaving } = useAutoSaveForm(form, {
37
+ onSave: async () => {
38
+ await axios.post('/api/posts', form)
39
+ },
40
+ debounce: 2000,
41
+ debug: true
42
+ })
43
+ </script>
44
+
45
+ <template>
46
+ <form>
47
+ <input v-model="form.title" placeholder="Post title" />
48
+ <textarea v-model="form.content" placeholder="Post content" />
49
+ <div v-if="isAutoSaving">Saving...</div>
50
+ </form>
51
+ </template>
52
+ ```
53
+
54
+ ## ๐Ÿ“– API Reference
55
+
56
+ ### Basic Usage
57
+
58
+ ```ts
59
+ const { isAutoSaving, blockWatcher, unblockWatcher, stop } = useAutoSaveForm(
60
+ form, // reactive object or ref
61
+ options
62
+ )
63
+ ```
64
+
65
+ ### Options
66
+
67
+ | Option | Type | Default | Description |
68
+ |--------|------|---------|-------------|
69
+ | `onSave` | `() => void \| Promise<void>` | **Required** | Function called when auto-save should trigger |
70
+ | `debounce` | `number` | `3000` | Delay in milliseconds before saving |
71
+ | `skipFields` | `string[]` | `[]` | Field names to exclude from tracking |
72
+ | `skipInertiaFields` | `boolean` | `true` | Skip common Inertia.js form helpers |
73
+ | `deep` | `boolean` | `true` | Deep watch the form object |
74
+ | `debug` | `boolean` | `false` | Enable console logging |
75
+ | `saveOnInit` | `boolean` | `false` | Save immediately on mount |
76
+ | `serialize` | `(obj) => string` | `JSON.stringify` | Custom serialization function |
77
+ | `compare` | `(a, b) => boolean` | `undefined` | Custom comparison function |
78
+ | `onBeforeSave` | `() => void` | `undefined` | Called before saving |
79
+ | `onAfterSave` | `() => void` | `undefined` | Called after successful save |
80
+ | `onError` | `(err) => void` | `undefined` | Called on save error |
81
+
82
+ ### Return Values
83
+
84
+ | Property | Type | Description |
85
+ |----------|------|-------------|
86
+ | `isAutoSaving` | `Ref<boolean>` | Reactive boolean indicating save status |
87
+ | `blockWatcher` | `(ms?: number) => void` | Temporarily block auto-save |
88
+ | `unblockWatcher` | `(ms?: number \| null) => void` | Unblock and optionally save immediately |
89
+ | `stop` | `() => void` | Manually stop the watcher |
90
+
91
+ ## ๐ŸŽฏ Examples
92
+
93
+ ### With Inertia.js
94
+
95
+ ```ts
96
+ import { useForm } from '@inertiajs/vue3'
97
+ import { useAutoSaveForm } from '@provydon/vue-auto-save'
98
+
99
+ const form = useForm({
100
+ title: '',
101
+ content: '',
102
+ published: false
103
+ })
104
+
105
+ const { isAutoSaving } = useAutoSaveForm(form, {
106
+ onSave: () => form.post('/posts', { preserveState: true }),
107
+ skipInertiaFields: true, // Skips processing, errors, etc.
108
+ debounce: 1000
109
+ })
110
+ ```
111
+
112
+ ### Custom Serialization
113
+
114
+ ```ts
115
+ const { isAutoSaving } = useAutoSaveForm(form, {
116
+ onSave: saveToAPI,
117
+ serialize: (obj) => {
118
+ // Handle circular references or functions
119
+ return JSON.stringify(obj, (key, value) => {
120
+ if (typeof value === 'function') return '[Function]'
121
+ return value
122
+ })
123
+ }
124
+ })
125
+ ```
126
+
127
+ ### Custom Comparator
128
+
129
+ ```ts
130
+ import { isEqual } from 'lodash-es'
131
+
132
+ const { isAutoSaving } = useAutoSaveForm(form, {
133
+ onSave: saveToAPI,
134
+ compare: (a, b) => isEqual(a, b), // Deep equality without stringification
135
+ serialize: undefined // Not used when compare is provided
136
+ })
137
+ ```
138
+
139
+ ### Block During Initialization
140
+
141
+ ```ts
142
+ const { isAutoSaving, blockWatcher } = useAutoSaveForm(form, {
143
+ onSave: saveToAPI,
144
+ saveOnInit: false
145
+ })
146
+
147
+ // Block auto-save during form initialization
148
+ blockWatcher(5000) // Block for 5 seconds
149
+
150
+ // Or block indefinitely and unblock manually
151
+ blockWatcher()
152
+ // ... do initialization work ...
153
+ unblockWatcher() // Resume auto-save
154
+ ```
155
+
156
+ ### With Ref Forms
157
+
158
+ ```ts
159
+ const form = ref({
160
+ name: '',
161
+ email: ''
162
+ })
163
+
164
+ const { isAutoSaving } = useAutoSaveForm(form, {
165
+ onSave: saveToAPI
166
+ })
167
+ ```
168
+
169
+ ## ๐Ÿ”ง Advanced Usage
170
+
171
+ ### Lifecycle Hooks
172
+
173
+ ```ts
174
+ const { isAutoSaving } = useAutoSaveForm(form, {
175
+ onSave: saveToAPI,
176
+ onBeforeSave: () => {
177
+ console.log('About to save...')
178
+ },
179
+ onAfterSave: () => {
180
+ console.log('Save completed!')
181
+ },
182
+ onError: (error) => {
183
+ console.error('Save failed:', error)
184
+ }
185
+ })
186
+ ```
187
+
188
+ ### Manual Control
189
+
190
+ ```ts
191
+ const { isAutoSaving, stop } = useAutoSaveForm(form, {
192
+ onSave: saveToAPI
193
+ })
194
+
195
+ // Manually stop the watcher
196
+ stop()
197
+
198
+ // The watcher will also stop automatically on component unmount
199
+ ```
200
+
201
+ ## ๐ŸŽจ Styling Examples
202
+
203
+ ### Loading Indicator
204
+
205
+ ```vue
206
+ <template>
207
+ <div class="form-container">
208
+ <input v-model="form.title" />
209
+ <div v-if="isAutoSaving" class="save-indicator">
210
+ <span class="spinner"></span>
211
+ Auto-saving...
212
+ </div>
213
+ </div>
214
+ </template>
215
+
216
+ <style scoped>
217
+ .save-indicator {
218
+ display: flex;
219
+ align-items: center;
220
+ gap: 8px;
221
+ color: #666;
222
+ font-size: 14px;
223
+ }
224
+
225
+ .spinner {
226
+ width: 16px;
227
+ height: 16px;
228
+ border: 2px solid #ddd;
229
+ border-top: 2px solid #007bff;
230
+ border-radius: 50%;
231
+ animation: spin 1s linear infinite;
232
+ }
233
+
234
+ @keyframes spin {
235
+ 0% { transform: rotate(0deg); }
236
+ 100% { transform: rotate(360deg); }
237
+ }
238
+ </style>
239
+ ```
240
+
241
+ ## ๐Ÿšจ Important Notes
242
+
243
+ - **Circular References**: `JSON.stringify` (default serializer) will throw on circular references. Use a custom `serialize` function if needed.
244
+ - **Functions**: Functions won't survive `JSON.stringify`. Use custom serialization for function-heavy forms.
245
+ - **Vue Version**: Requires Vue 3.2+ (supports both reactive objects and refs).
246
+ - **Cleanup**: Watchers and timers are automatically cleaned up on component unmount.
247
+
248
+ ## ๐Ÿค Contributing
249
+
250
+ Contributions are welcome! Please feel free to submit a Pull Request.
251
+
252
+ ## ๐Ÿ“„ License
253
+
254
+ MIT ยฉ [Providence Ifeosame](https://github.com/provydon)
255
+
256
+ ## ๐Ÿ™ Support
257
+
258
+ If you find this package helpful, consider:
259
+ - โญ Starring the repository
260
+ - ๐Ÿ› Reporting bugs
261
+ - ๐Ÿ’ก Suggesting features
262
+ - โ˜• [Buying me a coffee](https://buymeacoffee.com/provydon)
263
+
264
+ Follow me on [Twitter](https://x.com/ProvyDon1) or connect on [LinkedIn](https://www.linkedin.com/in/providence-ifeosame/).
@@ -0,0 +1,69 @@
1
+ import { Ref } from 'vue';
2
+ export interface UseAutoSaveFormOptions {
3
+ /**
4
+ * Delay in milliseconds before auto-saving after changes (default: 3000ms)
5
+ */
6
+ debounce?: number;
7
+ /**
8
+ * List of form field keys to exclude from tracking
9
+ */
10
+ skipFields?: string[];
11
+ /**
12
+ * Whether to skip common Inertia form fields (default: true)
13
+ */
14
+ skipInertiaFields?: boolean;
15
+ /**
16
+ * Whether to deeply watch the form (default: true)
17
+ */
18
+ deep?: boolean;
19
+ /**
20
+ * Enable debug logs in the console (default: false)
21
+ */
22
+ debug?: boolean;
23
+ /**
24
+ * Custom serializer function (default: JSON.stringify)
25
+ * Note: Functions and non-serializable fields won't survive JSON.stringify.
26
+ * Circular references will also cause JSON.stringify to throw.
27
+ * Use a custom serializer if you need to handle these cases.
28
+ */
29
+ serialize?: (obj: Record<string, unknown>) => string;
30
+ /**
31
+ * Custom comparator function (optional)
32
+ * If provided, this will be used instead of string comparison
33
+ */
34
+ compare?: (a: Record<string, unknown>, b: Record<string, unknown>) => boolean;
35
+ /**
36
+ * Whether to save on initial mount (default: false)
37
+ */
38
+ saveOnInit?: boolean;
39
+ /**
40
+ * Called when a save should be triggered (required)
41
+ */
42
+ onSave: () => void | Promise<void>;
43
+ /**
44
+ * Called just before auto-saving starts
45
+ */
46
+ onBeforeSave?: () => void;
47
+ /**
48
+ * Called after a successful auto-save
49
+ */
50
+ onAfterSave?: () => void;
51
+ /**
52
+ * Called if auto-saving throws or fails
53
+ */
54
+ onError?: (err: unknown) => void;
55
+ }
56
+ /**
57
+ * Automatically watches a Vue 3 form object and triggers save on change with debounce.
58
+ * Includes support for skipping specific fields and Inertia form helpers.
59
+ *
60
+ * @param form - The form object to watch (typically a reactive or ref object)
61
+ * @param options - Configuration for debounce, lifecycle hooks, and field skipping
62
+ * @returns An object with `isAutoSaving` and `blockWatcher()` for temporary disable
63
+ */
64
+ export declare function useAutoSaveForm(form: Record<string, unknown> | Ref<Record<string, unknown>>, options: UseAutoSaveFormOptions): {
65
+ isAutoSaving: Ref<boolean, boolean>;
66
+ blockWatcher: (ms?: number) => void;
67
+ unblockWatcher: (ms?: number | null) => void;
68
+ stop: import('vue').WatchHandle;
69
+ };
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("vue"),P=["save","applicationId","isDirty","processing","errors","hasErrors","recentlySuccessful","wasSuccessful","data","transform","get","post","put","patch","delete","cancel","reset","clearErrors","setError","setData"];function q(l,p){const{debounce:n=3e3,skipFields:T=[],skipInertiaFields:I=!0,deep:O=!0,debug:o=!1,serialize:D=JSON.stringify,compare:S,saveOnInit:b=!1,onSave:W,onBeforeSave:h,onAfterSave:m,onError:c}=p,u=s.ref(!1),a=s.ref(!0);let r=()=>{},i=()=>{};const j=(t=1e3)=>{a.value=!1,r(),i(),setTimeout(()=>{a.value=!0},t)},z=(t=null)=>{if(a.value=!0,r(),i(),t===null)d();else{const e=A(d,t);i=e.cancel,e.call()}},f=()=>{const t=s.isRef(l)?s.unref(l):l,e={};for(const v of Object.keys(t))I&&P.includes(v)||T.includes(v)||(e[v]=t[v]);return e};let g=b?null:D(f()),y=S?b?null:f():null;const d=()=>{if(!a.value)return;const t=f();if(S){if(y&&S(y,t))return;y=t}else{const e=D(t);if(g!==null&&e===g)return;g=e}o&&console.log("[AutoSave] Detected changes. Saving..."),u.value=!0;try{h==null||h(),Promise.resolve(W()).then(()=>{m==null||m(),o&&console.log("[AutoSave] Save successful.")}).catch(e=>{c==null||c(e),o&&console.error("[AutoSave] Save failed:",e)}).finally(()=>{u.value=!1})}catch(e){c==null||c(e),o&&console.error("[AutoSave] Immediate error:",e),u.value=!1}},F=A(d,n),w=F.call;r=F.cancel;const k=s.watch(()=>f(),w,{deep:O,flush:"post"});return s.onScopeDispose(()=>{k(),r(),i()}),b&&d(),{isAutoSaving:u,blockWatcher:j,unblockWatcher:z,stop:k}}function A(l,p){let n;return{call:()=>{clearTimeout(n),n=setTimeout(()=>l(),p)},cancel:()=>{clearTimeout(n)}}}exports.useAutoSaveForm=q;
@@ -0,0 +1,115 @@
1
+ import { ref as F, watch as x, onScopeDispose as J, isRef as N, unref as P } from "vue";
2
+ const R = [
3
+ "save",
4
+ "applicationId",
5
+ "isDirty",
6
+ "processing",
7
+ "errors",
8
+ "hasErrors",
9
+ "recentlySuccessful",
10
+ "wasSuccessful",
11
+ "data",
12
+ "transform",
13
+ "get",
14
+ "post",
15
+ "put",
16
+ "patch",
17
+ "delete",
18
+ "cancel",
19
+ "reset",
20
+ "clearErrors",
21
+ "setError",
22
+ "setData"
23
+ ];
24
+ function C(s, v) {
25
+ const {
26
+ debounce: l = 3e3,
27
+ skipFields: I = [],
28
+ skipInertiaFields: T = !0,
29
+ deep: O = !0,
30
+ debug: n = !1,
31
+ serialize: y = JSON.stringify,
32
+ compare: p,
33
+ saveOnInit: S = !1,
34
+ onSave: W,
35
+ onBeforeSave: h,
36
+ onAfterSave: m,
37
+ onError: c
38
+ } = v, o = F(!1), a = F(!0);
39
+ let u = () => {
40
+ }, r = () => {
41
+ };
42
+ const z = (t = 1e3) => {
43
+ a.value = !1, u(), r(), setTimeout(() => {
44
+ a.value = !0;
45
+ }, t);
46
+ }, j = (t = null) => {
47
+ if (a.value = !0, u(), r(), t === null)
48
+ f();
49
+ else {
50
+ const e = A(f, t);
51
+ r = e.cancel, e.call();
52
+ }
53
+ }, i = () => {
54
+ const t = N(s) ? P(s) : s, e = {};
55
+ for (const d of Object.keys(t))
56
+ T && R.includes(d) || I.includes(d) || (e[d] = t[d]);
57
+ return e;
58
+ };
59
+ let b = S ? null : y(i()), g = p ? S ? null : i() : null;
60
+ const f = () => {
61
+ if (!a.value) return;
62
+ const t = i();
63
+ if (p) {
64
+ if (g && p(g, t)) return;
65
+ g = t;
66
+ } else {
67
+ const e = y(t);
68
+ if (b !== null && e === b) return;
69
+ b = e;
70
+ }
71
+ n && console.log("[AutoSave] Detected changes. Saving..."), o.value = !0;
72
+ try {
73
+ h == null || h(), Promise.resolve(W()).then(() => {
74
+ m == null || m(), n && console.log("[AutoSave] Save successful.");
75
+ }).catch((e) => {
76
+ c == null || c(e), n && console.error("[AutoSave] Save failed:", e);
77
+ }).finally(() => {
78
+ o.value = !1;
79
+ });
80
+ } catch (e) {
81
+ c == null || c(e), n && console.error("[AutoSave] Immediate error:", e), o.value = !1;
82
+ }
83
+ }, D = A(f, l), w = D.call;
84
+ u = D.cancel;
85
+ const k = x(
86
+ () => i(),
87
+ w,
88
+ {
89
+ deep: O,
90
+ flush: "post"
91
+ }
92
+ );
93
+ return J(() => {
94
+ k(), u(), r();
95
+ }), S && f(), {
96
+ isAutoSaving: o,
97
+ blockWatcher: z,
98
+ unblockWatcher: j,
99
+ stop: k
100
+ };
101
+ }
102
+ function A(s, v) {
103
+ let l;
104
+ return {
105
+ call: () => {
106
+ clearTimeout(l), l = setTimeout(() => s(), v);
107
+ },
108
+ cancel: () => {
109
+ clearTimeout(l);
110
+ }
111
+ };
112
+ }
113
+ export {
114
+ C as useAutoSaveForm
115
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@provydon/vue-auto-save",
3
+ "version": "1.0.0",
4
+ "description": "A Vue 3 composable that autosaves forms with debounce, optional field skipping, and blockable watchers.",
5
+ "main": "./dist/useAutoSaveForm.cjs",
6
+ "module": "./dist/useAutoSaveForm.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/useAutoSaveForm.mjs",
12
+ "require": "./dist/useAutoSaveForm.cjs"
13
+ }
14
+ },
15
+ "files": ["dist"],
16
+ "keywords": ["vue", "composable", "autosave", "form", "debounce", "vue3"],
17
+ "author": "Providence Ifeosame",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/provydon/vue-auto-save.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/provydon/vue-auto-save/issues"
25
+ },
26
+ "homepage": "https://github.com/provydon/vue-auto-save#readme",
27
+ "sideEffects": false,
28
+ "publishConfig": { "access": "public" },
29
+ "scripts": {
30
+ "build": "vite build",
31
+ "dev": "vite",
32
+ "clean": "rm -rf dist",
33
+ "test": "vitest",
34
+ "test:run": "vitest run",
35
+ "test:coverage": "vitest run --coverage"
36
+ },
37
+ "peerDependencies": {
38
+ "vue": "^3.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "vue": "^3.5.18",
42
+ "typescript": "^5.9.2",
43
+ "vite": "^6.3.5",
44
+ "vite-plugin-dts": "^4.5.4",
45
+ "vitest": "^3.2.4",
46
+ "@vue/test-utils": "^2.4.5",
47
+ "@vitest/coverage-v8": "^3.2.4",
48
+ "jsdom": "^25.0.1"
49
+ }
50
+ }