@provydon/vue-auto-save 1.1.1 → 1.3.1

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 CHANGED
@@ -68,6 +68,8 @@ const { isAutoSaving, blockWatcher, unblockWatcher, stop } = useAutoSaveForm(
68
68
  |--------|------|---------|-------------|
69
69
  | `onSave` | `() => void \| Promise<void>` | **Required** | Function called when auto-save should trigger |
70
70
  | `debounce` | `number` | `3000` | Delay in milliseconds before saving |
71
+ | `minSaveInterval` | `number` | `2000` | Minimum time in milliseconds between saves (prevents rapid successive saves) |
72
+ | `trackLastSaved` | `boolean` | `true` | Track last successfully saved state to prevent saving identical data (prevents infinite loops) |
71
73
  | `skipFields` | `string[]` | `[]` | Field names to exclude from tracking |
72
74
  | `skipInertiaFields` | `boolean` | `true` | Skip common Inertia.js form helpers |
73
75
  | `deep` | `boolean` | `true` | Deep watch the form object |
@@ -77,6 +79,7 @@ const { isAutoSaving, blockWatcher, unblockWatcher, stop } = useAutoSaveForm(
77
79
  | `compare` | `(a, b) => boolean` | `undefined` | Custom comparison function |
78
80
  | `onBeforeSave` | `() => void` | `undefined` | Called before saving |
79
81
  | `onAfterSave` | `() => void` | `undefined` | Called after successful save |
82
+ | `onSaveSuccess` | `(savedData) => void` | `undefined` | Called when save completes with saved form data (useful for updating tracked state with server response) |
80
83
  | `onError` | `(err) => void` | `undefined` | Called on save error |
81
84
 
82
85
  ### Return Values
@@ -153,6 +156,28 @@ blockWatcher()
153
156
  unblockWatcher() // Resume auto-save
154
157
  ```
155
158
 
159
+ ### Prevent Infinite Loops with Server Updates (Default Behavior)
160
+
161
+ ```ts
162
+ // trackLastSaved and minSaveInterval are enabled by default
163
+ const { isAutoSaving } = useAutoSaveForm(form, {
164
+ onSave: async () => {
165
+ const response = await api.post('/save', form)
166
+ // Server response updates form data - no infinite loop!
167
+ Object.assign(form, response.data)
168
+ }
169
+ // trackLastSaved: true (default)
170
+ // minSaveInterval: 2000 (default)
171
+ })
172
+
173
+ // To disable these features if needed:
174
+ const { isAutoSaving } = useAutoSaveForm(form, {
175
+ onSave: saveToAPI,
176
+ trackLastSaved: false, // Disable if you want to save identical data
177
+ minSaveInterval: 0, // Disable minimum interval
178
+ })
179
+ ```
180
+
156
181
  ### With Ref Forms
157
182
 
158
183
  ```ts
package/dist/index.d.ts CHANGED
@@ -4,6 +4,17 @@ export interface UseAutoSaveFormOptions {
4
4
  * Delay in milliseconds before auto-saving after changes (default: 3000ms)
5
5
  */
6
6
  debounce?: number;
7
+ /**
8
+ * Minimum time in milliseconds between saves (default: 2000ms)
9
+ * Prevents rapid successive saves even if data changes
10
+ */
11
+ minSaveInterval?: number;
12
+ /**
13
+ * Track last successfully saved state to prevent saving identical data (default: true)
14
+ * When enabled, only saves if current state differs from last saved state
15
+ * Prevents infinite loops when server responses update form data
16
+ */
17
+ trackLastSaved?: boolean;
7
18
  /**
8
19
  * List of form field keys to exclude from tracking
9
20
  */
@@ -46,8 +57,14 @@ export interface UseAutoSaveFormOptions {
46
57
  onBeforeSave?: () => void;
47
58
  /**
48
59
  * Called after a successful auto-save
60
+ * If trackLastSaved is true, the form state at this point will be tracked as the last saved state
49
61
  */
50
62
  onAfterSave?: () => void;
63
+ /**
64
+ * Called when save completes successfully with the saved form data
65
+ * Use this to update the tracked last saved state with server response data
66
+ */
67
+ onSaveSuccess?: (savedData: Record<string, unknown>) => void;
51
68
  /**
52
69
  * Called if auto-saving throws or fails
53
70
  */
@@ -1 +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(c,h){const{debounce:n=3e3,skipFields:T=[],skipInertiaFields:I=!0,deep:O=!0,debug:u=!1,serialize:D=JSON.stringify,compare:o,saveOnInit:g=!1,onSave:W,onBeforeSave:m,onAfterSave:y,onError:l}=h,a=s.ref(!1),r=s.ref(!0);let i=()=>{},f=()=>{};const j=(t=1e3)=>{r.value=!1,i(),f(),setTimeout(()=>{r.value=!0},t)},z=(t=null)=>{if(r.value=!0,i(),f(),t===null)d(),o?p=null:v=null,S();else{const e=A(S,t);f=e.cancel,e.call()}},d=()=>{const t=s.isRef(c)?s.unref(c):c,e={};for(const b of Object.keys(t))I&&P.includes(b)||T.includes(b)||(e[b]=t[b]);return e};let v=g?null:D(d()),p=o?g?null:d():null;const S=()=>{if(!r.value)return;const t=d();if(o){if(p&&o(p,t))return;p=t}else{const e=D(t);if(v!==null&&e===v)return;v=e}u&&console.log("[AutoSave] Detected changes. Saving..."),a.value=!0;try{m==null||m(),Promise.resolve(W()).then(()=>{y==null||y(),u&&console.log("[AutoSave] Save successful.")}).catch(e=>{l==null||l(e),u&&console.error("[AutoSave] Save failed:",e)}).finally(()=>{a.value=!1})}catch(e){l==null||l(e),u&&console.error("[AutoSave] Immediate error:",e),a.value=!1}},F=A(S,n),w=F.call;i=F.cancel;const k=s.watch(c,w,{deep:O,flush:"post"});return s.onScopeDispose(()=>{k(),i(),f()}),g&&S(),{isAutoSaving:a,blockWatcher:j,unblockWatcher:z,stop:k}}function A(c,h){let n;return{call:()=>{clearTimeout(n),n=setTimeout(()=>c(),h)},cancel:()=>{clearTimeout(n)}}}exports.useAutoSaveForm=q;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("vue"),E=["save","applicationId","isDirty","processing","errors","hasErrors","recentlySuccessful","wasSuccessful","data","transform","get","post","put","patch","delete","cancel","reset","clearErrors","setError","setData"];function G(c,D){const{debounce:f=3e3,minSaveInterval:O=2e3,trackLastSaved:p=!0,skipFields:L=[],skipInertiaFields:q=!0,deep:J=!0,debug:n=!1,serialize:v=JSON.stringify,compare:s,saveOnInit:g=!1,onSave:M,onBeforeSave:F,onAfterSave:w,onSaveSuccess:I,onError:o}=D,b=a.ref(!1),m=a.ref(!0);let h=null,k=null,j=0,y=()=>{},A=()=>{},i=!1;const z=(e=1e3)=>{m.value=!1,y(),A(),setTimeout(()=>{m.value=!0},e)},N=(e=null)=>{if(m.value=!0,y(),A(),e===null)i=!0,s?r=null:u=null,T(),i=!1;else{const l=x(T,e);A=l.cancel,l.call()}},d=()=>{const e=a.isRef(c)?a.unref(c):c,l={};for(const t of Object.keys(e))q&&E.includes(t)||L.includes(t)||(l[t]=e[t]);return l};let u=g?null:v(d()),r=s?g?null:d():null;if(p&&!g){const e=d();s?k={...e}:h=v(e)}const R=e=>{p&&(s?k={...e}:h=v(e))},T=()=>{if(!m.value&&!i)return;const e=Date.now();if(!i&&O>0&&e-j<O){n&&console.log("[AutoSave] Skipping save - too soon after last save");return}const l=d();if(i)s?r=l:u=v(l);else if(s){if(p&&k&&s(k,l)){n&&console.log("[AutoSave] Skipping save - identical to last saved state"),r=l;return}if(r&&s(r,l))return;r=l}else{const t=v(l);if(p&&h!==null&&t===h){n&&console.log("[AutoSave] Skipping save - identical to last saved state"),u=t;return}if(u!==null&&t===u)return;u=t}n&&console.log("[AutoSave] Detected changes. Saving..."),b.value=!0,j=e;try{F==null||F();const t=Promise.resolve(M()).then(async()=>{await a.nextTick(),await a.nextTick();const S=d();R(S),I==null||I(S),w==null||w(),n&&console.log("[AutoSave] Save successful.")}).catch(S=>{o==null||o(S),n&&console.error("[AutoSave] Save failed:",S)}).finally(()=>{b.value=!1});return i||z(5e3),t}catch(t){o==null||o(t),n&&console.error("[AutoSave] Immediate error:",t),b.value=!1}},W=x(T,f),C=W.call;y=W.cancel;const P=a.watch(c,C,{deep:J,flush:"post"});return a.onScopeDispose(()=>{P(),y(),A()}),g&&T(),{isAutoSaving:b,blockWatcher:z,unblockWatcher:N,stop:P}}function x(c,D){let f;return{call:()=>{clearTimeout(f),f=setTimeout(()=>c(),D)},cancel:()=>{clearTimeout(f)}}}exports.useAutoSaveForm=G;
@@ -1,5 +1,5 @@
1
- import { ref as F, watch as x, onScopeDispose as J, isRef as N, unref as P } from "vue";
2
- const R = [
1
+ import { ref as x, watch as H, onScopeDispose as K, isRef as M, unref as Q, nextTick as L } from "vue";
2
+ const U = [
3
3
  "save",
4
4
  "applicationId",
5
5
  "isDirty",
@@ -21,95 +21,123 @@ const R = [
21
21
  "setError",
22
22
  "setData"
23
23
  ];
24
- function C(c, h) {
24
+ function X(n, D) {
25
25
  const {
26
- debounce: s = 3e3,
27
- skipFields: I = [],
28
- skipInertiaFields: T = !0,
29
- deep: O = !0,
30
- debug: n = !1,
31
- serialize: y = JSON.stringify,
32
- compare: o,
33
- saveOnInit: b = !1,
34
- onSave: W,
35
- onBeforeSave: m,
36
- onAfterSave: g,
37
- onError: l
38
- } = h, u = F(!1), a = F(!0);
39
- let r = () => {
40
- }, i = () => {
41
- };
42
- const z = (t = 1e3) => {
43
- a.value = !1, r(), i(), setTimeout(() => {
44
- a.value = !0;
45
- }, t);
46
- }, j = (t = null) => {
47
- if (a.value = !0, r(), i(), t === null)
48
- f(), o ? v = null : d = null, p();
26
+ debounce: u = 3e3,
27
+ minSaveInterval: I = 2e3,
28
+ trackLastSaved: S = !0,
29
+ skipFields: J = [],
30
+ skipInertiaFields: N = !0,
31
+ deep: R = !0,
32
+ debug: a = !1,
33
+ serialize: f = JSON.stringify,
34
+ compare: s,
35
+ saveOnInit: p = !1,
36
+ onSave: q,
37
+ onBeforeSave: T,
38
+ onAfterSave: w,
39
+ onSaveSuccess: F,
40
+ onError: c
41
+ } = D, g = x(!1), m = x(!0);
42
+ let b = null, h = null, O = 0, k = () => {
43
+ }, A = () => {
44
+ }, o = !1;
45
+ const z = (e = 1e3) => {
46
+ m.value = !1, k(), A(), setTimeout(() => {
47
+ m.value = !0;
48
+ }, e);
49
+ }, C = (e = null) => {
50
+ if (m.value = !0, k(), A(), e === null)
51
+ o = !0, s ? r = null : i = null, y(), o = !1;
49
52
  else {
50
- const e = A(p, t);
51
- i = e.cancel, e.call();
53
+ const l = P(y, e);
54
+ A = l.cancel, l.call();
52
55
  }
53
- }, f = () => {
54
- const t = N(c) ? P(c) : c, e = {};
55
- for (const S of Object.keys(t))
56
- T && R.includes(S) || I.includes(S) || (e[S] = t[S]);
57
- return e;
56
+ }, v = () => {
57
+ const e = M(n) ? Q(n) : n, l = {};
58
+ for (const t of Object.keys(e))
59
+ N && U.includes(t) || J.includes(t) || (l[t] = e[t]);
60
+ return l;
58
61
  };
59
- let d = b ? null : y(f()), v = o ? b ? null : f() : null;
60
- const p = () => {
61
- if (!a.value) return;
62
- const t = f();
63
- if (o) {
64
- if (v && o(v, t)) return;
65
- v = t;
62
+ let i = p ? null : f(v()), r = s ? p ? null : v() : null;
63
+ if (S && !p) {
64
+ const e = v();
65
+ s ? h = { ...e } : b = f(e);
66
+ }
67
+ const E = (e) => {
68
+ S && (s ? h = { ...e } : b = f(e));
69
+ }, y = () => {
70
+ if (!m.value && !o) return;
71
+ const e = Date.now();
72
+ if (!o && I > 0 && e - O < I) {
73
+ a && console.log("[AutoSave] Skipping save - too soon after last save");
74
+ return;
75
+ }
76
+ const l = v();
77
+ if (o)
78
+ s ? r = l : i = f(l);
79
+ else if (s) {
80
+ if (S && h && s(h, l)) {
81
+ a && console.log("[AutoSave] Skipping save - identical to last saved state"), r = l;
82
+ return;
83
+ }
84
+ if (r && s(r, l)) return;
85
+ r = l;
66
86
  } else {
67
- const e = y(t);
68
- if (d !== null && e === d) return;
69
- d = e;
87
+ const t = f(l);
88
+ if (S && b !== null && t === b) {
89
+ a && console.log("[AutoSave] Skipping save - identical to last saved state"), i = t;
90
+ return;
91
+ }
92
+ if (i !== null && t === i) return;
93
+ i = t;
70
94
  }
71
- n && console.log("[AutoSave] Detected changes. Saving..."), u.value = !0;
95
+ a && console.log("[AutoSave] Detected changes. Saving..."), g.value = !0, O = e;
72
96
  try {
73
- m == null || m(), Promise.resolve(W()).then(() => {
74
- g == null || g(), n && console.log("[AutoSave] Save successful.");
75
- }).catch((e) => {
76
- l == null || l(e), n && console.error("[AutoSave] Save failed:", e);
97
+ T == null || T();
98
+ const t = Promise.resolve(q()).then(async () => {
99
+ await L(), await L();
100
+ const d = v();
101
+ E(d), F == null || F(d), w == null || w(), a && console.log("[AutoSave] Save successful.");
102
+ }).catch((d) => {
103
+ c == null || c(d), a && console.error("[AutoSave] Save failed:", d);
77
104
  }).finally(() => {
78
- u.value = !1;
105
+ g.value = !1;
79
106
  });
80
- } catch (e) {
81
- l == null || l(e), n && console.error("[AutoSave] Immediate error:", e), u.value = !1;
107
+ return o || z(5e3), t;
108
+ } catch (t) {
109
+ c == null || c(t), a && console.error("[AutoSave] Immediate error:", t), g.value = !1;
82
110
  }
83
- }, D = A(p, s), w = D.call;
84
- r = D.cancel;
85
- const k = x(
86
- c,
87
- w,
111
+ }, W = P(y, u), G = W.call;
112
+ k = W.cancel;
113
+ const j = H(
114
+ n,
115
+ G,
88
116
  {
89
- deep: O,
117
+ deep: R,
90
118
  flush: "post"
91
119
  }
92
120
  );
93
- return J(() => {
94
- k(), r(), i();
95
- }), b && p(), {
96
- isAutoSaving: u,
121
+ return K(() => {
122
+ j(), k(), A();
123
+ }), p && y(), {
124
+ isAutoSaving: g,
97
125
  blockWatcher: z,
98
- unblockWatcher: j,
99
- stop: k
126
+ unblockWatcher: C,
127
+ stop: j
100
128
  };
101
129
  }
102
- function A(c, h) {
103
- let s;
130
+ function P(n, D) {
131
+ let u;
104
132
  return {
105
133
  call: () => {
106
- clearTimeout(s), s = setTimeout(() => c(), h);
134
+ clearTimeout(u), u = setTimeout(() => n(), D);
107
135
  },
108
136
  cancel: () => {
109
- clearTimeout(s);
137
+ clearTimeout(u);
110
138
  }
111
139
  };
112
140
  }
113
141
  export {
114
- C as useAutoSaveForm
142
+ X as useAutoSaveForm
115
143
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@provydon/vue-auto-save",
3
- "version": "1.1.1",
3
+ "version": "1.3.1",
4
4
  "description": "A Vue 3 composable that autosaves forms with debounce, optional field skipping, and blockable watchers.",
5
5
  "main": "./dist/useAutoSaveForm.cjs",
6
6
  "module": "./dist/useAutoSaveForm.mjs",