@vuehookform/core 0.4.1 → 0.4.2

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,6 +1,7 @@
1
- import { computed, inject, nextTick, provide, reactive, ref, shallowRef, toValue, watch } from "vue";
1
+ import { computed, inject, nextTick, onUnmounted, provide, reactive, ref, shallowRef, toValue, watch } from "vue";
2
2
  var pathCache = /* @__PURE__ */ new Map();
3
3
  var PATH_CACHE_MAX_SIZE = 256;
4
+ var MAX_ARRAY_INDEX = 1e4;
4
5
  function getPathSegments(path) {
5
6
  let segments = pathCache.get(path);
6
7
  if (segments) return segments;
@@ -12,6 +13,9 @@ function getPathSegments(path) {
12
13
  pathCache.set(path, segments);
13
14
  return segments;
14
15
  }
16
+ function clearPathCache() {
17
+ pathCache.clear();
18
+ }
15
19
  function get(obj, path) {
16
20
  if (!path || obj === null || obj === void 0) return obj;
17
21
  const keys = getPathSegments(path);
@@ -31,6 +35,13 @@ function set(obj, path, value) {
31
35
  "prototype"
32
36
  ];
33
37
  if (keys.some((k) => UNSAFE_KEYS.includes(k))) return;
38
+ for (const key of keys) if (/^\d+$/.test(key)) {
39
+ const index = parseInt(key, 10);
40
+ if (index > MAX_ARRAY_INDEX) {
41
+ if (typeof console !== "undefined" && console.warn) console.warn(`[vue-hook-form] set(): Array index ${index} exceeds maximum allowed (${MAX_ARRAY_INDEX}). Path "${path}" was not set to prevent memory exhaustion.`);
42
+ return;
43
+ }
44
+ }
34
45
  const lastKey = keys.pop();
35
46
  let current = obj;
36
47
  for (let i = 0; i < keys.length; i++) {
@@ -65,13 +76,22 @@ function generateId() {
65
76
  const random = Math.random().toString(36).substring(2, 11);
66
77
  return `field_${Date.now()}_${idCounter++}_${random}`;
67
78
  }
68
- function deepClone(obj) {
79
+ function deepClone(obj, seen) {
69
80
  if (obj === null || obj === void 0) return obj;
70
81
  if (typeof obj !== "object") return obj;
71
82
  if (obj instanceof Date) return new Date(obj.getTime());
72
- if (Array.isArray(obj)) return obj.map((item) => deepClone(item));
83
+ if (!seen) seen = /* @__PURE__ */ new Map();
84
+ const existingClone = seen.get(obj);
85
+ if (existingClone !== void 0) return existingClone;
86
+ if (Array.isArray(obj)) {
87
+ const clonedArray = [];
88
+ seen.set(obj, clonedArray);
89
+ for (const item of obj) clonedArray.push(deepClone(item, seen));
90
+ return clonedArray;
91
+ }
73
92
  const cloned = {};
74
- for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) cloned[key] = deepClone(obj[key]);
93
+ seen.set(obj, cloned);
94
+ for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) cloned[key] = deepClone(obj[key], seen);
75
95
  return cloned;
76
96
  }
77
97
  const __DEV__ = globalThis.process?.env?.NODE_ENV !== "production";
@@ -228,8 +248,8 @@ function createFormContext(options) {
228
248
  if (isAsyncDefaults) {
229
249
  const asyncFn = options.defaultValues;
230
250
  asyncFn().then((values) => {
231
- Object.assign(defaultValues, values);
232
- Object.assign(formData, values);
251
+ Object.assign(defaultValues, deepClone(values));
252
+ Object.assign(formData, deepClone(values));
233
253
  isLoading.value = false;
234
254
  }).catch((error) => {
235
255
  console.error("Failed to load async default values:", error);
@@ -238,8 +258,8 @@ function createFormContext(options) {
238
258
  options.onDefaultValuesError?.(error);
239
259
  });
240
260
  } else if (options.defaultValues) {
241
- Object.assign(defaultValues, options.defaultValues);
242
- Object.assign(formData, defaultValues);
261
+ Object.assign(defaultValues, deepClone(options.defaultValues));
262
+ Object.assign(formData, deepClone(options.defaultValues));
243
263
  }
244
264
  const errors = shallowRef({});
245
265
  const touchedFields = shallowRef({});
@@ -259,23 +279,24 @@ function createFormContext(options) {
259
279
  const debounceTimers = /* @__PURE__ */ new Map();
260
280
  const validationRequestIds = /* @__PURE__ */ new Map();
261
281
  const resetGeneration = ref(0);
262
- const dirtyFieldCount = ref(0);
263
- const touchedFieldCount = ref(0);
264
282
  const validationCache = /* @__PURE__ */ new Map();
265
283
  const schemaValidationTimers = /* @__PURE__ */ new Map();
284
+ const persistentErrorFields = /* @__PURE__ */ new Set();
285
+ const defaultValueHashes = /* @__PURE__ */ new Map();
266
286
  const isDisabled = ref(false);
287
+ const watchStopHandles = [];
267
288
  if (options.disabled !== void 0) {
268
289
  isDisabled.value = toValue(options.disabled) ?? false;
269
- watch(() => toValue(options.disabled), (newDisabled) => {
290
+ watchStopHandles.push(watch(() => toValue(options.disabled), (newDisabled) => {
270
291
  isDisabled.value = newDisabled ?? false;
271
- });
292
+ }));
272
293
  }
273
294
  if (options.values !== void 0) {
274
295
  const initialValues = toValue(options.values);
275
296
  if (initialValues && !isAsyncDefaults) {
276
297
  for (const [key, value] of Object.entries(initialValues)) if (value !== void 0) set(formData, key, value);
277
298
  }
278
- watch(() => toValue(options.values), (newValues) => {
299
+ watchStopHandles.push(watch(() => toValue(options.values), (newValues) => {
279
300
  if (newValues) {
280
301
  for (const [key, value] of Object.entries(newValues)) if (value !== void 0) {
281
302
  set(formData, key, value);
@@ -288,14 +309,14 @@ function createFormContext(options) {
288
309
  }
289
310
  }
290
311
  }
291
- }, { deep: true });
312
+ }, { deep: true }));
292
313
  }
293
314
  if (options.errors !== void 0) {
294
315
  const initialErrors = toValue(options.errors);
295
316
  if (initialErrors) externalErrors.value = initialErrors;
296
- watch(() => toValue(options.errors), (newErrors) => {
317
+ watchStopHandles.push(watch(() => toValue(options.errors), (newErrors) => {
297
318
  externalErrors.value = newErrors || {};
298
- }, { deep: true });
319
+ }, { deep: true }));
299
320
  }
300
321
  return {
301
322
  formData,
@@ -320,25 +341,40 @@ function createFormContext(options) {
320
341
  validationRequestIds,
321
342
  resetGeneration,
322
343
  isDisabled,
323
- dirtyFieldCount,
324
- touchedFieldCount,
325
344
  validationCache,
326
345
  schemaValidationTimers,
327
- options
346
+ persistentErrorFields,
347
+ defaultValueHashes,
348
+ options,
349
+ cleanup: () => {
350
+ for (const stop of watchStopHandles) stop();
351
+ }
328
352
  };
329
353
  }
330
354
  var uniqueIdCounter = 0;
355
+ var circularRefMap = /* @__PURE__ */ new WeakMap();
331
356
  function hashValue(value) {
332
357
  if (value === null) return "null";
333
358
  if (value === void 0) return "undefined";
334
359
  const type = typeof value;
335
360
  if (type === "string") return `s:${value}`;
336
- if (type === "number") return `n:${value}`;
361
+ if (type === "number") {
362
+ const num = value;
363
+ if (Number.isNaN(num)) return "n:NaN";
364
+ if (num === 0) return "n:0";
365
+ if (!Number.isFinite(num)) return `n:${num > 0 ? "Infinity" : "-Infinity"}`;
366
+ return `n:${num}`;
367
+ }
337
368
  if (type === "boolean") return `b:${value}`;
338
369
  if (type === "object") try {
339
370
  return `o:${JSON.stringify(value)}`;
340
371
  } catch {
341
- return `o:_${++uniqueIdCounter}`;
372
+ let id = circularRefMap.get(value);
373
+ if (!id) {
374
+ id = String(++uniqueIdCounter);
375
+ circularRefMap.set(value, id);
376
+ }
377
+ return `o:_${id}`;
342
378
  }
343
379
  return `x:_${++uniqueIdCounter}`;
344
380
  }
@@ -373,7 +409,7 @@ function extractSubSchema(schema, path) {
373
409
  let currentSchema = schema;
374
410
  let hasEffects = false;
375
411
  for (const segment of segments) {
376
- if (!segment) continue;
412
+ if (!segment) return null;
377
413
  if (hasChecks(currentSchema)) hasEffects = true;
378
414
  const unwrapped = unwrapNonEffects(currentSchema);
379
415
  if (hasChecks(unwrapped)) hasEffects = true;
@@ -440,10 +476,54 @@ function analyzeSchemaPath(schema, path) {
440
476
  cache.set(path, result);
441
477
  return result;
442
478
  }
443
- function clearFieldErrors$1(errors, fieldPath) {
444
- const newErrors = { ...errors };
445
- for (const key of Object.keys(newErrors)) if (key === fieldPath || key.startsWith(`${fieldPath}.`)) delete newErrors[key];
446
- return newErrors;
479
+ function markFieldTouched(touchedFields, fieldName) {
480
+ if (touchedFields.value[fieldName]) return;
481
+ touchedFields.value = {
482
+ ...touchedFields.value,
483
+ [fieldName]: true
484
+ };
485
+ }
486
+ function clearFieldDirty(dirtyFields, fieldName) {
487
+ if (!(fieldName in dirtyFields.value)) return;
488
+ const newDirty = { ...dirtyFields.value };
489
+ delete newDirty[fieldName];
490
+ dirtyFields.value = newDirty;
491
+ }
492
+ function clearFieldTouched(touchedFields, fieldName) {
493
+ if (!(fieldName in touchedFields.value)) return;
494
+ const newTouched = { ...touchedFields.value };
495
+ delete newTouched[fieldName];
496
+ touchedFields.value = newTouched;
497
+ }
498
+ function clearFieldErrors(errors, fieldName) {
499
+ const currentErrors = errors.value;
500
+ const keys = Object.keys(currentErrors);
501
+ if (keys.length === 0) return;
502
+ const prefix = `${fieldName}.`;
503
+ const keysToDelete = [];
504
+ for (const key of keys) if (key === fieldName || key.startsWith(prefix)) keysToDelete.push(key);
505
+ if (keysToDelete.length === 0) return;
506
+ const newErrors = { ...currentErrors };
507
+ for (const key of keysToDelete) delete newErrors[key];
508
+ errors.value = newErrors;
509
+ }
510
+ function updateFieldDirtyState(dirtyFields, defaultValues, defaultValueHashes, fieldName, currentValue) {
511
+ let defaultHash = defaultValueHashes.get(fieldName);
512
+ if (defaultHash === void 0) {
513
+ defaultHash = hashValue(get(defaultValues, fieldName));
514
+ defaultValueHashes.set(fieldName, defaultHash);
515
+ }
516
+ const isDirty = hashValue(currentValue) !== defaultHash;
517
+ const wasDirty = dirtyFields.value[fieldName] === true;
518
+ if (isDirty && !wasDirty) dirtyFields.value = {
519
+ ...dirtyFields.value,
520
+ [fieldName]: true
521
+ };
522
+ else if (!isDirty && wasDirty) {
523
+ const newDirty = { ...dirtyFields.value };
524
+ delete newDirty[fieldName];
525
+ dirtyFields.value = newDirty;
526
+ }
447
527
  }
448
528
  function setValidating(ctx, fieldPath, isValidating) {
449
529
  const newSet = new Set(ctx.validatingFields.value);
@@ -537,37 +617,43 @@ function createValidation(ctx) {
537
617
  }
538
618
  ctx.pendingErrors.delete(fieldPath);
539
619
  applyNativeValidation(fieldPath, null);
540
- return clearFieldErrors$1(ctx.errors.value, fieldPath);
620
+ if (ctx.persistentErrorFields.has(fieldPath)) return;
621
+ clearFieldErrors(ctx.errors, fieldPath);
541
622
  }
542
623
  function clearAllPendingErrors() {
543
624
  for (const timer of ctx.errorDelayTimers.values()) clearTimeout(timer);
544
625
  ctx.errorDelayTimers.clear();
545
626
  ctx.pendingErrors.clear();
546
627
  }
547
- async function validateFieldPartial(fieldPath, subSchema, valueHash, criteriaMode, generationAtStart) {
628
+ async function validateFieldPartial(fieldPath, subSchema, valueHash, cacheKey, criteriaMode, generationAtStart) {
548
629
  const fieldValue = get(ctx.formData, fieldPath);
549
630
  setValidating(ctx, fieldPath, true);
550
631
  try {
551
632
  const result = await subSchema.safeParseAsync(fieldValue);
552
633
  if (ctx.resetGeneration.value !== generationAtStart) return true;
553
634
  if (result.success) {
554
- ctx.errors.value = cancelError(fieldPath);
555
- if (valueHash) ctx.validationCache.set(fieldPath, {
635
+ cancelError(fieldPath);
636
+ if (valueHash) ctx.validationCache.set(cacheKey, {
556
637
  hash: valueHash,
557
638
  isValid: true
558
639
  });
559
640
  return true;
560
641
  }
561
- const fieldErrors = result.error.issues.map((issue) => ({
562
- ...issue,
563
- path: fieldPath.split(".").concat(issue.path.map(String))
564
- }));
565
- ctx.errors.value = cancelError(fieldPath);
642
+ const fieldSegments = fieldPath.split(".");
643
+ const fieldErrors = result.error.issues.map((issue) => {
644
+ const issuePath = issue.path.map(String);
645
+ const alreadyPrefixed = issuePath.length >= fieldSegments.length && fieldSegments.every((seg, i) => issuePath[i] === seg);
646
+ return {
647
+ ...issue,
648
+ path: alreadyPrefixed ? issuePath : fieldSegments.concat(issuePath)
649
+ };
650
+ });
651
+ cancelError(fieldPath);
566
652
  const grouped = groupErrorsByPath(fieldErrors);
567
653
  const errorBatch = [];
568
654
  for (const [path, errors] of grouped) errorBatch.push([path, createFieldError(errors, criteriaMode)]);
569
655
  scheduleErrorsBatch(errorBatch);
570
- if (valueHash) ctx.validationCache.set(fieldPath, {
656
+ if (valueHash) ctx.validationCache.set(cacheKey, {
571
657
  hash: valueHash,
572
658
  isValid: false
573
659
  });
@@ -576,14 +662,14 @@ function createValidation(ctx) {
576
662
  setValidating(ctx, fieldPath, false);
577
663
  }
578
664
  }
579
- async function validateFieldFull(fieldPath, valueHash, criteriaMode, generationAtStart) {
665
+ async function validateFieldFull(fieldPath, valueHash, cacheKey, criteriaMode, generationAtStart) {
580
666
  setValidating(ctx, fieldPath, true);
581
667
  try {
582
668
  const result = await ctx.options.schema.safeParseAsync(ctx.formData);
583
669
  if (ctx.resetGeneration.value !== generationAtStart) return true;
584
670
  if (result.success) {
585
- ctx.errors.value = cancelError(fieldPath);
586
- if (valueHash) ctx.validationCache.set(fieldPath, {
671
+ cancelError(fieldPath);
672
+ if (valueHash) ctx.validationCache.set(cacheKey, {
587
673
  hash: valueHash,
588
674
  isValid: true
589
675
  });
@@ -594,19 +680,19 @@ function createValidation(ctx) {
594
680
  return path === fieldPath || path.startsWith(`${fieldPath}.`);
595
681
  });
596
682
  if (fieldErrors.length === 0) {
597
- ctx.errors.value = cancelError(fieldPath);
598
- if (valueHash) ctx.validationCache.set(fieldPath, {
683
+ cancelError(fieldPath);
684
+ if (valueHash) ctx.validationCache.set(cacheKey, {
599
685
  hash: valueHash,
600
686
  isValid: true
601
687
  });
602
688
  return true;
603
689
  }
604
- ctx.errors.value = cancelError(fieldPath);
690
+ cancelError(fieldPath);
605
691
  const grouped = groupErrorsByPath(fieldErrors);
606
692
  const errorBatch = [];
607
693
  for (const [path, errors] of grouped) errorBatch.push([path, createFieldError(errors, criteriaMode)]);
608
694
  scheduleErrorsBatch(errorBatch);
609
- if (valueHash) ctx.validationCache.set(fieldPath, {
695
+ if (valueHash) ctx.validationCache.set(cacheKey, {
610
696
  hash: valueHash,
611
697
  isValid: false
612
698
  });
@@ -621,14 +707,16 @@ function createValidation(ctx) {
621
707
  let valueHash;
622
708
  if (fieldPath) {
623
709
  valueHash = hashValue(get(ctx.formData, fieldPath));
624
- const cached = ctx.validationCache.get(fieldPath);
625
- if (cached && cached.hash === valueHash) {
626
- if (cached.isValid) ctx.errors.value = cancelError(fieldPath);
627
- return cached.isValid;
628
- }
629
710
  const analysis = analyzeSchemaPath(ctx.options.schema, fieldPath);
630
- if (analysis.canPartialValidate && analysis.subSchema) return validateFieldPartial(fieldPath, analysis.subSchema, valueHash, criteriaMode, generationAtStart);
631
- return validateFieldFull(fieldPath, valueHash, criteriaMode, generationAtStart);
711
+ const usePartial = analysis.canPartialValidate && analysis.subSchema;
712
+ const cacheKey = `${fieldPath}:${usePartial ? "partial" : "full"}`;
713
+ const cached = ctx.validationCache.get(cacheKey);
714
+ if (cached && cached.hash === valueHash && cached.isValid) {
715
+ cancelError(fieldPath);
716
+ return true;
717
+ }
718
+ if (usePartial) return validateFieldPartial(fieldPath, analysis.subSchema, valueHash, cacheKey, criteriaMode, generationAtStart);
719
+ return validateFieldFull(fieldPath, valueHash, cacheKey, criteriaMode, generationAtStart);
632
720
  }
633
721
  const validatingKey = "_form";
634
722
  setValidating(ctx, validatingKey, true);
@@ -659,47 +747,31 @@ function createValidation(ctx) {
659
747
  clearAllPendingErrors
660
748
  };
661
749
  }
662
- function markFieldDirty(dirtyFields, dirtyFieldCount, fieldName) {
663
- if (dirtyFields.value[fieldName]) return;
664
- dirtyFields.value = {
665
- ...dirtyFields.value,
666
- [fieldName]: true
667
- };
668
- dirtyFieldCount.value++;
669
- }
670
- function markFieldTouched(touchedFields, touchedFieldCount, fieldName) {
671
- if (touchedFields.value[fieldName]) return;
672
- touchedFields.value = {
673
- ...touchedFields.value,
674
- [fieldName]: true
675
- };
676
- touchedFieldCount.value++;
677
- }
678
- function clearFieldDirty(dirtyFields, dirtyFieldCount, fieldName) {
679
- if (!(fieldName in dirtyFields.value)) return;
680
- const newDirty = { ...dirtyFields.value };
681
- delete newDirty[fieldName];
682
- dirtyFields.value = newDirty;
683
- dirtyFieldCount.value--;
750
+ var VALID_MODES = [
751
+ "onSubmit",
752
+ "onBlur",
753
+ "onChange",
754
+ "onTouched"
755
+ ];
756
+ function validateMode(mode, paramName) {
757
+ if (__DEV__ && !VALID_MODES.includes(mode)) warnOnce(`Invalid ${paramName}: "${mode}". Expected one of: ${VALID_MODES.join(", ")}`, `invalid-mode-${mode}`);
684
758
  }
685
- function clearFieldTouched(touchedFields, touchedFieldCount, fieldName) {
686
- if (!(fieldName in touchedFields.value)) return;
687
- const newTouched = { ...touchedFields.value };
688
- delete newTouched[fieldName];
689
- touchedFields.value = newTouched;
690
- touchedFieldCount.value--;
759
+ function shouldValidateOnChange(mode, isTouched, reValidateMode, hasSubmitted) {
760
+ if (__DEV__) {
761
+ validateMode(mode, "validation mode");
762
+ if (reValidateMode) validateMode(reValidateMode, "reValidateMode");
763
+ }
764
+ if (mode === "onChange") return true;
765
+ if (mode === "onTouched" && isTouched) return true;
766
+ if (hasSubmitted === true && reValidateMode === "onChange") return true;
767
+ return false;
691
768
  }
692
- function clearFieldErrors(errors, fieldName) {
693
- const currentErrors = errors.value;
694
- const keys = Object.keys(currentErrors);
695
- if (keys.length === 0) return;
696
- const prefix = `${fieldName}.`;
697
- const keysToDelete = [];
698
- for (const key of keys) if (key === fieldName || key.startsWith(prefix)) keysToDelete.push(key);
699
- if (keysToDelete.length === 0) return;
700
- const newErrors = { ...currentErrors };
701
- for (const key of keysToDelete) delete newErrors[key];
702
- errors.value = newErrors;
769
+ function shouldValidateOnBlur(mode, hasSubmitted, reValidateMode) {
770
+ if (__DEV__) {
771
+ validateMode(mode, "validation mode");
772
+ if (reValidateMode) validateMode(reValidateMode, "reValidateMode");
773
+ }
774
+ return mode === "onBlur" || mode === "onTouched" || hasSubmitted && (reValidateMode === "onBlur" || reValidateMode === "onTouched");
703
775
  }
704
776
  var validationRequestCounter = 0;
705
777
  function createFieldRegistration(ctx, validate) {
@@ -740,11 +812,14 @@ function createFieldRegistration(ctx, validate) {
740
812
  };
741
813
  const onInput = async (e) => {
742
814
  const target = e.target;
743
- const value = target.type === "checkbox" ? target.checked : target.value;
815
+ let value;
816
+ if (target.type === "checkbox") value = target.checked;
817
+ else if (target.type === "number" || target.type === "range") value = target.valueAsNumber;
818
+ else value = target.value;
744
819
  set(ctx.formData, name, value);
745
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
820
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, value);
746
821
  const fieldOpts = ctx.fieldOptions.get(name);
747
- if (ctx.options.mode === "onChange" || ctx.options.mode === "onTouched" && ctx.touchedFields.value[name] || ctx.touchedFields.value[name] && ctx.options.reValidateMode === "onChange") {
822
+ if (shouldValidateOnChange(ctx.options.mode ?? "onSubmit", ctx.touchedFields.value[name] === true, ctx.options.reValidateMode, ctx.submitCount.value > 0)) {
748
823
  const validationDebounceMs = ctx.options.validationDebounce || 0;
749
824
  if (validationDebounceMs > 0) {
750
825
  const existingTimer = ctx.schemaValidationTimers.get(name);
@@ -777,15 +852,18 @@ function createFieldRegistration(ctx, validate) {
777
852
  const timer = setTimeout(async () => {
778
853
  ctx.debounceTimers.delete(name);
779
854
  await runCustomValidation(name, value, requestId, resetGenAtStart);
780
- ctx.validationRequestIds.delete(name);
855
+ if (ctx.validationRequestIds.get(name) === requestId) ctx.validationRequestIds.delete(name);
781
856
  }, debounceMs);
782
857
  ctx.debounceTimers.set(name, timer);
783
- } else await runCustomValidation(name, value, requestId, resetGenAtStart);
858
+ } else {
859
+ await runCustomValidation(name, value, requestId, resetGenAtStart);
860
+ if (ctx.validationRequestIds.get(name) === requestId) ctx.validationRequestIds.delete(name);
861
+ }
784
862
  }
785
863
  };
786
864
  const onBlur = async (_e) => {
787
- markFieldTouched(ctx.touchedFields, ctx.touchedFieldCount, name);
788
- if (ctx.options.mode === "onBlur" || ctx.options.mode === "onTouched" || ctx.submitCount.value > 0 && ctx.options.reValidateMode === "onBlur") {
865
+ markFieldTouched(ctx.touchedFields, name);
866
+ if (shouldValidateOnBlur(ctx.options.mode ?? "onSubmit", ctx.submitCount.value > 0, ctx.options.reValidateMode)) {
789
867
  await validate(name);
790
868
  const fieldOpts = ctx.fieldOptions.get(name);
791
869
  if (fieldOpts?.deps && fieldOpts.deps.length > 0) {
@@ -818,15 +896,21 @@ function createFieldRegistration(ctx, validate) {
818
896
  clearTimeout(schemaTimer);
819
897
  ctx.schemaValidationTimers.delete(name);
820
898
  }
899
+ const errorTimer = ctx.errorDelayTimers.get(name);
900
+ if (errorTimer) {
901
+ clearTimeout(errorTimer);
902
+ ctx.errorDelayTimers.delete(name);
903
+ }
904
+ ctx.pendingErrors.delete(name);
821
905
  ctx.validationRequestIds.delete(name);
906
+ ctx.fieldRefs.delete(name);
907
+ ctx.fieldOptions.delete(name);
908
+ ctx.fieldHandlers.delete(name);
822
909
  if (opts?.shouldUnregister ?? ctx.options.shouldUnregister ?? false) {
823
910
  unset(ctx.formData, name);
824
911
  clearFieldErrors(ctx.errors, name);
825
- clearFieldTouched(ctx.touchedFields, ctx.touchedFieldCount, name);
826
- clearFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
827
- ctx.fieldRefs.delete(name);
828
- ctx.fieldOptions.delete(name);
829
- ctx.fieldHandlers.delete(name);
912
+ clearFieldTouched(ctx.touchedFields, name);
913
+ clearFieldDirty(ctx.dirtyFields, name);
830
914
  }
831
915
  }
832
916
  };
@@ -847,7 +931,7 @@ function createFieldRegistration(ctx, validate) {
847
931
  get: () => get(ctx.formData, name),
848
932
  set: (val) => {
849
933
  set(ctx.formData, name, val);
850
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
934
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, val);
851
935
  }
852
936
  }) }
853
937
  };
@@ -856,8 +940,8 @@ function createFieldRegistration(ctx, validate) {
856
940
  const opts = options || {};
857
941
  if (!opts.keepValue) unset(ctx.formData, name);
858
942
  if (!opts.keepError) clearFieldErrors(ctx.errors, name);
859
- if (!opts.keepTouched) clearFieldTouched(ctx.touchedFields, ctx.touchedFieldCount, name);
860
- if (!opts.keepDirty) clearFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
943
+ if (!opts.keepTouched) clearFieldTouched(ctx.touchedFields, name);
944
+ if (!opts.keepDirty) clearFieldDirty(ctx.dirtyFields, name);
861
945
  ctx.fieldRefs.delete(name);
862
946
  ctx.fieldOptions.delete(name);
863
947
  ctx.fieldHandlers.delete(name);
@@ -872,6 +956,8 @@ function createFieldRegistration(ctx, validate) {
872
956
  ctx.schemaValidationTimers.delete(name);
873
957
  }
874
958
  ctx.validationRequestIds.delete(name);
959
+ ctx.validationCache.delete(`${name}:partial`);
960
+ ctx.validationCache.delete(`${name}:full`);
875
961
  }
876
962
  return {
877
963
  register,
@@ -961,10 +1047,32 @@ function createFieldArrayManager(ctx, validate, setFocus) {
961
1047
  const normalizeToArray = (value) => {
962
1048
  return Array.isArray(value) ? value : [value];
963
1049
  };
1050
+ const clearValidationCache = () => {
1051
+ for (const key of ctx.validationCache.keys()) {
1052
+ const colonIndex = key.lastIndexOf(":");
1053
+ const fieldPath = colonIndex > 0 ? key.slice(0, colonIndex) : key;
1054
+ if (fieldPath === name || fieldPath.startsWith(`${name}.`)) ctx.validationCache.delete(key);
1055
+ }
1056
+ };
1057
+ const validateIfNeeded = () => {
1058
+ const isTouched = ctx.touchedFields.value[name] === true;
1059
+ const hasSubmitted = ctx.submitCount.value > 0;
1060
+ if (shouldValidateOnChange(ctx.options.mode ?? "onSubmit", isTouched, ctx.options.reValidateMode, hasSubmitted)) validate(name);
1061
+ };
1062
+ const ensureSync = () => {
1063
+ const currentValues = get(ctx.formData, name) || [];
1064
+ if (fa.items.value.length !== currentValues.length) {
1065
+ if (__DEV__) console.warn(`[vue-hook-form] Field array out of sync with formData. Rebuilding items array (items: ${fa.items.value.length}, formData: ${currentValues.length})`);
1066
+ fa.items.value = currentValues.map(() => createItem(generateId()));
1067
+ rebuildIndexCache();
1068
+ }
1069
+ return currentValues;
1070
+ };
964
1071
  const append = (value, focusOptions) => {
1072
+ clearValidationCache();
965
1073
  const values = normalizeToArray(value);
966
1074
  if (values.length === 0) return true;
967
- const currentValues = get(ctx.formData, name) || [];
1075
+ const currentValues = ensureSync();
968
1076
  const insertIndex = currentValues.length;
969
1077
  const rules = fa.rules;
970
1078
  if (rules?.maxLength && currentValues.length + values.length > rules.maxLength.value) {
@@ -979,15 +1087,16 @@ function createFieldArrayManager(ctx, validate, setFocus) {
979
1087
  const newItems = values.map(() => createItem(generateId()));
980
1088
  fa.items.value = [...fa.items.value, ...newItems];
981
1089
  appendToCache(insertIndex);
982
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
983
- if (ctx.options.mode === "onChange") validate(name);
1090
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1091
+ validateIfNeeded();
984
1092
  handleFocus(insertIndex, values.length, focusOptions);
985
1093
  return true;
986
1094
  };
987
1095
  const prepend = (value, focusOptions) => {
1096
+ clearValidationCache();
988
1097
  const values = normalizeToArray(value);
989
1098
  if (values.length === 0) return true;
990
- const currentValues = get(ctx.formData, name) || [];
1099
+ const currentValues = ensureSync();
991
1100
  const rules = fa.rules;
992
1101
  if (rules?.maxLength && currentValues.length + values.length > rules.maxLength.value) {
993
1102
  if (__DEV__) warnArrayOperationRejected("prepend", name, "maxLength", {
@@ -1001,13 +1110,14 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1001
1110
  const newItems = values.map(() => createItem(generateId()));
1002
1111
  fa.items.value = [...newItems, ...fa.items.value];
1003
1112
  updateCacheAfterInsert(0, values.length);
1004
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1005
- if (ctx.options.mode === "onChange") validate(name);
1113
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1114
+ validateIfNeeded();
1006
1115
  handleFocus(0, values.length, focusOptions);
1007
1116
  return true;
1008
1117
  };
1009
1118
  const update = (index, value) => {
1010
- const currentValues = get(ctx.formData, name) || [];
1119
+ clearValidationCache();
1120
+ const currentValues = ensureSync();
1011
1121
  if (index < 0 || index >= currentValues.length) {
1012
1122
  if (__DEV__) warnArrayIndexOutOfBounds("update", name, index, currentValues.length);
1013
1123
  return false;
@@ -1015,12 +1125,13 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1015
1125
  const newValues = [...currentValues];
1016
1126
  newValues[index] = value;
1017
1127
  set(ctx.formData, name, newValues);
1018
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1019
- if (ctx.options.mode === "onChange") validate(name);
1128
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1129
+ validateIfNeeded();
1020
1130
  return true;
1021
1131
  };
1022
1132
  const removeAt = (index) => {
1023
- const currentValues = get(ctx.formData, name) || [];
1133
+ clearValidationCache();
1134
+ const currentValues = ensureSync();
1024
1135
  if (index < 0 || index >= currentValues.length) {
1025
1136
  if (__DEV__) warnArrayIndexOutOfBounds("remove", name, index, currentValues.length);
1026
1137
  return false;
@@ -1038,14 +1149,15 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1038
1149
  const keyToRemove = fa.items.value[index]?.key;
1039
1150
  fa.items.value = fa.items.value.filter((item) => item.key !== keyToRemove);
1040
1151
  if (keyToRemove) updateCacheAfterRemove(keyToRemove, index);
1041
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1042
- if (ctx.options.mode === "onChange") validate(name);
1152
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1153
+ validateIfNeeded();
1043
1154
  return true;
1044
1155
  };
1045
1156
  const insert = (index, value, focusOptions) => {
1157
+ clearValidationCache();
1046
1158
  const values = normalizeToArray(value);
1047
1159
  if (values.length === 0) return true;
1048
- const currentValues = get(ctx.formData, name) || [];
1160
+ const currentValues = ensureSync();
1049
1161
  const rules = fa.rules;
1050
1162
  if (rules?.maxLength && currentValues.length + values.length > rules.maxLength.value) {
1051
1163
  if (__DEV__) warnArrayOperationRejected("insert", name, "maxLength", {
@@ -1054,27 +1166,31 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1054
1166
  });
1055
1167
  return false;
1056
1168
  }
1057
- const clampedIndex = Math.max(0, Math.min(index, currentValues.length));
1169
+ if (index < 0 || index > currentValues.length) {
1170
+ if (__DEV__) warnArrayIndexOutOfBounds("insert", name, index, currentValues.length);
1171
+ return false;
1172
+ }
1058
1173
  const newValues = [
1059
- ...currentValues.slice(0, clampedIndex),
1174
+ ...currentValues.slice(0, index),
1060
1175
  ...values,
1061
- ...currentValues.slice(clampedIndex)
1176
+ ...currentValues.slice(index)
1062
1177
  ];
1063
1178
  set(ctx.formData, name, newValues);
1064
1179
  const newItems = values.map(() => createItem(generateId()));
1065
1180
  fa.items.value = [
1066
- ...fa.items.value.slice(0, clampedIndex),
1181
+ ...fa.items.value.slice(0, index),
1067
1182
  ...newItems,
1068
- ...fa.items.value.slice(clampedIndex)
1183
+ ...fa.items.value.slice(index)
1069
1184
  ];
1070
- updateCacheAfterInsert(clampedIndex, values.length);
1071
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1072
- if (ctx.options.mode === "onChange") validate(name);
1073
- handleFocus(clampedIndex, values.length, focusOptions);
1185
+ updateCacheAfterInsert(index, values.length);
1186
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1187
+ validateIfNeeded();
1188
+ handleFocus(index, values.length, focusOptions);
1074
1189
  return true;
1075
1190
  };
1076
1191
  const swap = (indexA, indexB) => {
1077
- const currentValues = get(ctx.formData, name) || [];
1192
+ clearValidationCache();
1193
+ const currentValues = ensureSync();
1078
1194
  if (indexA < 0 || indexB < 0 || indexA >= currentValues.length || indexB >= currentValues.length) {
1079
1195
  if (__DEV__) warnArrayIndexOutOfBounds("swap", name, indexA < 0 || indexA >= currentValues.length ? indexA : indexB, currentValues.length);
1080
1196
  return false;
@@ -1091,51 +1207,52 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1091
1207
  fa.items.value = newItems;
1092
1208
  swapInCache(indexA, indexB);
1093
1209
  }
1094
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1095
- if (ctx.options.mode === "onChange") validate(name);
1210
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1211
+ validateIfNeeded();
1096
1212
  return true;
1097
1213
  };
1098
1214
  const move = (from, to) => {
1099
- const currentValues = get(ctx.formData, name) || [];
1100
- if (from < 0 || from >= currentValues.length || to < 0) {
1215
+ clearValidationCache();
1216
+ const currentValues = ensureSync();
1217
+ if (from < 0 || from >= currentValues.length || to < 0 || to >= currentValues.length) {
1101
1218
  if (__DEV__) warnArrayIndexOutOfBounds("move", name, from < 0 || from >= currentValues.length ? from : to, currentValues.length);
1102
1219
  return false;
1103
1220
  }
1104
1221
  const newValues = [...currentValues];
1105
1222
  const [removed] = newValues.splice(from, 1);
1106
1223
  if (removed !== void 0) {
1107
- const clampedTo = Math.min(to, newValues.length);
1108
- newValues.splice(clampedTo, 0, removed);
1224
+ newValues.splice(to, 0, removed);
1109
1225
  set(ctx.formData, name, newValues);
1110
1226
  }
1111
1227
  const newItems = [...fa.items.value];
1112
1228
  const [removedItem] = newItems.splice(from, 1);
1113
1229
  if (removedItem) {
1114
- const clampedTo = Math.min(to, newItems.length);
1115
- newItems.splice(clampedTo, 0, removedItem);
1230
+ newItems.splice(to, 0, removedItem);
1116
1231
  fa.items.value = newItems;
1117
- const minIdx = Math.min(from, clampedTo);
1118
- const maxIdx = Math.max(from, clampedTo);
1232
+ const minIdx = Math.min(from, to);
1233
+ const maxIdx = Math.max(from, to);
1119
1234
  const items = fa.items.value;
1120
1235
  for (let i = minIdx; i <= maxIdx; i++) {
1121
1236
  const item = items[i];
1122
1237
  if (item) indexCache.set(item.key, i);
1123
1238
  }
1124
1239
  }
1125
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1126
- if (ctx.options.mode === "onChange") validate(name);
1240
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1241
+ validateIfNeeded();
1127
1242
  return true;
1128
1243
  };
1129
1244
  const replace = (newValues) => {
1245
+ clearValidationCache();
1130
1246
  if (!Array.isArray(newValues)) return false;
1131
1247
  set(ctx.formData, name, newValues);
1132
1248
  fa.items.value = newValues.map(() => createItem(generateId()));
1133
1249
  rebuildIndexCache();
1134
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1135
- if (ctx.options.mode === "onChange") validate(name);
1250
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1251
+ validateIfNeeded();
1136
1252
  return true;
1137
1253
  };
1138
1254
  const removeAll = () => {
1255
+ clearValidationCache();
1139
1256
  const rules = fa.rules;
1140
1257
  if (rules?.minLength && rules.minLength.value > 0) {
1141
1258
  if (__DEV__) warnArrayOperationRejected("removeAll", name, "minLength", {
@@ -1147,12 +1264,13 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1147
1264
  set(ctx.formData, name, []);
1148
1265
  fa.items.value = [];
1149
1266
  indexCache.clear();
1150
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1151
- if (ctx.options.mode === "onChange") validate(name);
1267
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1268
+ validateIfNeeded();
1152
1269
  return true;
1153
1270
  };
1154
1271
  const removeMany = (indices) => {
1155
- const currentValues = get(ctx.formData, name) || [];
1272
+ clearValidationCache();
1273
+ const currentValues = ensureSync();
1156
1274
  const validIndices = indices.filter((i) => i >= 0 && i < currentValues.length);
1157
1275
  if (validIndices.length === 0) return true;
1158
1276
  const rules = fa.rules;
@@ -1174,8 +1292,8 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1174
1292
  fa.items.value.forEach((item, idx) => {
1175
1293
  indexCache.set(item.key, idx);
1176
1294
  });
1177
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1178
- if (ctx.options.mode === "onChange") validate(name);
1295
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1296
+ validateIfNeeded();
1179
1297
  return true;
1180
1298
  };
1181
1299
  return {
@@ -1198,7 +1316,13 @@ function syncUncontrolledInputs(fieldRefs, fieldOptions, formData) {
1198
1316
  for (const [name, fieldRef] of Array.from(fieldRefs.entries())) {
1199
1317
  const el = fieldRef.value;
1200
1318
  if (el) {
1201
- if (!fieldOptions.get(name)?.controlled) set(formData, name, el.type === "checkbox" ? el.checked : el.value);
1319
+ if (!fieldOptions.get(name)?.controlled) {
1320
+ let value;
1321
+ if (el.type === "checkbox") value = el.checked;
1322
+ else if (el.type === "number" || el.type === "range") value = el.valueAsNumber;
1323
+ else value = el.value;
1324
+ set(formData, name, value);
1325
+ }
1202
1326
  }
1203
1327
  }
1204
1328
  }
@@ -1208,6 +1332,10 @@ function updateDomElement(el, value) {
1208
1332
  }
1209
1333
  function useForm(options) {
1210
1334
  const ctx = createFormContext(options);
1335
+ let isSubmissionLocked = false;
1336
+ onUnmounted(() => {
1337
+ ctx.cleanup();
1338
+ });
1211
1339
  const { validate, clearAllPendingErrors } = createValidation(ctx);
1212
1340
  const { register, unregister } = createFieldRegistration(ctx, validate);
1213
1341
  function setFocus(name, focusOptions) {
@@ -1250,10 +1378,10 @@ function useForm(options) {
1250
1378
  };
1251
1379
  return cachedMergedErrors;
1252
1380
  }
1253
- const isDirtyComputed = computed(() => ctx.dirtyFieldCount.value > 0);
1381
+ const isDirtyComputed = computed(() => Object.keys(ctx.dirtyFields.value).length > 0);
1254
1382
  const errorsComputed = computed(() => getMergedErrors());
1255
1383
  const isValidComputed = computed(() => {
1256
- if (!(ctx.submitCount.value > 0 || ctx.touchedFieldCount.value > 0)) return false;
1384
+ if (!(ctx.submitCount.value > 0 || Object.keys(ctx.touchedFields.value).length > 0)) return false;
1257
1385
  return Object.keys(errorsComputed.value).length === 0;
1258
1386
  });
1259
1387
  const isReadyComputed = computed(() => !ctx.isLoading.value);
@@ -1311,7 +1439,8 @@ function useForm(options) {
1311
1439
  return async (e) => {
1312
1440
  e.preventDefault();
1313
1441
  if (ctx.isDisabled.value) return;
1314
- if (ctx.isSubmitting.value) return;
1442
+ if (isSubmissionLocked) return;
1443
+ isSubmissionLocked = true;
1315
1444
  ctx.isSubmitting.value = true;
1316
1445
  ctx.submitCount.value++;
1317
1446
  ctx.isSubmitSuccessful.value = false;
@@ -1329,6 +1458,7 @@ function useForm(options) {
1329
1458
  }
1330
1459
  } finally {
1331
1460
  ctx.isSubmitting.value = false;
1461
+ isSubmissionLocked = false;
1332
1462
  }
1333
1463
  };
1334
1464
  }
@@ -1342,8 +1472,9 @@ function useForm(options) {
1342
1472
  }
1343
1473
  }
1344
1474
  set(ctx.formData, name, value);
1345
- if (setValueOptions?.shouldDirty !== false) markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1346
- if (setValueOptions?.shouldTouch) markFieldTouched(ctx.touchedFields, ctx.touchedFieldCount, name);
1475
+ if (setValueOptions?.shouldDirty === false) clearFieldDirty(ctx.dirtyFields, name);
1476
+ else updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, value);
1477
+ if (setValueOptions?.shouldTouch) markFieldTouched(ctx.touchedFields, name);
1347
1478
  if (!ctx.fieldOptions.get(name)?.controlled) {
1348
1479
  const fieldRef = ctx.fieldRefs.get(name);
1349
1480
  if (fieldRef?.value) updateDomElement(fieldRef.value, value);
@@ -1352,25 +1483,28 @@ function useForm(options) {
1352
1483
  }
1353
1484
  function reset(values, resetOptions) {
1354
1485
  const opts = resetOptions || {};
1486
+ ctx.validationCache.clear();
1355
1487
  ctx.resetGeneration.value++;
1356
1488
  clearAllPendingErrors();
1357
1489
  ctx.validatingFields.value = /* @__PURE__ */ new Set();
1358
- ctx.validationCache.clear();
1359
1490
  for (const timer of ctx.schemaValidationTimers.values()) clearTimeout(timer);
1360
1491
  ctx.schemaValidationTimers.clear();
1361
- if (!opts.keepDefaultValues && values) Object.assign(ctx.defaultValues, values);
1492
+ for (const timer of ctx.debounceTimers.values()) clearTimeout(timer);
1493
+ ctx.debounceTimers.clear();
1494
+ ctx.validationRequestIds.clear();
1495
+ if (!opts.keepDefaultValues && values) {
1496
+ Object.assign(ctx.defaultValues, values);
1497
+ ctx.defaultValueHashes.clear();
1498
+ }
1362
1499
  Object.keys(ctx.formData).forEach((key) => delete ctx.formData[key]);
1363
1500
  const newValues = deepClone(values || ctx.defaultValues);
1364
1501
  Object.assign(ctx.formData, newValues);
1365
- if (!opts.keepErrors) ctx.errors.value = {};
1366
- if (!opts.keepTouched) {
1367
- ctx.touchedFields.value = {};
1368
- ctx.touchedFieldCount.value = 0;
1369
- }
1370
- if (!opts.keepDirty) {
1371
- ctx.dirtyFields.value = {};
1372
- ctx.dirtyFieldCount.value = 0;
1502
+ if (!opts.keepErrors) {
1503
+ ctx.errors.value = {};
1504
+ ctx.persistentErrorFields.clear();
1373
1505
  }
1506
+ if (!opts.keepTouched) ctx.touchedFields.value = {};
1507
+ if (!opts.keepDirty) ctx.dirtyFields.value = {};
1374
1508
  if (!opts.keepSubmitCount) ctx.submitCount.value = 0;
1375
1509
  if (!opts.keepIsSubmitting) ctx.isSubmitting.value = false;
1376
1510
  if (!opts.keepIsSubmitSuccessful) ctx.isSubmitSuccessful.value = false;
@@ -1395,6 +1529,8 @@ function useForm(options) {
1395
1529
  }
1396
1530
  const opts = resetFieldOptions || {};
1397
1531
  ctx.resetGeneration.value++;
1532
+ ctx.validationCache.delete(`${name}:partial`);
1533
+ ctx.validationCache.delete(`${name}:full`);
1398
1534
  const errorTimer = ctx.errorDelayTimers.get(name);
1399
1535
  if (errorTimer) {
1400
1536
  clearTimeout(errorTimer);
@@ -1403,12 +1539,15 @@ function useForm(options) {
1403
1539
  ctx.pendingErrors.delete(name);
1404
1540
  let defaultValue = opts.defaultValue;
1405
1541
  if (defaultValue === void 0) defaultValue = get(ctx.defaultValues, name);
1406
- else set(ctx.defaultValues, name, defaultValue);
1542
+ else {
1543
+ set(ctx.defaultValues, name, defaultValue);
1544
+ ctx.defaultValueHashes.set(name, hashValue(defaultValue));
1545
+ }
1407
1546
  const clonedValue = defaultValue !== void 0 ? deepClone(defaultValue) : void 0;
1408
1547
  set(ctx.formData, name, clonedValue);
1409
1548
  if (!opts.keepError) clearFieldErrors(ctx.errors, name);
1410
- if (!opts.keepDirty) clearFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1411
- if (!opts.keepTouched) clearFieldTouched(ctx.touchedFields, ctx.touchedFieldCount, name);
1549
+ if (!opts.keepDirty) clearFieldDirty(ctx.dirtyFields, name);
1550
+ if (!opts.keepTouched) clearFieldTouched(ctx.touchedFields, name);
1412
1551
  if (!ctx.fieldOptions.get(name)?.controlled) {
1413
1552
  const fieldRef = ctx.fieldRefs.get(name);
1414
1553
  if (fieldRef?.value) updateDomElement(fieldRef.value, clonedValue ?? (fieldRef.value.type === "checkbox" ? false : ""));
@@ -1451,10 +1590,16 @@ function useForm(options) {
1451
1590
  }
1452
1591
  if (name === void 0) {
1453
1592
  ctx.errors.value = {};
1593
+ ctx.externalErrors.value = {};
1594
+ ctx.persistentErrorFields.clear();
1454
1595
  return;
1455
1596
  }
1456
1597
  const fieldsToClean = Array.isArray(name) ? name : [name];
1457
- for (const field of fieldsToClean) clearFieldErrors(ctx.errors, field);
1598
+ for (const field of fieldsToClean) {
1599
+ clearFieldErrors(ctx.errors, field);
1600
+ clearFieldErrors(ctx.externalErrors, field);
1601
+ ctx.persistentErrorFields.delete(field);
1602
+ }
1458
1603
  }
1459
1604
  function setError(name, error) {
1460
1605
  const newErrors = { ...ctx.errors.value };
@@ -1463,6 +1608,7 @@ function useForm(options) {
1463
1608
  message: error.message
1464
1609
  } : error.message);
1465
1610
  ctx.errors.value = newErrors;
1611
+ if (error.persistent) ctx.persistentErrorFields.add(name);
1466
1612
  }
1467
1613
  function setErrors(errors, options$1) {
1468
1614
  const newErrors = options$1?.shouldReplace ? {} : { ...ctx.errors.value };
@@ -1524,7 +1670,7 @@ function useForm(options) {
1524
1670
  error
1525
1671
  };
1526
1672
  }
1527
- async function trigger(name) {
1673
+ async function trigger(name, options$1) {
1528
1674
  if (__DEV__ && name) {
1529
1675
  const names = Array.isArray(name) ? name : [name];
1530
1676
  for (const n of names) {
@@ -1536,6 +1682,7 @@ function useForm(options) {
1536
1682
  }
1537
1683
  }
1538
1684
  }
1685
+ if (options$1?.markAsSubmitted) ctx.submitCount.value++;
1539
1686
  if (name === void 0) return await validate();
1540
1687
  if (Array.isArray(name)) {
1541
1688
  let allValid = true;
@@ -1563,7 +1710,11 @@ function useForm(options) {
1563
1710
  getValues,
1564
1711
  getFieldState,
1565
1712
  trigger,
1566
- setFocus
1713
+ setFocus,
1714
+ options: {
1715
+ mode: ctx.options.mode ?? "onSubmit",
1716
+ reValidateMode: ctx.options.reValidateMode
1717
+ }
1567
1718
  };
1568
1719
  }
1569
1720
  const FormContextKey = Symbol("FormContext");
@@ -1602,13 +1753,22 @@ function useController(options) {
1602
1753
  }
1603
1754
  });
1604
1755
  const onChange = (newValue) => {
1605
- form.setValue(name, newValue);
1756
+ const isTouched = form.formState.value.touchedFields[name] === true;
1757
+ const hasSubmitted = form.formState.value.submitCount > 0;
1758
+ const mode = form.options.mode ?? "onSubmit";
1759
+ const reValidateMode = form.options.reValidateMode;
1760
+ const shouldValidate = shouldValidateOnChange(mode, isTouched, reValidateMode, hasSubmitted);
1761
+ form.setValue(name, newValue, { shouldValidate });
1606
1762
  };
1607
1763
  const onBlur = () => {
1764
+ const hasSubmitted = form.formState.value.submitCount > 0;
1765
+ const mode = form.options.mode ?? "onSubmit";
1766
+ const reValidateMode = form.options.reValidateMode;
1767
+ const shouldValidate = shouldValidateOnBlur(mode, hasSubmitted, reValidateMode);
1608
1768
  const currentValue = form.getValues(name);
1609
1769
  form.setValue(name, currentValue, {
1610
1770
  shouldTouch: true,
1611
- shouldValidate: true,
1771
+ shouldValidate,
1612
1772
  shouldDirty: false
1613
1773
  });
1614
1774
  };
@@ -1646,4 +1806,4 @@ function useFormState(options = {}) {
1646
1806
  function isFieldError(error) {
1647
1807
  return typeof error === "object" && error !== null && "type" in error && "message" in error && typeof error.type === "string" && typeof error.message === "string";
1648
1808
  }
1649
- export { FormContextKey, isFieldError, provideForm, useController, useForm, useFormContext, useFormState, useWatch };
1809
+ export { FormContextKey, clearPathCache, generateId, get, isFieldError, provideForm, set, unset, useController, useForm, useFormContext, useFormState, useWatch };