@vuehookform/core 0.1.2 → 0.2.7

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.
@@ -21,9 +21,14 @@ function set(obj, path, value) {
21
21
  if (keys.some((k) => UNSAFE_KEYS.includes(k))) return;
22
22
  const lastKey = keys.pop();
23
23
  let current = obj;
24
- for (const key of keys) {
25
- if (!(key in current) || typeof current[key] !== "object") {
26
- const nextKey = keys[keys.indexOf(key) + 1];
24
+ for (let i = 0; i < keys.length; i++) {
25
+ const key = keys[i];
26
+ const existing = current[key];
27
+ if (existing !== void 0 && existing !== null && typeof existing !== "object") try {
28
+ if (globalThis.process?.env?.NODE_ENV !== "production") console.warn(`[vue-hook-form] set(): Overwriting primitive value at path "${keys.slice(0, i + 1).join(".")}" with an object. Previous value: ${JSON.stringify(existing)}`);
29
+ } catch {}
30
+ if (!(key in current) || typeof current[key] !== "object" || current[key] === null) {
31
+ const nextKey = keys[i + 1];
27
32
  current[key] = nextKey && /^\d+$/.test(nextKey) ? [] : {};
28
33
  }
29
34
  current = current[key];
@@ -37,13 +42,16 @@ function unset(obj, path) {
37
42
  let current = obj;
38
43
  for (const key of keys) {
39
44
  if (!(key in current)) return;
40
- current = current[key];
45
+ const next = current[key];
46
+ if (next === null || typeof next !== "object") return;
47
+ current = next;
41
48
  }
42
49
  delete current[lastKey];
43
50
  }
44
51
  var idCounter = 0;
45
52
  function generateId() {
46
- return `field_${Date.now()}_${idCounter++}`;
53
+ const random = Math.random().toString(36).substring(2, 11);
54
+ return `field_${Date.now()}_${idCounter++}_${random}`;
47
55
  }
48
56
  function createFormContext(options) {
49
57
  const formData = (0, vue.reactive)({});
@@ -58,27 +66,81 @@ function createFormContext(options) {
58
66
  isLoading.value = false;
59
67
  }).catch((error) => {
60
68
  console.error("Failed to load async default values:", error);
69
+ defaultValuesError.value = error;
61
70
  isLoading.value = false;
71
+ options.onDefaultValuesError?.(error);
62
72
  });
63
73
  } else if (options.defaultValues) {
64
74
  Object.assign(defaultValues, options.defaultValues);
65
75
  Object.assign(formData, defaultValues);
66
76
  }
77
+ const errors = (0, vue.shallowRef)({});
78
+ const touchedFields = (0, vue.shallowRef)({});
79
+ const dirtyFields = (0, vue.shallowRef)({});
80
+ const isSubmitting = (0, vue.ref)(false);
81
+ const submitCount = (0, vue.ref)(0);
82
+ const defaultValuesError = (0, vue.ref)(null);
83
+ const isSubmitSuccessful = (0, vue.ref)(false);
84
+ const validatingFields = (0, vue.shallowRef)({});
85
+ const externalErrors = (0, vue.shallowRef)({});
86
+ const errorDelayTimers = /* @__PURE__ */ new Map();
87
+ const pendingErrors = /* @__PURE__ */ new Map();
88
+ const fieldRefs = /* @__PURE__ */ new Map();
89
+ const fieldOptions = /* @__PURE__ */ new Map();
90
+ const fieldArrays = /* @__PURE__ */ new Map();
91
+ const fieldHandlers = /* @__PURE__ */ new Map();
92
+ const debounceTimers = /* @__PURE__ */ new Map();
93
+ const validationRequestIds = /* @__PURE__ */ new Map();
94
+ const resetGeneration = (0, vue.ref)(0);
95
+ if (options.values !== void 0) {
96
+ const initialValues = (0, vue.toValue)(options.values);
97
+ if (initialValues && !isAsyncDefaults) {
98
+ for (const [key, value] of Object.entries(initialValues)) if (value !== void 0) set(formData, key, value);
99
+ }
100
+ (0, vue.watch)(() => (0, vue.toValue)(options.values), (newValues) => {
101
+ if (newValues) {
102
+ for (const [key, value] of Object.entries(newValues)) if (value !== void 0) {
103
+ set(formData, key, value);
104
+ const fieldRef = fieldRefs.get(key);
105
+ const opts = fieldOptions.get(key);
106
+ if (fieldRef?.value && !opts?.controlled) {
107
+ const el = fieldRef.value;
108
+ if (el.type === "checkbox") el.checked = value;
109
+ else el.value = value;
110
+ }
111
+ }
112
+ }
113
+ }, { deep: true });
114
+ }
115
+ if (options.errors !== void 0) {
116
+ const initialErrors = (0, vue.toValue)(options.errors);
117
+ if (initialErrors) externalErrors.value = initialErrors;
118
+ (0, vue.watch)(() => (0, vue.toValue)(options.errors), (newErrors) => {
119
+ externalErrors.value = newErrors || {};
120
+ }, { deep: true });
121
+ }
67
122
  return {
68
123
  formData,
69
124
  defaultValues,
70
- errors: (0, vue.shallowRef)({}),
71
- touchedFields: (0, vue.shallowRef)({}),
72
- dirtyFields: (0, vue.shallowRef)({}),
73
- isSubmitting: (0, vue.ref)(false),
125
+ errors,
126
+ touchedFields,
127
+ dirtyFields,
128
+ isSubmitting,
74
129
  isLoading,
75
- submitCount: (0, vue.ref)(0),
76
- fieldRefs: /* @__PURE__ */ new Map(),
77
- fieldOptions: /* @__PURE__ */ new Map(),
78
- fieldArrays: /* @__PURE__ */ new Map(),
79
- fieldHandlers: /* @__PURE__ */ new Map(),
80
- debounceTimers: /* @__PURE__ */ new Map(),
81
- validationRequestIds: /* @__PURE__ */ new Map(),
130
+ submitCount,
131
+ defaultValuesError,
132
+ isSubmitSuccessful,
133
+ validatingFields,
134
+ externalErrors,
135
+ errorDelayTimers,
136
+ pendingErrors,
137
+ fieldRefs,
138
+ fieldOptions,
139
+ fieldArrays,
140
+ fieldHandlers,
141
+ debounceTimers,
142
+ validationRequestIds,
143
+ resetGeneration,
82
144
  options
83
145
  };
84
146
  }
@@ -87,6 +149,17 @@ function clearFieldErrors(errors, fieldPath) {
87
149
  for (const key of Object.keys(newErrors)) if (key === fieldPath || key.startsWith(`${fieldPath}.`)) delete newErrors[key];
88
150
  return newErrors;
89
151
  }
152
+ function setValidating(ctx, fieldPath, isValidating) {
153
+ if (isValidating) ctx.validatingFields.value = {
154
+ ...ctx.validatingFields.value,
155
+ [fieldPath]: true
156
+ };
157
+ else {
158
+ const newValidating = { ...ctx.validatingFields.value };
159
+ delete newValidating[fieldPath];
160
+ ctx.validatingFields.value = newValidating;
161
+ }
162
+ }
90
163
  function groupErrorsByPath(issues) {
91
164
  const grouped = /* @__PURE__ */ new Map();
92
165
  for (const issue of issues) {
@@ -100,9 +173,10 @@ function groupErrorsByPath(issues) {
100
173
  }
101
174
  return grouped;
102
175
  }
103
- function createFieldError(errors) {
176
+ function createFieldError(errors, criteriaMode = "firstError") {
104
177
  const firstError = errors[0];
105
178
  if (!firstError) return "";
179
+ if (criteriaMode === "firstError") return firstError.message;
106
180
  if (errors.length === 1) return firstError.message;
107
181
  const types = {};
108
182
  for (const err of errors) {
@@ -117,37 +191,89 @@ function createFieldError(errors) {
117
191
  };
118
192
  }
119
193
  function createValidation(ctx) {
120
- async function validate(fieldPath) {
121
- const result = await ctx.options.schema.safeParseAsync(ctx.formData);
122
- if (result.success) {
123
- if (fieldPath) ctx.errors.value = clearFieldErrors(ctx.errors.value, fieldPath);
124
- else ctx.errors.value = {};
125
- return true;
194
+ function scheduleError(fieldPath, error) {
195
+ const delayMs = ctx.options.delayError || 0;
196
+ if (delayMs <= 0) {
197
+ const newErrors = { ...ctx.errors.value };
198
+ set(newErrors, fieldPath, error);
199
+ ctx.errors.value = newErrors;
200
+ return;
126
201
  }
127
- const zodErrors = result.error.issues;
128
- if (fieldPath) {
129
- const fieldErrors = zodErrors.filter((issue) => {
130
- const path = issue.path.join(".");
131
- return path === fieldPath || path.startsWith(`${fieldPath}.`);
132
- });
133
- if (fieldErrors.length === 0) {
134
- ctx.errors.value = clearFieldErrors(ctx.errors.value, fieldPath);
202
+ const existingTimer = ctx.errorDelayTimers.get(fieldPath);
203
+ if (existingTimer) clearTimeout(existingTimer);
204
+ ctx.pendingErrors.set(fieldPath, error);
205
+ const timer = setTimeout(() => {
206
+ ctx.errorDelayTimers.delete(fieldPath);
207
+ const pendingError = ctx.pendingErrors.get(fieldPath);
208
+ if (pendingError !== void 0) {
209
+ ctx.pendingErrors.delete(fieldPath);
210
+ const newErrors = { ...ctx.errors.value };
211
+ set(newErrors, fieldPath, pendingError);
212
+ ctx.errors.value = newErrors;
213
+ }
214
+ }, delayMs);
215
+ ctx.errorDelayTimers.set(fieldPath, timer);
216
+ }
217
+ function cancelError(fieldPath) {
218
+ const timer = ctx.errorDelayTimers.get(fieldPath);
219
+ if (timer) {
220
+ clearTimeout(timer);
221
+ ctx.errorDelayTimers.delete(fieldPath);
222
+ }
223
+ ctx.pendingErrors.delete(fieldPath);
224
+ return clearFieldErrors(ctx.errors.value, fieldPath);
225
+ }
226
+ function clearAllPendingErrors() {
227
+ for (const timer of ctx.errorDelayTimers.values()) clearTimeout(timer);
228
+ ctx.errorDelayTimers.clear();
229
+ ctx.pendingErrors.clear();
230
+ }
231
+ async function validate(fieldPath) {
232
+ const generationAtStart = ctx.resetGeneration.value;
233
+ const criteriaMode = ctx.options.criteriaMode || "firstError";
234
+ const validatingKey = fieldPath || "_form";
235
+ setValidating(ctx, validatingKey, true);
236
+ try {
237
+ const result = await ctx.options.schema.safeParseAsync(ctx.formData);
238
+ if (ctx.resetGeneration.value !== generationAtStart) return true;
239
+ if (result.success) {
240
+ if (fieldPath) ctx.errors.value = cancelError(fieldPath);
241
+ else {
242
+ clearAllPendingErrors();
243
+ ctx.errors.value = {};
244
+ }
135
245
  return true;
136
246
  }
137
- const newErrors$1 = clearFieldErrors(ctx.errors.value, fieldPath);
138
- const grouped$1 = groupErrorsByPath(fieldErrors);
139
- for (const [path, errors] of grouped$1) set(newErrors$1, path, createFieldError(errors));
140
- ctx.errors.value = newErrors$1;
247
+ const zodErrors = result.error.issues;
248
+ if (fieldPath) {
249
+ const fieldErrors = zodErrors.filter((issue) => {
250
+ const path = issue.path.join(".");
251
+ return path === fieldPath || path.startsWith(`${fieldPath}.`);
252
+ });
253
+ if (fieldErrors.length === 0) {
254
+ ctx.errors.value = cancelError(fieldPath);
255
+ return true;
256
+ }
257
+ ctx.errors.value = cancelError(fieldPath);
258
+ const grouped$1 = groupErrorsByPath(fieldErrors);
259
+ for (const [path, errors] of grouped$1) scheduleError(path, createFieldError(errors, criteriaMode));
260
+ return false;
261
+ }
262
+ clearAllPendingErrors();
263
+ ctx.errors.value = {};
264
+ const grouped = groupErrorsByPath(zodErrors);
265
+ for (const [path, errors] of grouped) scheduleError(path, createFieldError(errors, criteriaMode));
141
266
  return false;
267
+ } finally {
268
+ setValidating(ctx, validatingKey, false);
142
269
  }
143
- const newErrors = {};
144
- const grouped = groupErrorsByPath(zodErrors);
145
- for (const [path, errors] of grouped) set(newErrors, path, createFieldError(errors));
146
- ctx.errors.value = newErrors;
147
- return false;
148
270
  }
149
- return { validate };
271
+ return {
272
+ validate,
273
+ clearAllPendingErrors
274
+ };
150
275
  }
276
+ var validationRequestCounter = 0;
151
277
  function createFieldRegistration(ctx, validate) {
152
278
  function register(name, registerOptions) {
153
279
  let fieldRef = ctx.fieldRefs.get(name);
@@ -162,11 +288,12 @@ function createFieldRegistration(ctx, validate) {
162
288
  if (registerOptions) ctx.fieldOptions.set(name, registerOptions);
163
289
  let handlers = ctx.fieldHandlers.get(name);
164
290
  if (!handlers) {
165
- const runCustomValidation = async (fieldName, value, requestId) => {
291
+ const runCustomValidation = async (fieldName, value, requestId, resetGenAtStart) => {
166
292
  const fieldOpts = ctx.fieldOptions.get(fieldName);
167
293
  if (!fieldOpts?.validate || fieldOpts.disabled) return;
168
294
  const error = await fieldOpts.validate(value);
169
295
  if (requestId !== ctx.validationRequestIds.get(fieldName)) return;
296
+ if (ctx.resetGeneration.value !== resetGenAtStart) return;
170
297
  if (error) ctx.errors.value = {
171
298
  ...ctx.errors.value,
172
299
  [fieldName]: error
@@ -185,21 +312,26 @@ function createFieldRegistration(ctx, validate) {
185
312
  ...ctx.dirtyFields.value,
186
313
  [name]: true
187
314
  };
188
- if (ctx.options.mode === "onChange" || ctx.options.mode === "onTouched" && ctx.touchedFields.value[name] || ctx.touchedFields.value[name] && ctx.options.reValidateMode === "onChange") await validate(name);
315
+ if (ctx.options.mode === "onChange" || ctx.options.mode === "onTouched" && ctx.touchedFields.value[name] || ctx.touchedFields.value[name] && ctx.options.reValidateMode === "onChange") {
316
+ await validate(name);
317
+ const fieldOpts$1 = ctx.fieldOptions.get(name);
318
+ if (fieldOpts$1?.deps && fieldOpts$1.deps.length > 0) for (const depField of fieldOpts$1.deps) validate(depField);
319
+ }
189
320
  const fieldOpts = ctx.fieldOptions.get(name);
190
321
  if (fieldOpts?.validate && !fieldOpts.disabled) {
191
- const requestId = Date.now() + Math.random();
322
+ const requestId = ++validationRequestCounter;
192
323
  ctx.validationRequestIds.set(name, requestId);
324
+ const resetGenAtStart = ctx.resetGeneration.value;
193
325
  const debounceMs = fieldOpts.validateDebounce || 0;
194
326
  if (debounceMs > 0) {
195
327
  const existingTimer = ctx.debounceTimers.get(name);
196
328
  if (existingTimer) clearTimeout(existingTimer);
197
329
  const timer = setTimeout(() => {
198
330
  ctx.debounceTimers.delete(name);
199
- runCustomValidation(name, value, requestId);
331
+ runCustomValidation(name, value, requestId, resetGenAtStart);
200
332
  }, debounceMs);
201
333
  ctx.debounceTimers.set(name, timer);
202
- } else await runCustomValidation(name, value, requestId);
334
+ } else await runCustomValidation(name, value, requestId, resetGenAtStart);
203
335
  }
204
336
  };
205
337
  const onBlur = async (_e) => {
@@ -207,12 +339,18 @@ function createFieldRegistration(ctx, validate) {
207
339
  ...ctx.touchedFields.value,
208
340
  [name]: true
209
341
  };
210
- if (ctx.options.mode === "onBlur" || ctx.options.mode === "onTouched" || ctx.submitCount.value > 0 && ctx.options.reValidateMode === "onBlur") await validate(name);
342
+ if (ctx.options.mode === "onBlur" || ctx.options.mode === "onTouched" || ctx.submitCount.value > 0 && ctx.options.reValidateMode === "onBlur") {
343
+ await validate(name);
344
+ const fieldOpts = ctx.fieldOptions.get(name);
345
+ if (fieldOpts?.deps && fieldOpts.deps.length > 0) for (const depField of fieldOpts.deps) validate(depField);
346
+ }
211
347
  };
212
348
  const refCallback = (el) => {
213
349
  const currentFieldRef = ctx.fieldRefs.get(name);
214
350
  if (!currentFieldRef) return;
215
351
  const previousEl = currentFieldRef.value;
352
+ if (previousEl === el) return;
353
+ if (previousEl && el) return;
216
354
  currentFieldRef.value = el;
217
355
  const opts = ctx.fieldOptions.get(name);
218
356
  if (el && !opts?.controlled && el instanceof HTMLInputElement) {
@@ -221,6 +359,12 @@ function createFieldRegistration(ctx, validate) {
221
359
  else el.value = value;
222
360
  }
223
361
  if (previousEl && !el) {
362
+ const timer = ctx.debounceTimers.get(name);
363
+ if (timer) {
364
+ clearTimeout(timer);
365
+ ctx.debounceTimers.delete(name);
366
+ }
367
+ ctx.validationRequestIds.delete(name);
224
368
  if (opts?.shouldUnregister ?? ctx.options.shouldUnregister ?? false) {
225
369
  unset(ctx.formData, name);
226
370
  const newErrors = { ...ctx.errors.value };
@@ -235,12 +379,6 @@ function createFieldRegistration(ctx, validate) {
235
379
  ctx.fieldRefs.delete(name);
236
380
  ctx.fieldOptions.delete(name);
237
381
  ctx.fieldHandlers.delete(name);
238
- const timer = ctx.debounceTimers.get(name);
239
- if (timer) {
240
- clearTimeout(timer);
241
- ctx.debounceTimers.delete(name);
242
- }
243
- ctx.validationRequestIds.delete(name);
244
382
  }
245
383
  }
246
384
  };
@@ -268,7 +406,24 @@ function createFieldRegistration(ctx, validate) {
268
406
  }) }
269
407
  };
270
408
  }
271
- function unregister(name) {
409
+ function unregister(name, options) {
410
+ const opts = options || {};
411
+ if (!opts.keepValue) unset(ctx.formData, name);
412
+ if (!opts.keepError) {
413
+ const newErrors = { ...ctx.errors.value };
414
+ delete newErrors[name];
415
+ ctx.errors.value = newErrors;
416
+ }
417
+ if (!opts.keepTouched) {
418
+ const newTouched = { ...ctx.touchedFields.value };
419
+ delete newTouched[name];
420
+ ctx.touchedFields.value = newTouched;
421
+ }
422
+ if (!opts.keepDirty) {
423
+ const newDirty = { ...ctx.dirtyFields.value };
424
+ delete newDirty[name];
425
+ ctx.dirtyFields.value = newDirty;
426
+ }
272
427
  ctx.fieldRefs.delete(name);
273
428
  ctx.fieldOptions.delete(name);
274
429
  ctx.fieldHandlers.delete(name);
@@ -284,42 +439,89 @@ function createFieldRegistration(ctx, validate) {
284
439
  unregister
285
440
  };
286
441
  }
287
- function createFieldArrayManager(ctx, validate) {
288
- function fields(name) {
442
+ function createFieldArrayManager(ctx, validate, setFocus) {
443
+ function fields(name, options) {
289
444
  let fieldArray = ctx.fieldArrays.get(name);
290
445
  if (!fieldArray) {
291
446
  const existingValues = get(ctx.formData, name) || [];
292
447
  fieldArray = {
293
448
  items: (0, vue.ref)([]),
294
- values: existingValues
449
+ values: existingValues,
450
+ indexCache: /* @__PURE__ */ new Map(),
451
+ rules: options?.rules
295
452
  };
296
453
  ctx.fieldArrays.set(name, fieldArray);
297
454
  if (!get(ctx.formData, name)) set(ctx.formData, name, []);
298
- }
455
+ } else if (options?.rules) fieldArray.rules = options.rules;
299
456
  const fa = fieldArray;
457
+ const indexCache = fa.indexCache;
458
+ const rebuildIndexCache = () => {
459
+ indexCache.clear();
460
+ fa.items.value.forEach((item, idx) => {
461
+ indexCache.set(item.key, idx);
462
+ });
463
+ };
300
464
  const createItem = (key) => ({
301
465
  key,
302
466
  get index() {
303
- return fa.items.value.findIndex((item) => item.key === key);
467
+ return indexCache.get(key) ?? -1;
304
468
  },
305
469
  remove() {
306
- const currentIndex = fa.items.value.findIndex((item) => item.key === key);
470
+ const currentIndex = indexCache.get(key) ?? -1;
307
471
  if (currentIndex !== -1) removeAt(currentIndex);
308
472
  }
309
473
  });
310
- if (fa.items.value.length === 0 && fa.values.length > 0) fa.items.value = fa.values.map(() => createItem(generateId()));
311
- const append = (value) => {
312
- const newValues = [...get(ctx.formData, name) || [], value];
474
+ if (fa.items.value.length === 0 && fa.values.length > 0) {
475
+ fa.items.value = fa.values.map(() => createItem(generateId()));
476
+ rebuildIndexCache();
477
+ }
478
+ const handleFocus = async (baseIndex, addedCount, focusOptions) => {
479
+ if (!focusOptions?.shouldFocus) return;
480
+ await (0, vue.nextTick)();
481
+ const focusItemOffset = focusOptions?.focusIndex ?? 0;
482
+ let fieldPath = `${name}.${baseIndex + Math.min(focusItemOffset, addedCount - 1)}`;
483
+ if (focusOptions?.focusName) fieldPath = `${fieldPath}.${focusOptions.focusName}`;
484
+ setFocus(fieldPath);
485
+ };
486
+ const normalizeToArray = (value) => {
487
+ return Array.isArray(value) ? value : [value];
488
+ };
489
+ const append = (value, focusOptions) => {
490
+ const values = normalizeToArray(value);
491
+ if (values.length === 0) return;
492
+ const currentValues = get(ctx.formData, name) || [];
493
+ const insertIndex = currentValues.length;
494
+ const rules = fa.rules;
495
+ if (rules?.maxLength && currentValues.length + values.length > rules.maxLength.value) return;
496
+ const newValues = [...currentValues, ...values];
313
497
  set(ctx.formData, name, newValues);
314
- fa.items.value = [...fa.items.value, createItem(generateId())];
498
+ const newItems = values.map(() => createItem(generateId()));
499
+ fa.items.value = [...fa.items.value, ...newItems];
500
+ rebuildIndexCache();
315
501
  ctx.dirtyFields.value = {
316
502
  ...ctx.dirtyFields.value,
317
503
  [name]: true
318
504
  };
319
505
  if (ctx.options.mode === "onChange") validate(name);
506
+ handleFocus(insertIndex, values.length, focusOptions);
320
507
  };
321
- const prepend = (value) => {
322
- insert(0, value);
508
+ const prepend = (value, focusOptions) => {
509
+ const values = normalizeToArray(value);
510
+ if (values.length === 0) return;
511
+ const currentValues = get(ctx.formData, name) || [];
512
+ const rules = fa.rules;
513
+ if (rules?.maxLength && currentValues.length + values.length > rules.maxLength.value) return;
514
+ const newValues = [...values, ...currentValues];
515
+ set(ctx.formData, name, newValues);
516
+ const newItems = values.map(() => createItem(generateId()));
517
+ fa.items.value = [...newItems, ...fa.items.value];
518
+ rebuildIndexCache();
519
+ ctx.dirtyFields.value = {
520
+ ...ctx.dirtyFields.value,
521
+ [name]: true
522
+ };
523
+ if (ctx.options.mode === "onChange") validate(name);
524
+ handleFocus(0, values.length, focusOptions);
323
525
  };
324
526
  const update = (index, value) => {
325
527
  const currentValues = get(ctx.formData, name) || [];
@@ -334,38 +536,52 @@ function createFieldArrayManager(ctx, validate) {
334
536
  if (ctx.options.mode === "onChange") validate(name);
335
537
  };
336
538
  const removeAt = (index) => {
337
- const newValues = (get(ctx.formData, name) || []).filter((_, i) => i !== index);
539
+ const currentValues = get(ctx.formData, name) || [];
540
+ if (index < 0 || index >= currentValues.length) return;
541
+ const rules = fa.rules;
542
+ if (rules?.minLength && currentValues.length - 1 < rules.minLength.value) return;
543
+ const newValues = currentValues.filter((_, i) => i !== index);
338
544
  set(ctx.formData, name, newValues);
339
545
  const keyToRemove = fa.items.value[index]?.key;
340
546
  fa.items.value = fa.items.value.filter((item) => item.key !== keyToRemove);
547
+ rebuildIndexCache();
341
548
  ctx.dirtyFields.value = {
342
549
  ...ctx.dirtyFields.value,
343
550
  [name]: true
344
551
  };
345
552
  if (ctx.options.mode === "onChange") validate(name);
346
553
  };
347
- const insert = (index, value) => {
554
+ const insert = (index, value, focusOptions) => {
555
+ const values = normalizeToArray(value);
556
+ if (values.length === 0) return;
348
557
  const currentValues = get(ctx.formData, name) || [];
558
+ const rules = fa.rules;
559
+ if (rules?.maxLength && currentValues.length + values.length > rules.maxLength.value) return;
560
+ const clampedIndex = Math.max(0, Math.min(index, currentValues.length));
349
561
  const newValues = [
350
- ...currentValues.slice(0, index),
351
- value,
352
- ...currentValues.slice(index)
562
+ ...currentValues.slice(0, clampedIndex),
563
+ ...values,
564
+ ...currentValues.slice(clampedIndex)
353
565
  ];
354
566
  set(ctx.formData, name, newValues);
355
- const newItem = createItem(generateId());
567
+ const newItems = values.map(() => createItem(generateId()));
356
568
  fa.items.value = [
357
- ...fa.items.value.slice(0, index),
358
- newItem,
359
- ...fa.items.value.slice(index)
569
+ ...fa.items.value.slice(0, clampedIndex),
570
+ ...newItems,
571
+ ...fa.items.value.slice(clampedIndex)
360
572
  ];
573
+ rebuildIndexCache();
361
574
  ctx.dirtyFields.value = {
362
575
  ...ctx.dirtyFields.value,
363
576
  [name]: true
364
577
  };
365
578
  if (ctx.options.mode === "onChange") validate(name);
579
+ handleFocus(clampedIndex, values.length, focusOptions);
366
580
  };
367
581
  const swap = (indexA, indexB) => {
368
- const newValues = [...get(ctx.formData, name) || []];
582
+ const currentValues = get(ctx.formData, name) || [];
583
+ if (indexA < 0 || indexB < 0 || indexA >= currentValues.length || indexB >= currentValues.length) return;
584
+ const newValues = [...currentValues];
369
585
  [newValues[indexA], newValues[indexB]] = [newValues[indexB], newValues[indexA]];
370
586
  set(ctx.formData, name, newValues);
371
587
  const newItems = [...fa.items.value];
@@ -375,6 +591,7 @@ function createFieldArrayManager(ctx, validate) {
375
591
  newItems[indexA] = itemB;
376
592
  newItems[indexB] = itemA;
377
593
  fa.items.value = newItems;
594
+ rebuildIndexCache();
378
595
  }
379
596
  ctx.dirtyFields.value = {
380
597
  ...ctx.dirtyFields.value,
@@ -383,17 +600,22 @@ function createFieldArrayManager(ctx, validate) {
383
600
  if (ctx.options.mode === "onChange") validate(name);
384
601
  };
385
602
  const move = (from, to) => {
386
- const newValues = [...get(ctx.formData, name) || []];
603
+ const currentValues = get(ctx.formData, name) || [];
604
+ if (from < 0 || from >= currentValues.length || to < 0) return;
605
+ const newValues = [...currentValues];
387
606
  const [removed] = newValues.splice(from, 1);
388
607
  if (removed !== void 0) {
389
- newValues.splice(to, 0, removed);
608
+ const clampedTo = Math.min(to, newValues.length);
609
+ newValues.splice(clampedTo, 0, removed);
390
610
  set(ctx.formData, name, newValues);
391
611
  }
392
612
  const newItems = [...fa.items.value];
393
613
  const [removedItem] = newItems.splice(from, 1);
394
614
  if (removedItem) {
395
- newItems.splice(to, 0, removedItem);
615
+ const clampedTo = Math.min(to, newItems.length);
616
+ newItems.splice(clampedTo, 0, removedItem);
396
617
  fa.items.value = newItems;
618
+ rebuildIndexCache();
397
619
  }
398
620
  ctx.dirtyFields.value = {
399
621
  ...ctx.dirtyFields.value,
@@ -401,6 +623,17 @@ function createFieldArrayManager(ctx, validate) {
401
623
  };
402
624
  if (ctx.options.mode === "onChange") validate(name);
403
625
  };
626
+ const replace = (newValues) => {
627
+ if (!Array.isArray(newValues)) return;
628
+ set(ctx.formData, name, newValues);
629
+ fa.items.value = newValues.map(() => createItem(generateId()));
630
+ rebuildIndexCache();
631
+ ctx.dirtyFields.value = {
632
+ ...ctx.dirtyFields.value,
633
+ [name]: true
634
+ };
635
+ if (ctx.options.mode === "onChange") validate(name);
636
+ };
404
637
  return {
405
638
  value: fa.items.value,
406
639
  append,
@@ -409,31 +642,56 @@ function createFieldArrayManager(ctx, validate) {
409
642
  insert,
410
643
  swap,
411
644
  move,
412
- update
645
+ update,
646
+ replace
413
647
  };
414
648
  }
415
649
  return { fields };
416
650
  }
417
651
  function useForm(options) {
418
652
  const ctx = createFormContext(options);
419
- const { validate } = createValidation(ctx);
653
+ const { validate, clearAllPendingErrors } = createValidation(ctx);
420
654
  const { register, unregister } = createFieldRegistration(ctx, validate);
421
- const { fields } = createFieldArrayManager(ctx, validate);
422
- const formState = (0, vue.computed)(() => ({
423
- errors: ctx.errors.value,
424
- isDirty: Object.keys(ctx.dirtyFields.value).some((k) => ctx.dirtyFields.value[k]),
425
- dirtyFields: ctx.dirtyFields.value,
426
- isValid: (ctx.submitCount.value > 0 || Object.keys(ctx.touchedFields.value).length > 0) && Object.keys(ctx.errors.value).length === 0,
427
- isSubmitting: ctx.isSubmitting.value,
428
- isLoading: ctx.isLoading.value,
429
- touchedFields: ctx.touchedFields.value,
430
- submitCount: ctx.submitCount.value
431
- }));
655
+ function setFocus(name, focusOptions) {
656
+ const fieldRef = ctx.fieldRefs.get(name);
657
+ if (!fieldRef?.value) return;
658
+ const el = fieldRef.value;
659
+ if (typeof el.focus === "function") {
660
+ el.focus();
661
+ if (focusOptions?.shouldSelect && el instanceof HTMLInputElement && typeof el.select === "function") el.select();
662
+ }
663
+ }
664
+ const setFocusWrapper = (name) => setFocus(name);
665
+ const { fields } = createFieldArrayManager(ctx, validate, setFocusWrapper);
666
+ const formState = (0, vue.computed)(() => {
667
+ const mergedErrors = {
668
+ ...ctx.errors.value,
669
+ ...ctx.externalErrors.value
670
+ };
671
+ return {
672
+ errors: mergedErrors,
673
+ isDirty: Object.keys(ctx.dirtyFields.value).some((k) => ctx.dirtyFields.value[k]),
674
+ dirtyFields: ctx.dirtyFields.value,
675
+ isValid: (ctx.submitCount.value > 0 || Object.keys(ctx.touchedFields.value).length > 0) && Object.keys(mergedErrors).length === 0,
676
+ isSubmitting: ctx.isSubmitting.value,
677
+ isLoading: ctx.isLoading.value,
678
+ isReady: !ctx.isLoading.value,
679
+ isValidating: Object.keys(ctx.validatingFields.value).some((k) => ctx.validatingFields.value[k]),
680
+ validatingFields: ctx.validatingFields.value,
681
+ touchedFields: ctx.touchedFields.value,
682
+ submitCount: ctx.submitCount.value,
683
+ defaultValuesError: ctx.defaultValuesError.value,
684
+ isSubmitted: ctx.submitCount.value > 0,
685
+ isSubmitSuccessful: ctx.isSubmitSuccessful.value
686
+ };
687
+ });
432
688
  function handleSubmit(onValid, onInvalid) {
433
689
  return async (e) => {
434
690
  e.preventDefault();
691
+ if (ctx.isSubmitting.value) return;
435
692
  ctx.isSubmitting.value = true;
436
693
  ctx.submitCount.value++;
694
+ ctx.isSubmitSuccessful.value = false;
437
695
  try {
438
696
  for (const [name, fieldRef] of ctx.fieldRefs.entries()) {
439
697
  const el = fieldRef.value;
@@ -444,41 +702,60 @@ function useForm(options) {
444
702
  }
445
703
  }
446
704
  }
447
- if (await validate()) await onValid(ctx.formData);
448
- else onInvalid?.(ctx.errors.value);
705
+ if (await validate()) {
706
+ await onValid(ctx.formData);
707
+ ctx.isSubmitSuccessful.value = true;
708
+ } else {
709
+ onInvalid?.(formState.value.errors);
710
+ if (options.shouldFocusError !== false) {
711
+ const firstErrorField = Object.keys(formState.value.errors)[0];
712
+ if (firstErrorField) setFocus(firstErrorField);
713
+ }
714
+ }
449
715
  } finally {
450
716
  ctx.isSubmitting.value = false;
451
717
  }
452
718
  };
453
719
  }
454
- function setValue(name, value) {
720
+ function setValue(name, value, setValueOptions) {
455
721
  set(ctx.formData, name, value);
456
- ctx.dirtyFields.value = {
722
+ if (setValueOptions?.shouldDirty !== false) ctx.dirtyFields.value = {
457
723
  ...ctx.dirtyFields.value,
458
724
  [name]: true
459
725
  };
460
- const fieldRef = ctx.fieldRefs.get(name);
461
- if (fieldRef?.value) {
462
- const el = fieldRef.value;
463
- if (el.type === "checkbox") el.checked = value;
464
- else el.value = value;
726
+ if (setValueOptions?.shouldTouch) ctx.touchedFields.value = {
727
+ ...ctx.touchedFields.value,
728
+ [name]: true
729
+ };
730
+ if (!ctx.fieldOptions.get(name)?.controlled) {
731
+ const fieldRef = ctx.fieldRefs.get(name);
732
+ if (fieldRef?.value) {
733
+ const el = fieldRef.value;
734
+ if (el.type === "checkbox") el.checked = value;
735
+ else el.value = value;
736
+ }
465
737
  }
466
- if (options.mode === "onChange" || ctx.touchedFields.value[name]) validate(name);
738
+ if (setValueOptions?.shouldValidate) validate(name);
467
739
  }
468
740
  function getValue(name) {
469
741
  return get(ctx.formData, name);
470
742
  }
471
743
  function reset(values, resetOptions) {
472
744
  const opts = resetOptions || {};
745
+ ctx.resetGeneration.value++;
746
+ clearAllPendingErrors();
747
+ ctx.validatingFields.value = {};
473
748
  if (!opts.keepDefaultValues && values) Object.assign(ctx.defaultValues, values);
474
749
  Object.keys(ctx.formData).forEach((key) => delete ctx.formData[key]);
475
- const newValues = values || ctx.defaultValues;
750
+ const sourceValues = values || ctx.defaultValues;
751
+ const newValues = JSON.parse(JSON.stringify(sourceValues));
476
752
  Object.assign(ctx.formData, newValues);
477
753
  if (!opts.keepErrors) ctx.errors.value = {};
478
754
  if (!opts.keepTouched) ctx.touchedFields.value = {};
479
755
  if (!opts.keepDirty) ctx.dirtyFields.value = {};
480
756
  if (!opts.keepSubmitCount) ctx.submitCount.value = 0;
481
757
  if (!opts.keepIsSubmitting) ctx.isSubmitting.value = false;
758
+ if (!opts.keepIsSubmitSuccessful) ctx.isSubmitSuccessful.value = false;
482
759
  ctx.fieldArrays.clear();
483
760
  for (const [name, fieldRef] of ctx.fieldRefs.entries()) {
484
761
  const el = fieldRef.value;
@@ -489,13 +766,54 @@ function useForm(options) {
489
766
  }
490
767
  }
491
768
  }
492
- function watch(name) {
769
+ function resetField(name, resetFieldOptions) {
770
+ const opts = resetFieldOptions || {};
771
+ ctx.resetGeneration.value++;
772
+ const errorTimer = ctx.errorDelayTimers.get(name);
773
+ if (errorTimer) {
774
+ clearTimeout(errorTimer);
775
+ ctx.errorDelayTimers.delete(name);
776
+ }
777
+ ctx.pendingErrors.delete(name);
778
+ let defaultValue = opts.defaultValue;
779
+ if (defaultValue === void 0) defaultValue = get(ctx.defaultValues, name);
780
+ else set(ctx.defaultValues, name, defaultValue);
781
+ const clonedValue = defaultValue !== void 0 ? JSON.parse(JSON.stringify(defaultValue)) : void 0;
782
+ set(ctx.formData, name, clonedValue);
783
+ if (!opts.keepError) {
784
+ const newErrors = { ...ctx.errors.value };
785
+ for (const key of Object.keys(newErrors)) if (key === name || key.startsWith(`${name}.`)) delete newErrors[key];
786
+ ctx.errors.value = newErrors;
787
+ }
788
+ if (!opts.keepDirty) {
789
+ const newDirty = { ...ctx.dirtyFields.value };
790
+ delete newDirty[name];
791
+ ctx.dirtyFields.value = newDirty;
792
+ }
793
+ if (!opts.keepTouched) {
794
+ const newTouched = { ...ctx.touchedFields.value };
795
+ delete newTouched[name];
796
+ ctx.touchedFields.value = newTouched;
797
+ }
798
+ if (!ctx.fieldOptions.get(name)?.controlled) {
799
+ const fieldRef = ctx.fieldRefs.get(name);
800
+ if (fieldRef?.value) {
801
+ const el = fieldRef.value;
802
+ if (clonedValue !== void 0) if (el.type === "checkbox") el.checked = clonedValue;
803
+ else el.value = clonedValue;
804
+ else if (el.type === "checkbox") el.checked = false;
805
+ else el.value = "";
806
+ }
807
+ }
808
+ }
809
+ function watch$1(name) {
493
810
  return (0, vue.computed)(() => {
494
811
  if (!name) return ctx.formData;
495
- if (Array.isArray(name)) return name.reduce((acc, n) => {
496
- acc[n] = get(ctx.formData, n);
497
- return acc;
498
- }, {});
812
+ if (Array.isArray(name)) {
813
+ const result = {};
814
+ for (const n of name) result[n] = get(ctx.formData, n);
815
+ return result;
816
+ }
499
817
  return get(ctx.formData, name);
500
818
  });
501
819
  }
@@ -553,15 +871,6 @@ function useForm(options) {
553
871
  }
554
872
  return await validate(name);
555
873
  }
556
- function setFocus(name, focusOptions) {
557
- const fieldRef = ctx.fieldRefs.get(name);
558
- if (!fieldRef?.value) return;
559
- const el = fieldRef.value;
560
- if (typeof el.focus === "function") {
561
- el.focus();
562
- if (focusOptions?.shouldSelect && el instanceof HTMLInputElement && typeof el.select === "function") el.select();
563
- }
564
- }
565
874
  return {
566
875
  register,
567
876
  unregister,
@@ -571,7 +880,8 @@ function useForm(options) {
571
880
  setValue,
572
881
  getValue,
573
882
  reset,
574
- watch,
883
+ resetField,
884
+ watch: watch$1,
575
885
  validate,
576
886
  clearErrors,
577
887
  setError,
@@ -653,7 +963,11 @@ function useFormState(options = {}) {
653
963
  return { [name]: fullState[name] };
654
964
  });
655
965
  }
966
+ function isFieldError(error) {
967
+ return typeof error === "object" && error !== null && "type" in error && "message" in error && typeof error.type === "string" && typeof error.message === "string";
968
+ }
656
969
  exports.FormContextKey = FormContextKey;
970
+ exports.isFieldError = isFieldError;
657
971
  exports.provideForm = provideForm;
658
972
  exports.useController = useController;
659
973
  exports.useForm = useForm;