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