@vuehookform/core 0.1.1 → 0.2.0

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