@vuehookform/core 0.4.1 → 0.4.3

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, getCurrentInstance, inject, nextTick, onScopeDispose, 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";
@@ -200,26 +220,68 @@ function warnArrayIndexOutOfBounds(operation, path, index, length) {
200
220
  if (!__DEV__) return;
201
221
  warn(`${operation}() on "${path}": Index ${index} is out of bounds (array length: ${length}). Operation was silently ignored.`);
202
222
  }
203
- function getDefProp$1(schema, prop) {
204
- return schema.def[prop];
205
- }
206
- function getTypeName(schema) {
207
- return getDefProp$1(schema, "typeName");
223
+ function getSchemaType(schema) {
224
+ return schema.type;
208
225
  }
209
226
  function isZodObject$1(schema) {
210
- return getTypeName(schema) === "ZodObject";
227
+ return getSchemaType(schema) === "object";
211
228
  }
212
229
  function isZodArray$1(schema) {
213
- return getTypeName(schema) === "ZodArray";
230
+ return getSchemaType(schema) === "array";
214
231
  }
215
232
  function unwrapSchema(schema) {
216
- const typeName = getTypeName(schema);
217
- const innerType = getDefProp$1(schema, "innerType");
218
- const schemaType = getDefProp$1(schema, "schema");
219
- if ((typeName === "ZodOptional" || typeName === "ZodNullable" || typeName === "ZodDefault") && innerType) return unwrapSchema(innerType);
220
- if (typeName === "ZodEffects" && schemaType) return unwrapSchema(schemaType);
233
+ const schemaType = getSchemaType(schema);
234
+ if (schemaType === "optional" || schemaType === "nullable" || schemaType === "default") {
235
+ const schemaWithUnwrap = schema;
236
+ if (typeof schemaWithUnwrap.unwrap === "function") return unwrapSchema(schemaWithUnwrap.unwrap());
237
+ }
221
238
  return schema;
222
239
  }
240
+ function getInputElement(refValue) {
241
+ if (!refValue) return null;
242
+ if (refValue instanceof HTMLInputElement) return refValue;
243
+ if (refValue instanceof HTMLSelectElement || refValue instanceof HTMLTextAreaElement) return refValue;
244
+ if (typeof refValue === "object" && "$el" in refValue) {
245
+ const el = refValue.$el;
246
+ if (el instanceof HTMLInputElement) return el;
247
+ if (el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement) return el;
248
+ if (el instanceof Element) {
249
+ const input = el.querySelector("input, select, textarea");
250
+ if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement || input instanceof HTMLTextAreaElement) return input;
251
+ }
252
+ }
253
+ return null;
254
+ }
255
+ function getFocusableElement(refValue) {
256
+ const input = getInputElement(refValue);
257
+ if (input) return input;
258
+ if (typeof refValue === "object" && refValue && "$el" in refValue) {
259
+ const el = refValue.$el;
260
+ if (el instanceof HTMLElement && typeof el.focus === "function") return el;
261
+ }
262
+ if (refValue instanceof HTMLElement && typeof refValue.focus === "function") return refValue;
263
+ return null;
264
+ }
265
+ function syncUncontrolledInputs(fieldRefs, fieldOptions, formData) {
266
+ for (const [name, fieldRef] of Array.from(fieldRefs.entries())) {
267
+ const el = getInputElement(fieldRef.value);
268
+ if (el) {
269
+ if (!fieldOptions.get(name)?.controlled) {
270
+ let value;
271
+ if (el.type === "checkbox") value = el.checked;
272
+ else if (el.type === "number" || el.type === "range") value = el.valueAsNumber;
273
+ else value = el.value;
274
+ set(formData, name, value);
275
+ }
276
+ }
277
+ }
278
+ }
279
+ function updateDomElement(refValue, value) {
280
+ const el = getInputElement(refValue);
281
+ if (!el) return;
282
+ if (el.type === "checkbox") el.checked = value;
283
+ else el.value = value;
284
+ }
223
285
  function createFormContext(options) {
224
286
  const formData = reactive({});
225
287
  const defaultValues = reactive({});
@@ -228,8 +290,8 @@ function createFormContext(options) {
228
290
  if (isAsyncDefaults) {
229
291
  const asyncFn = options.defaultValues;
230
292
  asyncFn().then((values) => {
231
- Object.assign(defaultValues, values);
232
- Object.assign(formData, values);
293
+ Object.assign(defaultValues, deepClone(values));
294
+ Object.assign(formData, deepClone(values));
233
295
  isLoading.value = false;
234
296
  }).catch((error) => {
235
297
  console.error("Failed to load async default values:", error);
@@ -238,8 +300,8 @@ function createFormContext(options) {
238
300
  options.onDefaultValuesError?.(error);
239
301
  });
240
302
  } else if (options.defaultValues) {
241
- Object.assign(defaultValues, options.defaultValues);
242
- Object.assign(formData, defaultValues);
303
+ Object.assign(defaultValues, deepClone(options.defaultValues));
304
+ Object.assign(formData, deepClone(options.defaultValues));
243
305
  }
244
306
  const errors = shallowRef({});
245
307
  const touchedFields = shallowRef({});
@@ -259,43 +321,44 @@ function createFormContext(options) {
259
321
  const debounceTimers = /* @__PURE__ */ new Map();
260
322
  const validationRequestIds = /* @__PURE__ */ new Map();
261
323
  const resetGeneration = ref(0);
262
- const dirtyFieldCount = ref(0);
263
- const touchedFieldCount = ref(0);
264
324
  const validationCache = /* @__PURE__ */ new Map();
265
325
  const schemaValidationTimers = /* @__PURE__ */ new Map();
326
+ const persistentErrorFields = /* @__PURE__ */ new Set();
327
+ const defaultValueHashes = /* @__PURE__ */ new Map();
266
328
  const isDisabled = ref(false);
329
+ const watchStopHandles = [];
267
330
  if (options.disabled !== void 0) {
268
331
  isDisabled.value = toValue(options.disabled) ?? false;
269
- watch(() => toValue(options.disabled), (newDisabled) => {
332
+ watchStopHandles.push(watch(() => toValue(options.disabled), (newDisabled) => {
270
333
  isDisabled.value = newDisabled ?? false;
271
- });
334
+ }));
272
335
  }
273
336
  if (options.values !== void 0) {
274
337
  const initialValues = toValue(options.values);
275
338
  if (initialValues && !isAsyncDefaults) {
276
339
  for (const [key, value] of Object.entries(initialValues)) if (value !== void 0) set(formData, key, value);
277
340
  }
278
- watch(() => toValue(options.values), (newValues) => {
341
+ watchStopHandles.push(watch(() => toValue(options.values), (newValues) => {
279
342
  if (newValues) {
280
343
  for (const [key, value] of Object.entries(newValues)) if (value !== void 0) {
281
344
  set(formData, key, value);
282
345
  const fieldRef = fieldRefs.get(key);
283
346
  const opts = fieldOptions.get(key);
284
347
  if (fieldRef?.value && !opts?.controlled) {
285
- const el = fieldRef.value;
286
- if (el.type === "checkbox") el.checked = value;
348
+ const el = getInputElement(fieldRef.value);
349
+ if (el) if (el.type === "checkbox") el.checked = value;
287
350
  else el.value = value;
288
351
  }
289
352
  }
290
353
  }
291
- }, { deep: true });
354
+ }, { deep: true }));
292
355
  }
293
356
  if (options.errors !== void 0) {
294
357
  const initialErrors = toValue(options.errors);
295
358
  if (initialErrors) externalErrors.value = initialErrors;
296
- watch(() => toValue(options.errors), (newErrors) => {
359
+ watchStopHandles.push(watch(() => toValue(options.errors), (newErrors) => {
297
360
  externalErrors.value = newErrors || {};
298
- }, { deep: true });
361
+ }, { deep: true }));
299
362
  }
300
363
  return {
301
364
  formData,
@@ -320,25 +383,40 @@ function createFormContext(options) {
320
383
  validationRequestIds,
321
384
  resetGeneration,
322
385
  isDisabled,
323
- dirtyFieldCount,
324
- touchedFieldCount,
325
386
  validationCache,
326
387
  schemaValidationTimers,
327
- options
388
+ persistentErrorFields,
389
+ defaultValueHashes,
390
+ options,
391
+ cleanup: () => {
392
+ for (const stop of watchStopHandles) stop();
393
+ }
328
394
  };
329
395
  }
330
396
  var uniqueIdCounter = 0;
397
+ var circularRefMap = /* @__PURE__ */ new WeakMap();
331
398
  function hashValue(value) {
332
399
  if (value === null) return "null";
333
400
  if (value === void 0) return "undefined";
334
401
  const type = typeof value;
335
402
  if (type === "string") return `s:${value}`;
336
- if (type === "number") return `n:${value}`;
403
+ if (type === "number") {
404
+ const num = value;
405
+ if (Number.isNaN(num)) return "n:NaN";
406
+ if (num === 0) return "n:0";
407
+ if (!Number.isFinite(num)) return `n:${num > 0 ? "Infinity" : "-Infinity"}`;
408
+ return `n:${num}`;
409
+ }
337
410
  if (type === "boolean") return `b:${value}`;
338
411
  if (type === "object") try {
339
412
  return `o:${JSON.stringify(value)}`;
340
413
  } catch {
341
- return `o:_${++uniqueIdCounter}`;
414
+ let id = circularRefMap.get(value);
415
+ if (!id) {
416
+ id = String(++uniqueIdCounter);
417
+ circularRefMap.set(value, id);
418
+ }
419
+ return `o:_${id}`;
342
420
  }
343
421
  return `x:_${++uniqueIdCounter}`;
344
422
  }
@@ -373,7 +451,7 @@ function extractSubSchema(schema, path) {
373
451
  let currentSchema = schema;
374
452
  let hasEffects = false;
375
453
  for (const segment of segments) {
376
- if (!segment) continue;
454
+ if (!segment) return null;
377
455
  if (hasChecks(currentSchema)) hasEffects = true;
378
456
  const unwrapped = unwrapNonEffects(currentSchema);
379
457
  if (hasChecks(unwrapped)) hasEffects = true;
@@ -440,10 +518,54 @@ function analyzeSchemaPath(schema, path) {
440
518
  cache.set(path, result);
441
519
  return result;
442
520
  }
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;
521
+ function markFieldTouched(touchedFields, fieldName) {
522
+ if (touchedFields.value[fieldName]) return;
523
+ touchedFields.value = {
524
+ ...touchedFields.value,
525
+ [fieldName]: true
526
+ };
527
+ }
528
+ function clearFieldDirty(dirtyFields, fieldName) {
529
+ if (!(fieldName in dirtyFields.value)) return;
530
+ const newDirty = { ...dirtyFields.value };
531
+ delete newDirty[fieldName];
532
+ dirtyFields.value = newDirty;
533
+ }
534
+ function clearFieldTouched(touchedFields, fieldName) {
535
+ if (!(fieldName in touchedFields.value)) return;
536
+ const newTouched = { ...touchedFields.value };
537
+ delete newTouched[fieldName];
538
+ touchedFields.value = newTouched;
539
+ }
540
+ function clearFieldErrors(errors, fieldName) {
541
+ const currentErrors = errors.value;
542
+ const keys = Object.keys(currentErrors);
543
+ if (keys.length === 0) return;
544
+ const prefix = `${fieldName}.`;
545
+ const keysToDelete = [];
546
+ for (const key of keys) if (key === fieldName || key.startsWith(prefix)) keysToDelete.push(key);
547
+ if (keysToDelete.length === 0) return;
548
+ const newErrors = { ...currentErrors };
549
+ for (const key of keysToDelete) delete newErrors[key];
550
+ errors.value = newErrors;
551
+ }
552
+ function updateFieldDirtyState(dirtyFields, defaultValues, defaultValueHashes, fieldName, currentValue) {
553
+ let defaultHash = defaultValueHashes.get(fieldName);
554
+ if (defaultHash === void 0) {
555
+ defaultHash = hashValue(get(defaultValues, fieldName));
556
+ defaultValueHashes.set(fieldName, defaultHash);
557
+ }
558
+ const isDirty = hashValue(currentValue) !== defaultHash;
559
+ const wasDirty = dirtyFields.value[fieldName] === true;
560
+ if (isDirty && !wasDirty) dirtyFields.value = {
561
+ ...dirtyFields.value,
562
+ [fieldName]: true
563
+ };
564
+ else if (!isDirty && wasDirty) {
565
+ const newDirty = { ...dirtyFields.value };
566
+ delete newDirty[fieldName];
567
+ dirtyFields.value = newDirty;
568
+ }
447
569
  }
448
570
  function setValidating(ctx, fieldPath, isValidating) {
449
571
  const newSet = new Set(ctx.validatingFields.value);
@@ -484,7 +606,7 @@ function createFieldError(errors, criteriaMode = "firstError") {
484
606
  function createValidation(ctx) {
485
607
  function applyNativeValidation(fieldPath, errorMessage) {
486
608
  if (!ctx.options.shouldUseNativeValidation) return;
487
- const el = ctx.fieldRefs.get(fieldPath)?.value;
609
+ const el = getInputElement(ctx.fieldRefs.get(fieldPath)?.value);
488
610
  if (el && "setCustomValidity" in el) el.setCustomValidity(errorMessage || "");
489
611
  }
490
612
  function clearAllNativeValidation() {
@@ -537,37 +659,43 @@ function createValidation(ctx) {
537
659
  }
538
660
  ctx.pendingErrors.delete(fieldPath);
539
661
  applyNativeValidation(fieldPath, null);
540
- return clearFieldErrors$1(ctx.errors.value, fieldPath);
662
+ if (ctx.persistentErrorFields.has(fieldPath)) return;
663
+ clearFieldErrors(ctx.errors, fieldPath);
541
664
  }
542
665
  function clearAllPendingErrors() {
543
666
  for (const timer of ctx.errorDelayTimers.values()) clearTimeout(timer);
544
667
  ctx.errorDelayTimers.clear();
545
668
  ctx.pendingErrors.clear();
546
669
  }
547
- async function validateFieldPartial(fieldPath, subSchema, valueHash, criteriaMode, generationAtStart) {
670
+ async function validateFieldPartial(fieldPath, subSchema, valueHash, cacheKey, criteriaMode, generationAtStart) {
548
671
  const fieldValue = get(ctx.formData, fieldPath);
549
672
  setValidating(ctx, fieldPath, true);
550
673
  try {
551
674
  const result = await subSchema.safeParseAsync(fieldValue);
552
675
  if (ctx.resetGeneration.value !== generationAtStart) return true;
553
676
  if (result.success) {
554
- ctx.errors.value = cancelError(fieldPath);
555
- if (valueHash) ctx.validationCache.set(fieldPath, {
677
+ cancelError(fieldPath);
678
+ if (valueHash) ctx.validationCache.set(cacheKey, {
556
679
  hash: valueHash,
557
680
  isValid: true
558
681
  });
559
682
  return true;
560
683
  }
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);
684
+ const fieldSegments = fieldPath.split(".");
685
+ const fieldErrors = result.error.issues.map((issue) => {
686
+ const issuePath = issue.path.map(String);
687
+ const alreadyPrefixed = issuePath.length >= fieldSegments.length && fieldSegments.every((seg, i) => issuePath[i] === seg);
688
+ return {
689
+ ...issue,
690
+ path: alreadyPrefixed ? issuePath : fieldSegments.concat(issuePath)
691
+ };
692
+ });
693
+ cancelError(fieldPath);
566
694
  const grouped = groupErrorsByPath(fieldErrors);
567
695
  const errorBatch = [];
568
696
  for (const [path, errors] of grouped) errorBatch.push([path, createFieldError(errors, criteriaMode)]);
569
697
  scheduleErrorsBatch(errorBatch);
570
- if (valueHash) ctx.validationCache.set(fieldPath, {
698
+ if (valueHash) ctx.validationCache.set(cacheKey, {
571
699
  hash: valueHash,
572
700
  isValid: false
573
701
  });
@@ -576,14 +704,14 @@ function createValidation(ctx) {
576
704
  setValidating(ctx, fieldPath, false);
577
705
  }
578
706
  }
579
- async function validateFieldFull(fieldPath, valueHash, criteriaMode, generationAtStart) {
707
+ async function validateFieldFull(fieldPath, valueHash, cacheKey, criteriaMode, generationAtStart) {
580
708
  setValidating(ctx, fieldPath, true);
581
709
  try {
582
710
  const result = await ctx.options.schema.safeParseAsync(ctx.formData);
583
711
  if (ctx.resetGeneration.value !== generationAtStart) return true;
584
712
  if (result.success) {
585
- ctx.errors.value = cancelError(fieldPath);
586
- if (valueHash) ctx.validationCache.set(fieldPath, {
713
+ cancelError(fieldPath);
714
+ if (valueHash) ctx.validationCache.set(cacheKey, {
587
715
  hash: valueHash,
588
716
  isValid: true
589
717
  });
@@ -594,19 +722,19 @@ function createValidation(ctx) {
594
722
  return path === fieldPath || path.startsWith(`${fieldPath}.`);
595
723
  });
596
724
  if (fieldErrors.length === 0) {
597
- ctx.errors.value = cancelError(fieldPath);
598
- if (valueHash) ctx.validationCache.set(fieldPath, {
725
+ cancelError(fieldPath);
726
+ if (valueHash) ctx.validationCache.set(cacheKey, {
599
727
  hash: valueHash,
600
728
  isValid: true
601
729
  });
602
730
  return true;
603
731
  }
604
- ctx.errors.value = cancelError(fieldPath);
732
+ cancelError(fieldPath);
605
733
  const grouped = groupErrorsByPath(fieldErrors);
606
734
  const errorBatch = [];
607
735
  for (const [path, errors] of grouped) errorBatch.push([path, createFieldError(errors, criteriaMode)]);
608
736
  scheduleErrorsBatch(errorBatch);
609
- if (valueHash) ctx.validationCache.set(fieldPath, {
737
+ if (valueHash) ctx.validationCache.set(cacheKey, {
610
738
  hash: valueHash,
611
739
  isValid: false
612
740
  });
@@ -621,14 +749,16 @@ function createValidation(ctx) {
621
749
  let valueHash;
622
750
  if (fieldPath) {
623
751
  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
752
  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);
753
+ const usePartial = analysis.canPartialValidate && analysis.subSchema;
754
+ const cacheKey = `${fieldPath}:${usePartial ? "partial" : "full"}`;
755
+ const cached = ctx.validationCache.get(cacheKey);
756
+ if (cached && cached.hash === valueHash && cached.isValid) {
757
+ cancelError(fieldPath);
758
+ return true;
759
+ }
760
+ if (usePartial) return validateFieldPartial(fieldPath, analysis.subSchema, valueHash, cacheKey, criteriaMode, generationAtStart);
761
+ return validateFieldFull(fieldPath, valueHash, cacheKey, criteriaMode, generationAtStart);
632
762
  }
633
763
  const validatingKey = "_form";
634
764
  setValidating(ctx, validatingKey, true);
@@ -659,47 +789,31 @@ function createValidation(ctx) {
659
789
  clearAllPendingErrors
660
790
  };
661
791
  }
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++;
792
+ var VALID_MODES = [
793
+ "onSubmit",
794
+ "onBlur",
795
+ "onChange",
796
+ "onTouched"
797
+ ];
798
+ function validateMode(mode, paramName) {
799
+ if (__DEV__ && !VALID_MODES.includes(mode)) warnOnce(`Invalid ${paramName}: "${mode}". Expected one of: ${VALID_MODES.join(", ")}`, `invalid-mode-${mode}`);
669
800
  }
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--;
684
- }
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--;
801
+ function shouldValidateOnChange(mode, isTouched, reValidateMode, hasSubmitted) {
802
+ if (__DEV__) {
803
+ validateMode(mode, "validation mode");
804
+ if (reValidateMode) validateMode(reValidateMode, "reValidateMode");
805
+ }
806
+ if (mode === "onChange") return true;
807
+ if (mode === "onTouched" && isTouched) return true;
808
+ if (hasSubmitted === true && reValidateMode === "onChange") return true;
809
+ return false;
691
810
  }
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;
811
+ function shouldValidateOnBlur(mode, hasSubmitted, reValidateMode) {
812
+ if (__DEV__) {
813
+ validateMode(mode, "validation mode");
814
+ if (reValidateMode) validateMode(reValidateMode, "reValidateMode");
815
+ }
816
+ return mode === "onBlur" || mode === "onTouched" || hasSubmitted && (reValidateMode === "onBlur" || reValidateMode === "onTouched");
703
817
  }
704
818
  var validationRequestCounter = 0;
705
819
  function createFieldRegistration(ctx, validate) {
@@ -740,11 +854,14 @@ function createFieldRegistration(ctx, validate) {
740
854
  };
741
855
  const onInput = async (e) => {
742
856
  const target = e.target;
743
- const value = target.type === "checkbox" ? target.checked : target.value;
857
+ let value;
858
+ if (target.type === "checkbox") value = target.checked;
859
+ else if (target.type === "number" || target.type === "range") value = target.valueAsNumber;
860
+ else value = target.value;
744
861
  set(ctx.formData, name, value);
745
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
862
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, value);
746
863
  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") {
864
+ if (shouldValidateOnChange(ctx.options.mode ?? "onSubmit", ctx.touchedFields.value[name] === true, ctx.options.reValidateMode, ctx.submitCount.value > 0)) {
748
865
  const validationDebounceMs = ctx.options.validationDebounce || 0;
749
866
  if (validationDebounceMs > 0) {
750
867
  const existingTimer = ctx.schemaValidationTimers.get(name);
@@ -777,15 +894,18 @@ function createFieldRegistration(ctx, validate) {
777
894
  const timer = setTimeout(async () => {
778
895
  ctx.debounceTimers.delete(name);
779
896
  await runCustomValidation(name, value, requestId, resetGenAtStart);
780
- ctx.validationRequestIds.delete(name);
897
+ if (ctx.validationRequestIds.get(name) === requestId) ctx.validationRequestIds.delete(name);
781
898
  }, debounceMs);
782
899
  ctx.debounceTimers.set(name, timer);
783
- } else await runCustomValidation(name, value, requestId, resetGenAtStart);
900
+ } else {
901
+ await runCustomValidation(name, value, requestId, resetGenAtStart);
902
+ if (ctx.validationRequestIds.get(name) === requestId) ctx.validationRequestIds.delete(name);
903
+ }
784
904
  }
785
905
  };
786
- 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") {
906
+ const onBlur = async () => {
907
+ markFieldTouched(ctx.touchedFields, name);
908
+ if (shouldValidateOnBlur(ctx.options.mode ?? "onSubmit", ctx.submitCount.value > 0, ctx.options.reValidateMode)) {
789
909
  await validate(name);
790
910
  const fieldOpts = ctx.fieldOptions.get(name);
791
911
  if (fieldOpts?.deps && fieldOpts.deps.length > 0) {
@@ -802,10 +922,11 @@ function createFieldRegistration(ctx, validate) {
802
922
  if (previousEl && el) return;
803
923
  currentFieldRef.value = el;
804
924
  const opts = ctx.fieldOptions.get(name);
805
- if (el && !opts?.controlled && el instanceof HTMLInputElement) {
925
+ const inputEl = getInputElement(el);
926
+ if (inputEl && !opts?.controlled) {
806
927
  const value = get(ctx.formData, name);
807
- if (value !== void 0) if (el.type === "checkbox") el.checked = value;
808
- else el.value = value;
928
+ if (value !== void 0) if (inputEl.type === "checkbox") inputEl.checked = value;
929
+ else inputEl.value = value;
809
930
  }
810
931
  if (previousEl && !el) {
811
932
  const timer = ctx.debounceTimers.get(name);
@@ -818,15 +939,21 @@ function createFieldRegistration(ctx, validate) {
818
939
  clearTimeout(schemaTimer);
819
940
  ctx.schemaValidationTimers.delete(name);
820
941
  }
942
+ const errorTimer = ctx.errorDelayTimers.get(name);
943
+ if (errorTimer) {
944
+ clearTimeout(errorTimer);
945
+ ctx.errorDelayTimers.delete(name);
946
+ }
947
+ ctx.pendingErrors.delete(name);
821
948
  ctx.validationRequestIds.delete(name);
949
+ ctx.fieldRefs.delete(name);
950
+ ctx.fieldOptions.delete(name);
951
+ ctx.fieldHandlers.delete(name);
822
952
  if (opts?.shouldUnregister ?? ctx.options.shouldUnregister ?? false) {
823
953
  unset(ctx.formData, name);
824
954
  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);
955
+ clearFieldTouched(ctx.touchedFields, name);
956
+ clearFieldDirty(ctx.dirtyFields, name);
830
957
  }
831
958
  }
832
959
  };
@@ -847,7 +974,7 @@ function createFieldRegistration(ctx, validate) {
847
974
  get: () => get(ctx.formData, name),
848
975
  set: (val) => {
849
976
  set(ctx.formData, name, val);
850
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
977
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, val);
851
978
  }
852
979
  }) }
853
980
  };
@@ -856,8 +983,8 @@ function createFieldRegistration(ctx, validate) {
856
983
  const opts = options || {};
857
984
  if (!opts.keepValue) unset(ctx.formData, name);
858
985
  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);
986
+ if (!opts.keepTouched) clearFieldTouched(ctx.touchedFields, name);
987
+ if (!opts.keepDirty) clearFieldDirty(ctx.dirtyFields, name);
861
988
  ctx.fieldRefs.delete(name);
862
989
  ctx.fieldOptions.delete(name);
863
990
  ctx.fieldHandlers.delete(name);
@@ -872,6 +999,8 @@ function createFieldRegistration(ctx, validate) {
872
999
  ctx.schemaValidationTimers.delete(name);
873
1000
  }
874
1001
  ctx.validationRequestIds.delete(name);
1002
+ ctx.validationCache.delete(`${name}:partial`);
1003
+ ctx.validationCache.delete(`${name}:full`);
875
1004
  }
876
1005
  return {
877
1006
  register,
@@ -907,20 +1036,6 @@ function createFieldArrayManager(ctx, validate, setFocus) {
907
1036
  indexCache.set(item.key, idx);
908
1037
  });
909
1038
  };
910
- const appendToCache = (startIndex) => {
911
- const items = fa.items.value;
912
- for (let i = startIndex; i < items.length; i++) {
913
- const item = items[i];
914
- if (item) indexCache.set(item.key, i);
915
- }
916
- };
917
- const updateCacheAfterInsert = (insertIndex, _insertCount) => {
918
- const items = fa.items.value;
919
- for (let i = insertIndex; i < items.length; i++) {
920
- const item = items[i];
921
- if (item) indexCache.set(item.key, i);
922
- }
923
- };
924
1039
  const swapInCache = (indexA, indexB) => {
925
1040
  const items = fa.items.value;
926
1041
  const itemA = items[indexA];
@@ -961,10 +1076,32 @@ function createFieldArrayManager(ctx, validate, setFocus) {
961
1076
  const normalizeToArray = (value) => {
962
1077
  return Array.isArray(value) ? value : [value];
963
1078
  };
1079
+ const clearValidationCache = () => {
1080
+ for (const key of ctx.validationCache.keys()) {
1081
+ const colonIndex = key.lastIndexOf(":");
1082
+ const fieldPath = colonIndex > 0 ? key.slice(0, colonIndex) : key;
1083
+ if (fieldPath === name || fieldPath.startsWith(`${name}.`)) ctx.validationCache.delete(key);
1084
+ }
1085
+ };
1086
+ const validateIfNeeded = () => {
1087
+ const isTouched = ctx.touchedFields.value[name] === true;
1088
+ const hasSubmitted = ctx.submitCount.value > 0;
1089
+ if (shouldValidateOnChange(ctx.options.mode ?? "onSubmit", isTouched, ctx.options.reValidateMode, hasSubmitted)) validate(name);
1090
+ };
1091
+ const ensureSync = () => {
1092
+ const currentValues = get(ctx.formData, name) || [];
1093
+ if (fa.items.value.length !== currentValues.length) {
1094
+ 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})`);
1095
+ fa.items.value = currentValues.map(() => createItem(generateId()));
1096
+ rebuildIndexCache();
1097
+ }
1098
+ return currentValues;
1099
+ };
964
1100
  const append = (value, focusOptions) => {
1101
+ clearValidationCache();
965
1102
  const values = normalizeToArray(value);
966
1103
  if (values.length === 0) return true;
967
- const currentValues = get(ctx.formData, name) || [];
1104
+ const currentValues = ensureSync();
968
1105
  const insertIndex = currentValues.length;
969
1106
  const rules = fa.rules;
970
1107
  if (rules?.maxLength && currentValues.length + values.length > rules.maxLength.value) {
@@ -977,17 +1114,22 @@ function createFieldArrayManager(ctx, validate, setFocus) {
977
1114
  const newValues = [...currentValues, ...values];
978
1115
  set(ctx.formData, name, newValues);
979
1116
  const newItems = values.map(() => createItem(generateId()));
980
- fa.items.value = [...fa.items.value, ...newItems];
981
- appendToCache(insertIndex);
982
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
983
- if (ctx.options.mode === "onChange") validate(name);
1117
+ const newItemsArray = [...fa.items.value, ...newItems];
1118
+ for (let i = insertIndex; i < newItemsArray.length; i++) {
1119
+ const item = newItemsArray[i];
1120
+ if (item) indexCache.set(item.key, i);
1121
+ }
1122
+ fa.items.value = newItemsArray;
1123
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1124
+ validateIfNeeded();
984
1125
  handleFocus(insertIndex, values.length, focusOptions);
985
1126
  return true;
986
1127
  };
987
1128
  const prepend = (value, focusOptions) => {
1129
+ clearValidationCache();
988
1130
  const values = normalizeToArray(value);
989
1131
  if (values.length === 0) return true;
990
- const currentValues = get(ctx.formData, name) || [];
1132
+ const currentValues = ensureSync();
991
1133
  const rules = fa.rules;
992
1134
  if (rules?.maxLength && currentValues.length + values.length > rules.maxLength.value) {
993
1135
  if (__DEV__) warnArrayOperationRejected("prepend", name, "maxLength", {
@@ -998,16 +1140,20 @@ function createFieldArrayManager(ctx, validate, setFocus) {
998
1140
  }
999
1141
  const newValues = [...values, ...currentValues];
1000
1142
  set(ctx.formData, name, newValues);
1001
- const newItems = values.map(() => createItem(generateId()));
1002
- fa.items.value = [...newItems, ...fa.items.value];
1003
- updateCacheAfterInsert(0, values.length);
1004
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1005
- if (ctx.options.mode === "onChange") validate(name);
1143
+ const newItemsArray = [...values.map(() => createItem(generateId())), ...fa.items.value];
1144
+ for (let i = 0; i < newItemsArray.length; i++) {
1145
+ const item = newItemsArray[i];
1146
+ if (item) indexCache.set(item.key, i);
1147
+ }
1148
+ fa.items.value = newItemsArray;
1149
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1150
+ validateIfNeeded();
1006
1151
  handleFocus(0, values.length, focusOptions);
1007
1152
  return true;
1008
1153
  };
1009
1154
  const update = (index, value) => {
1010
- const currentValues = get(ctx.formData, name) || [];
1155
+ clearValidationCache();
1156
+ const currentValues = ensureSync();
1011
1157
  if (index < 0 || index >= currentValues.length) {
1012
1158
  if (__DEV__) warnArrayIndexOutOfBounds("update", name, index, currentValues.length);
1013
1159
  return false;
@@ -1015,12 +1161,13 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1015
1161
  const newValues = [...currentValues];
1016
1162
  newValues[index] = value;
1017
1163
  set(ctx.formData, name, newValues);
1018
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1019
- if (ctx.options.mode === "onChange") validate(name);
1164
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1165
+ validateIfNeeded();
1020
1166
  return true;
1021
1167
  };
1022
1168
  const removeAt = (index) => {
1023
- const currentValues = get(ctx.formData, name) || [];
1169
+ clearValidationCache();
1170
+ const currentValues = ensureSync();
1024
1171
  if (index < 0 || index >= currentValues.length) {
1025
1172
  if (__DEV__) warnArrayIndexOutOfBounds("remove", name, index, currentValues.length);
1026
1173
  return false;
@@ -1038,14 +1185,15 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1038
1185
  const keyToRemove = fa.items.value[index]?.key;
1039
1186
  fa.items.value = fa.items.value.filter((item) => item.key !== keyToRemove);
1040
1187
  if (keyToRemove) updateCacheAfterRemove(keyToRemove, index);
1041
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1042
- if (ctx.options.mode === "onChange") validate(name);
1188
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1189
+ validateIfNeeded();
1043
1190
  return true;
1044
1191
  };
1045
1192
  const insert = (index, value, focusOptions) => {
1193
+ clearValidationCache();
1046
1194
  const values = normalizeToArray(value);
1047
1195
  if (values.length === 0) return true;
1048
- const currentValues = get(ctx.formData, name) || [];
1196
+ const currentValues = ensureSync();
1049
1197
  const rules = fa.rules;
1050
1198
  if (rules?.maxLength && currentValues.length + values.length > rules.maxLength.value) {
1051
1199
  if (__DEV__) warnArrayOperationRejected("insert", name, "maxLength", {
@@ -1054,27 +1202,35 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1054
1202
  });
1055
1203
  return false;
1056
1204
  }
1057
- const clampedIndex = Math.max(0, Math.min(index, currentValues.length));
1205
+ if (index < 0 || index > currentValues.length) {
1206
+ if (__DEV__) warnArrayIndexOutOfBounds("insert", name, index, currentValues.length);
1207
+ return false;
1208
+ }
1058
1209
  const newValues = [
1059
- ...currentValues.slice(0, clampedIndex),
1210
+ ...currentValues.slice(0, index),
1060
1211
  ...values,
1061
- ...currentValues.slice(clampedIndex)
1212
+ ...currentValues.slice(index)
1062
1213
  ];
1063
1214
  set(ctx.formData, name, newValues);
1064
1215
  const newItems = values.map(() => createItem(generateId()));
1065
- fa.items.value = [
1066
- ...fa.items.value.slice(0, clampedIndex),
1216
+ const newItemsArray = [
1217
+ ...fa.items.value.slice(0, index),
1067
1218
  ...newItems,
1068
- ...fa.items.value.slice(clampedIndex)
1219
+ ...fa.items.value.slice(index)
1069
1220
  ];
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);
1221
+ for (let i = index; i < newItemsArray.length; i++) {
1222
+ const item = newItemsArray[i];
1223
+ if (item) indexCache.set(item.key, i);
1224
+ }
1225
+ fa.items.value = newItemsArray;
1226
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1227
+ validateIfNeeded();
1228
+ handleFocus(index, values.length, focusOptions);
1074
1229
  return true;
1075
1230
  };
1076
1231
  const swap = (indexA, indexB) => {
1077
- const currentValues = get(ctx.formData, name) || [];
1232
+ clearValidationCache();
1233
+ const currentValues = ensureSync();
1078
1234
  if (indexA < 0 || indexB < 0 || indexA >= currentValues.length || indexB >= currentValues.length) {
1079
1235
  if (__DEV__) warnArrayIndexOutOfBounds("swap", name, indexA < 0 || indexA >= currentValues.length ? indexA : indexB, currentValues.length);
1080
1236
  return false;
@@ -1091,51 +1247,52 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1091
1247
  fa.items.value = newItems;
1092
1248
  swapInCache(indexA, indexB);
1093
1249
  }
1094
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1095
- if (ctx.options.mode === "onChange") validate(name);
1250
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1251
+ validateIfNeeded();
1096
1252
  return true;
1097
1253
  };
1098
1254
  const move = (from, to) => {
1099
- const currentValues = get(ctx.formData, name) || [];
1100
- if (from < 0 || from >= currentValues.length || to < 0) {
1255
+ clearValidationCache();
1256
+ const currentValues = ensureSync();
1257
+ if (from < 0 || from >= currentValues.length || to < 0 || to >= currentValues.length) {
1101
1258
  if (__DEV__) warnArrayIndexOutOfBounds("move", name, from < 0 || from >= currentValues.length ? from : to, currentValues.length);
1102
1259
  return false;
1103
1260
  }
1104
1261
  const newValues = [...currentValues];
1105
1262
  const [removed] = newValues.splice(from, 1);
1106
1263
  if (removed !== void 0) {
1107
- const clampedTo = Math.min(to, newValues.length);
1108
- newValues.splice(clampedTo, 0, removed);
1264
+ newValues.splice(to, 0, removed);
1109
1265
  set(ctx.formData, name, newValues);
1110
1266
  }
1111
1267
  const newItems = [...fa.items.value];
1112
1268
  const [removedItem] = newItems.splice(from, 1);
1113
1269
  if (removedItem) {
1114
- const clampedTo = Math.min(to, newItems.length);
1115
- newItems.splice(clampedTo, 0, removedItem);
1270
+ newItems.splice(to, 0, removedItem);
1116
1271
  fa.items.value = newItems;
1117
- const minIdx = Math.min(from, clampedTo);
1118
- const maxIdx = Math.max(from, clampedTo);
1272
+ const minIdx = Math.min(from, to);
1273
+ const maxIdx = Math.max(from, to);
1119
1274
  const items = fa.items.value;
1120
1275
  for (let i = minIdx; i <= maxIdx; i++) {
1121
1276
  const item = items[i];
1122
1277
  if (item) indexCache.set(item.key, i);
1123
1278
  }
1124
1279
  }
1125
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1126
- if (ctx.options.mode === "onChange") validate(name);
1280
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1281
+ validateIfNeeded();
1127
1282
  return true;
1128
1283
  };
1129
1284
  const replace = (newValues) => {
1285
+ clearValidationCache();
1130
1286
  if (!Array.isArray(newValues)) return false;
1131
1287
  set(ctx.formData, name, newValues);
1132
1288
  fa.items.value = newValues.map(() => createItem(generateId()));
1133
1289
  rebuildIndexCache();
1134
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1135
- if (ctx.options.mode === "onChange") validate(name);
1290
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1291
+ validateIfNeeded();
1136
1292
  return true;
1137
1293
  };
1138
1294
  const removeAll = () => {
1295
+ clearValidationCache();
1139
1296
  const rules = fa.rules;
1140
1297
  if (rules?.minLength && rules.minLength.value > 0) {
1141
1298
  if (__DEV__) warnArrayOperationRejected("removeAll", name, "minLength", {
@@ -1147,12 +1304,13 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1147
1304
  set(ctx.formData, name, []);
1148
1305
  fa.items.value = [];
1149
1306
  indexCache.clear();
1150
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1151
- if (ctx.options.mode === "onChange") validate(name);
1307
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1308
+ validateIfNeeded();
1152
1309
  return true;
1153
1310
  };
1154
1311
  const removeMany = (indices) => {
1155
- const currentValues = get(ctx.formData, name) || [];
1312
+ clearValidationCache();
1313
+ const currentValues = ensureSync();
1156
1314
  const validIndices = indices.filter((i) => i >= 0 && i < currentValues.length);
1157
1315
  if (validIndices.length === 0) return true;
1158
1316
  const rules = fa.rules;
@@ -1174,12 +1332,14 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1174
1332
  fa.items.value.forEach((item, idx) => {
1175
1333
  indexCache.set(item.key, idx);
1176
1334
  });
1177
- markFieldDirty(ctx.dirtyFields, ctx.dirtyFieldCount, name);
1178
- if (ctx.options.mode === "onChange") validate(name);
1335
+ updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, get(ctx.formData, name));
1336
+ validateIfNeeded();
1179
1337
  return true;
1180
1338
  };
1181
1339
  return {
1182
- value: fa.items.value,
1340
+ get value() {
1341
+ return fa.items.value;
1342
+ },
1183
1343
  append,
1184
1344
  prepend,
1185
1345
  remove: removeAt,
@@ -1194,20 +1354,16 @@ function createFieldArrayManager(ctx, validate, setFocus) {
1194
1354
  }
1195
1355
  return { fields };
1196
1356
  }
1197
- function syncUncontrolledInputs(fieldRefs, fieldOptions, formData) {
1198
- for (const [name, fieldRef] of Array.from(fieldRefs.entries())) {
1199
- const el = fieldRef.value;
1200
- if (el) {
1201
- if (!fieldOptions.get(name)?.controlled) set(formData, name, el.type === "checkbox" ? el.checked : el.value);
1202
- }
1203
- }
1204
- }
1205
- function updateDomElement(el, value) {
1206
- if (el.type === "checkbox") el.checked = value;
1207
- else el.value = value;
1357
+ var isCalledFromController = false;
1358
+ function setCalledFromController(value) {
1359
+ isCalledFromController = value;
1208
1360
  }
1209
1361
  function useForm(options) {
1210
1362
  const ctx = createFormContext(options);
1363
+ let isSubmissionLocked = false;
1364
+ onScopeDispose(() => {
1365
+ ctx.cleanup();
1366
+ });
1211
1367
  const { validate, clearAllPendingErrors } = createValidation(ctx);
1212
1368
  const { register, unregister } = createFieldRegistration(ctx, validate);
1213
1369
  function setFocus(name, focusOptions) {
@@ -1221,11 +1377,10 @@ function useForm(options) {
1221
1377
  }
1222
1378
  const fieldRef = ctx.fieldRefs.get(name);
1223
1379
  if (!fieldRef?.value) return;
1224
- const el = fieldRef.value;
1225
- if (typeof el.focus === "function") {
1226
- el.focus();
1227
- if (focusOptions?.shouldSelect && el instanceof HTMLInputElement && typeof el.select === "function") el.select();
1228
- }
1380
+ const el = getFocusableElement(fieldRef.value);
1381
+ if (!el) return;
1382
+ el.focus();
1383
+ if (focusOptions?.shouldSelect && el instanceof HTMLInputElement && typeof el.select === "function") el.select();
1229
1384
  }
1230
1385
  const setFocusWrapper = (name) => setFocus(name);
1231
1386
  const { fields } = createFieldArrayManager(ctx, validate, setFocusWrapper);
@@ -1250,10 +1405,10 @@ function useForm(options) {
1250
1405
  };
1251
1406
  return cachedMergedErrors;
1252
1407
  }
1253
- const isDirtyComputed = computed(() => ctx.dirtyFieldCount.value > 0);
1408
+ const isDirtyComputed = computed(() => Object.keys(ctx.dirtyFields.value).length > 0);
1254
1409
  const errorsComputed = computed(() => getMergedErrors());
1255
1410
  const isValidComputed = computed(() => {
1256
- if (!(ctx.submitCount.value > 0 || ctx.touchedFieldCount.value > 0)) return false;
1411
+ if (!(ctx.submitCount.value > 0 || Object.keys(ctx.touchedFields.value).length > 0)) return false;
1257
1412
  return Object.keys(errorsComputed.value).length === 0;
1258
1413
  });
1259
1414
  const isReadyComputed = computed(() => !ctx.isLoading.value);
@@ -1311,7 +1466,8 @@ function useForm(options) {
1311
1466
  return async (e) => {
1312
1467
  e.preventDefault();
1313
1468
  if (ctx.isDisabled.value) return;
1314
- if (ctx.isSubmitting.value) return;
1469
+ if (isSubmissionLocked) return;
1470
+ isSubmissionLocked = true;
1315
1471
  ctx.isSubmitting.value = true;
1316
1472
  ctx.submitCount.value++;
1317
1473
  ctx.isSubmitSuccessful.value = false;
@@ -1329,6 +1485,7 @@ function useForm(options) {
1329
1485
  }
1330
1486
  } finally {
1331
1487
  ctx.isSubmitting.value = false;
1488
+ isSubmissionLocked = false;
1332
1489
  }
1333
1490
  };
1334
1491
  }
@@ -1342,8 +1499,9 @@ function useForm(options) {
1342
1499
  }
1343
1500
  }
1344
1501
  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);
1502
+ if (setValueOptions?.shouldDirty === false) clearFieldDirty(ctx.dirtyFields, name);
1503
+ else updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, value);
1504
+ if (setValueOptions?.shouldTouch) markFieldTouched(ctx.touchedFields, name);
1347
1505
  if (!ctx.fieldOptions.get(name)?.controlled) {
1348
1506
  const fieldRef = ctx.fieldRefs.get(name);
1349
1507
  if (fieldRef?.value) updateDomElement(fieldRef.value, value);
@@ -1352,31 +1510,34 @@ function useForm(options) {
1352
1510
  }
1353
1511
  function reset(values, resetOptions) {
1354
1512
  const opts = resetOptions || {};
1513
+ ctx.validationCache.clear();
1355
1514
  ctx.resetGeneration.value++;
1356
1515
  clearAllPendingErrors();
1357
1516
  ctx.validatingFields.value = /* @__PURE__ */ new Set();
1358
- ctx.validationCache.clear();
1359
1517
  for (const timer of ctx.schemaValidationTimers.values()) clearTimeout(timer);
1360
1518
  ctx.schemaValidationTimers.clear();
1361
- if (!opts.keepDefaultValues && values) Object.assign(ctx.defaultValues, values);
1519
+ for (const timer of ctx.debounceTimers.values()) clearTimeout(timer);
1520
+ ctx.debounceTimers.clear();
1521
+ ctx.validationRequestIds.clear();
1522
+ if (!opts.keepDefaultValues && values) {
1523
+ Object.assign(ctx.defaultValues, values);
1524
+ ctx.defaultValueHashes.clear();
1525
+ }
1362
1526
  Object.keys(ctx.formData).forEach((key) => delete ctx.formData[key]);
1363
1527
  const newValues = deepClone(values || ctx.defaultValues);
1364
1528
  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;
1529
+ if (!opts.keepErrors) {
1530
+ ctx.errors.value = {};
1531
+ ctx.persistentErrorFields.clear();
1373
1532
  }
1533
+ if (!opts.keepTouched) ctx.touchedFields.value = {};
1534
+ if (!opts.keepDirty) ctx.dirtyFields.value = {};
1374
1535
  if (!opts.keepSubmitCount) ctx.submitCount.value = 0;
1375
1536
  if (!opts.keepIsSubmitting) ctx.isSubmitting.value = false;
1376
1537
  if (!opts.keepIsSubmitSuccessful) ctx.isSubmitSuccessful.value = false;
1377
1538
  ctx.fieldArrays.clear();
1378
1539
  for (const [name, fieldRef] of Array.from(ctx.fieldRefs.entries())) {
1379
- const el = fieldRef.value;
1540
+ const el = getInputElement(fieldRef.value);
1380
1541
  if (el) {
1381
1542
  const value = get(newValues, name);
1382
1543
  if (value !== void 0) if (el.type === "checkbox") el.checked = value;
@@ -1395,6 +1556,8 @@ function useForm(options) {
1395
1556
  }
1396
1557
  const opts = resetFieldOptions || {};
1397
1558
  ctx.resetGeneration.value++;
1559
+ ctx.validationCache.delete(`${name}:partial`);
1560
+ ctx.validationCache.delete(`${name}:full`);
1398
1561
  const errorTimer = ctx.errorDelayTimers.get(name);
1399
1562
  if (errorTimer) {
1400
1563
  clearTimeout(errorTimer);
@@ -1403,15 +1566,21 @@ function useForm(options) {
1403
1566
  ctx.pendingErrors.delete(name);
1404
1567
  let defaultValue = opts.defaultValue;
1405
1568
  if (defaultValue === void 0) defaultValue = get(ctx.defaultValues, name);
1406
- else set(ctx.defaultValues, name, defaultValue);
1569
+ else {
1570
+ set(ctx.defaultValues, name, defaultValue);
1571
+ ctx.defaultValueHashes.set(name, hashValue(defaultValue));
1572
+ }
1407
1573
  const clonedValue = defaultValue !== void 0 ? deepClone(defaultValue) : void 0;
1408
1574
  set(ctx.formData, name, clonedValue);
1409
1575
  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);
1576
+ if (!opts.keepDirty) clearFieldDirty(ctx.dirtyFields, name);
1577
+ if (!opts.keepTouched) clearFieldTouched(ctx.touchedFields, name);
1412
1578
  if (!ctx.fieldOptions.get(name)?.controlled) {
1413
1579
  const fieldRef = ctx.fieldRefs.get(name);
1414
- if (fieldRef?.value) updateDomElement(fieldRef.value, clonedValue ?? (fieldRef.value.type === "checkbox" ? false : ""));
1580
+ if (fieldRef?.value) {
1581
+ const el = getInputElement(fieldRef.value);
1582
+ updateDomElement(fieldRef.value, clonedValue ?? (el?.type === "checkbox" ? false : ""));
1583
+ }
1415
1584
  }
1416
1585
  }
1417
1586
  function watch$1(name) {
@@ -1451,10 +1620,16 @@ function useForm(options) {
1451
1620
  }
1452
1621
  if (name === void 0) {
1453
1622
  ctx.errors.value = {};
1623
+ ctx.externalErrors.value = {};
1624
+ ctx.persistentErrorFields.clear();
1454
1625
  return;
1455
1626
  }
1456
1627
  const fieldsToClean = Array.isArray(name) ? name : [name];
1457
- for (const field of fieldsToClean) clearFieldErrors(ctx.errors, field);
1628
+ for (const field of fieldsToClean) {
1629
+ clearFieldErrors(ctx.errors, field);
1630
+ clearFieldErrors(ctx.externalErrors, field);
1631
+ ctx.persistentErrorFields.delete(field);
1632
+ }
1458
1633
  }
1459
1634
  function setError(name, error) {
1460
1635
  const newErrors = { ...ctx.errors.value };
@@ -1463,6 +1638,7 @@ function useForm(options) {
1463
1638
  message: error.message
1464
1639
  } : error.message);
1465
1640
  ctx.errors.value = newErrors;
1641
+ if (error.persistent) ctx.persistentErrorFields.add(name);
1466
1642
  }
1467
1643
  function setErrors(errors, options$1) {
1468
1644
  const newErrors = options$1?.shouldReplace ? {} : { ...ctx.errors.value };
@@ -1515,6 +1691,7 @@ function useForm(options) {
1515
1691
  const schemaResult = validatePathAgainstSchema(ctx.options.schema, name);
1516
1692
  if (!schemaResult.valid) warnPathNotInSchema("getFieldState", name, schemaResult.availableFields);
1517
1693
  }
1694
+ if (getCurrentInstance() && !isCalledFromController) console.warn(`[vue-hook-form] getFieldState('${name}') returns a snapshot, not reactive refs.\nFor reactive error display, use one of these alternatives:\n • formState.value.errors.${name}\n • const { fieldState } = useController({ name: '${name}', control })\n • const ${name}Error = computed(() => formState.value.errors.${name})\n\nSee docs: https://github.com/vuehookform/core#common-mistakes`);
1518
1695
  }
1519
1696
  const error = get(ctx.errors.value, name);
1520
1697
  return {
@@ -1524,7 +1701,7 @@ function useForm(options) {
1524
1701
  error
1525
1702
  };
1526
1703
  }
1527
- async function trigger(name) {
1704
+ async function trigger(name, options$1) {
1528
1705
  if (__DEV__ && name) {
1529
1706
  const names = Array.isArray(name) ? name : [name];
1530
1707
  for (const n of names) {
@@ -1536,6 +1713,7 @@ function useForm(options) {
1536
1713
  }
1537
1714
  }
1538
1715
  }
1716
+ if (options$1?.markAsSubmitted) ctx.submitCount.value++;
1539
1717
  if (name === void 0) return await validate();
1540
1718
  if (Array.isArray(name)) {
1541
1719
  let allValid = true;
@@ -1563,7 +1741,11 @@ function useForm(options) {
1563
1741
  getValues,
1564
1742
  getFieldState,
1565
1743
  trigger,
1566
- setFocus
1744
+ setFocus,
1745
+ options: {
1746
+ mode: ctx.options.mode ?? "onSubmit",
1747
+ reValidateMode: ctx.options.reValidateMode
1748
+ }
1567
1749
  };
1568
1750
  }
1569
1751
  const FormContextKey = Symbol("FormContext");
@@ -1602,13 +1784,22 @@ function useController(options) {
1602
1784
  }
1603
1785
  });
1604
1786
  const onChange = (newValue) => {
1605
- form.setValue(name, newValue);
1787
+ const isTouched = form.formState.value.touchedFields[name] === true;
1788
+ const hasSubmitted = form.formState.value.submitCount > 0;
1789
+ const mode = form.options.mode ?? "onSubmit";
1790
+ const reValidateMode = form.options.reValidateMode;
1791
+ const shouldValidate = shouldValidateOnChange(mode, isTouched, reValidateMode, hasSubmitted);
1792
+ form.setValue(name, newValue, { shouldValidate });
1606
1793
  };
1607
1794
  const onBlur = () => {
1795
+ const hasSubmitted = form.formState.value.submitCount > 0;
1796
+ const mode = form.options.mode ?? "onSubmit";
1797
+ const reValidateMode = form.options.reValidateMode;
1798
+ const shouldValidate = shouldValidateOnBlur(mode, hasSubmitted, reValidateMode);
1608
1799
  const currentValue = form.getValues(name);
1609
1800
  form.setValue(name, currentValue, {
1610
1801
  shouldTouch: true,
1611
- shouldValidate: true,
1802
+ shouldValidate,
1612
1803
  shouldDirty: false
1613
1804
  });
1614
1805
  };
@@ -1616,7 +1807,10 @@ function useController(options) {
1616
1807
  elementRef.value = el;
1617
1808
  };
1618
1809
  const fieldState = computed(() => {
1619
- return form.getFieldState(name);
1810
+ setCalledFromController(true);
1811
+ const state = form.getFieldState(name);
1812
+ setCalledFromController(false);
1813
+ return state;
1620
1814
  });
1621
1815
  return {
1622
1816
  field: {
@@ -1646,4 +1840,4 @@ function useFormState(options = {}) {
1646
1840
  function isFieldError(error) {
1647
1841
  return typeof error === "object" && error !== null && "type" in error && "message" in error && typeof error.type === "string" && typeof error.message === "string";
1648
1842
  }
1649
- export { FormContextKey, isFieldError, provideForm, useController, useForm, useFormContext, useFormState, useWatch };
1843
+ export { FormContextKey, clearPathCache, generateId, get, isFieldError, provideForm, set, unset, useController, useForm, useFormContext, useFormState, useWatch };