@vuehookform/core 0.6.0 → 0.7.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,9 +1,16 @@
1
1
  import { FormContext } from './formContext';
2
- import { RegisterOptions, RegisterReturn, UnregisterOptions, Path } from '../types';
2
+ import { RegisterOptions, RegisterReturn, UnregisterOptions, SetValueOptions, Path } from '../types';
3
+ /**
4
+ * Helpers object for onChange callbacks.
5
+ * Populated after useForm's setValue is defined (mutable reference pattern).
6
+ */
7
+ export interface OnChangeHelpers {
8
+ setValue: (name: string, value: unknown, options?: SetValueOptions) => void;
9
+ }
3
10
  /**
4
11
  * Create field registration functions
5
12
  */
6
- export declare function createFieldRegistration<FormValues>(ctx: FormContext<FormValues>, validate: (fieldPath?: string) => Promise<boolean>): {
13
+ export declare function createFieldRegistration<FormValues>(ctx: FormContext<FormValues>, validate: (fieldPath?: string) => Promise<boolean>, onChangeHelpers?: OnChangeHelpers): {
7
14
  register: <TPath extends Path<FormValues>>(name: TPath, registerOptions?: RegisterOptions) => RegisterReturn;
8
15
  unregister: <TPath extends Path<FormValues>>(name: TPath, options?: UnregisterOptions) => void;
9
16
  };
package/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export { provideForm, useFormContext, FormContextKey } from './context';
19
19
  export { useWatch, type UseWatchOptions } from './useWatch';
20
20
  export { useController, type UseControllerOptions, type UseControllerReturn, type ControllerFieldProps, type LooseControllerOptions, } from './useController';
21
21
  export { useFormState, type UseFormStateOptions, type FormStateKey } from './useFormState';
22
+ export { useFieldError, type UseFieldErrorOptions, type LooseFieldErrorOptions, } from './useFieldError';
22
23
  export { isFieldError } from './types';
23
24
  export type { UseFormOptions, UseFormReturn, UseFormReturn as Control, LooseControl, RegisterOptions, RegisterReturn, FormState, FieldState, FieldErrors, FieldError, FieldErrorValue, ErrorOption, SetErrorsOptions, FieldArray, FieldArrayItem, FieldArrayOptions, FieldArrayRules, FieldArrayFocusOptions, InferSchema, FormValues, FormPath, Path, PathValue, ArrayElement, ArrayPath, FieldPath, ValidationMode, SetFocusOptions, ResetOptions, ResetFieldOptions, AsyncDefaultValues, TriggerOptions, SetValueOptions, UnregisterOptions, CriteriaMode, } from './types';
24
25
  export { get, set, unset, clearPathCache } from './utils/paths';
package/dist/types.d.ts CHANGED
@@ -186,6 +186,10 @@ export interface FormState<T> {
186
186
  isSubmitSuccessful: boolean;
187
187
  /** Whether the form is disabled */
188
188
  disabled: boolean;
189
+ /** Whether form values match default values (opposite of isDirty) */
190
+ isPristine: boolean;
191
+ /** Whether the form can be submitted (isValid && !isSubmitting && !isLoading && !disabled) */
192
+ canSubmit: boolean;
189
193
  }
190
194
  /**
191
195
  * State of an individual field
@@ -328,6 +332,26 @@ export interface RegisterOptions<TValue = unknown> {
328
332
  shouldUnregister?: boolean;
329
333
  /** Dependent fields to re-validate when this field changes */
330
334
  deps?: string[];
335
+ /**
336
+ * Called after the field value changes via user input.
337
+ * Use for side effects like resetting dependent fields.
338
+ * Does NOT fire on programmatic setValue() calls.
339
+ *
340
+ * @param value - The new field value (typed based on field path)
341
+ * @param helpers - Helper functions for triggering side effects
342
+ *
343
+ * @example Reset dependent field when parent changes
344
+ * ```ts
345
+ * register('country', {
346
+ * onChange: (country, { setValue }) => {
347
+ * setValue('province', '') // Reset province when country changes
348
+ * },
349
+ * })
350
+ * ```
351
+ */
352
+ onChange?: (value: TValue, helpers: {
353
+ setValue: (name: string, value: unknown, options?: SetValueOptions) => void;
354
+ }) => void;
331
355
  }
332
356
  /**
333
357
  * Return value from register() for binding to inputs.
@@ -0,0 +1,54 @@
1
+ import { ComputedRef } from 'vue';
2
+ import { ZodType } from 'zod';
3
+ import { UseFormReturn, Path, InferSchema, LooseControl } from './types';
4
+ /**
5
+ * Options for useFieldError composable
6
+ */
7
+ export interface UseFieldErrorOptions<TSchema extends ZodType, TPath extends Path<InferSchema<TSchema>>> {
8
+ /** Field path to get the error for */
9
+ name: TPath;
10
+ /** Form control from useForm (uses context if not provided) */
11
+ control?: UseFormReturn<TSchema>;
12
+ }
13
+ /**
14
+ * Loose options for useFieldError when schema type is unknown.
15
+ */
16
+ export interface LooseFieldErrorOptions {
17
+ /** Field path as a string */
18
+ name: string;
19
+ /** Form control from useForm (uses context if not provided) */
20
+ control?: LooseControl;
21
+ }
22
+ /**
23
+ * Get a reactive error message for a single field.
24
+ *
25
+ * Returns a ComputedRef that automatically updates when the field's
26
+ * error state changes. Normalizes both string errors and structured
27
+ * FieldError objects to a plain string message.
28
+ *
29
+ * For field array paths with per-item errors (not a single array-level error),
30
+ * returns `undefined`. Use `formState.value.errors` directly for item-level errors.
31
+ *
32
+ * @example Basic usage with context
33
+ * ```vue
34
+ * <script setup>
35
+ * const emailError = useFieldError({ name: 'email' })
36
+ * </script>
37
+ * <template>
38
+ * <span v-if="emailError" class="error">{{ emailError }}</span>
39
+ * </template>
40
+ * ```
41
+ *
42
+ * @example With explicit control
43
+ * ```ts
44
+ * const form = useForm({ schema })
45
+ * const emailError = useFieldError({ name: 'email', control: form })
46
+ * ```
47
+ *
48
+ * @example Nested path
49
+ * ```ts
50
+ * const cityError = useFieldError({ name: 'user.address.city' })
51
+ * ```
52
+ */
53
+ export declare function useFieldError(options: LooseFieldErrorOptions): ComputedRef<string | undefined>;
54
+ export declare function useFieldError<TSchema extends ZodType, TPath extends Path<InferSchema<TSchema>>>(options: UseFieldErrorOptions<TSchema, TPath>): ComputedRef<string | undefined>;
@@ -23,17 +23,16 @@ export interface UseFormStateOptions<TSchema extends ZodType> {
23
23
  * @example
24
24
  * ```ts
25
25
  * // Subscribe to all form state
26
- * const formState = useFormState({})
26
+ * const state = useFormState({})
27
+ * // Access: state.value.errors, state.value.isDirty, etc.
27
28
  *
28
29
  * // Subscribe to specific properties
29
- * const { isSubmitting, errors } = useFormState({ name: ['isSubmitting', 'errors'] })
30
- *
31
- * // Subscribe to single property
32
- * const isDirty = useFormState({ name: 'isDirty' })
30
+ * const state = useFormState({ name: ['isSubmitting', 'errors'] })
31
+ * // Access: state.value.isSubmitting, state.value.errors
33
32
  *
34
33
  * // With explicit control
35
34
  * const { control } = useForm({ schema })
36
- * const formState = useFormState({ control })
35
+ * const state = useFormState({ control })
37
36
  * ```
38
37
  */
39
38
  export declare function useFormState<TSchema extends ZodType>(options?: UseFormStateOptions<TSchema>): ComputedRef<Partial<FormState<InferSchema<TSchema>>>>;
@@ -10,7 +10,7 @@ export interface UseWatchOptions<TSchema extends ZodType, TPath extends Path<Inf
10
10
  /** Field path or array of paths to watch (watches all if not provided) */
11
11
  name?: TPath | TPath[];
12
12
  /** Default value when field is undefined */
13
- defaultValue?: unknown;
13
+ defaultValue?: PathValue<InferSchema<TSchema>, TPath>;
14
14
  }
15
15
  /**
16
16
  * Watch form field values reactively without the full form instance
@@ -818,7 +818,7 @@ function shouldValidateOnBlur(mode, hasSubmitted, reValidateMode) {
818
818
  return mode === "onBlur" || mode === "onTouched" || hasSubmitted && (reValidateMode === "onBlur" || reValidateMode === "onTouched");
819
819
  }
820
820
  var validationRequestCounter = 0;
821
- function createFieldRegistration(ctx, validate) {
821
+ function createFieldRegistration(ctx, validate, onChangeHelpers) {
822
822
  function register(name, registerOptions) {
823
823
  if (__DEV__) {
824
824
  const syntaxError = validatePathSyntax(name);
@@ -864,6 +864,11 @@ function createFieldRegistration(ctx, validate) {
864
864
  set(ctx.formData, name, value);
865
865
  updateFieldDirtyState(ctx.dirtyFields, ctx.defaultValues, ctx.defaultValueHashes, name, value);
866
866
  const fieldOpts = ctx.fieldOptions.get(name);
867
+ if (fieldOpts?.onChange && onChangeHelpers?.setValue) try {
868
+ fieldOpts.onChange(value, { setValue: onChangeHelpers.setValue });
869
+ } catch (err) {
870
+ if (__DEV__) console.error(`[vue-hook-form] Error in onChange callback for field '${name}':`, err);
871
+ }
867
872
  if (shouldValidateOnChange(ctx.options.mode ?? "onSubmit", ctx.touchedFields.value[name] === true, ctx.submitCount.value > 0, ctx.options.reValidateMode)) {
868
873
  const validationDebounceMs = ctx.options.validationDebounce || 0;
869
874
  if (validationDebounceMs > 0) {
@@ -1423,7 +1428,8 @@ function useForm(options) {
1423
1428
  ctx.cleanup();
1424
1429
  });
1425
1430
  const { validate, clearAllPendingErrors } = createValidation(ctx);
1426
- const { register, unregister } = createFieldRegistration(ctx, validate);
1431
+ const onChangeHelpers = { setValue: null };
1432
+ const { register, unregister } = createFieldRegistration(ctx, validate, onChangeHelpers);
1427
1433
  function setFocus(name, focusOptions) {
1428
1434
  if (__DEV__) {
1429
1435
  const syntaxError = validatePathSyntax(name);
@@ -1514,6 +1520,12 @@ function useForm(options) {
1514
1520
  },
1515
1521
  get disabled() {
1516
1522
  return ctx.isDisabled.value;
1523
+ },
1524
+ get isPristine() {
1525
+ return !isDirtyComputed.value;
1526
+ },
1527
+ get canSubmit() {
1528
+ return isValidComputed.value && !ctx.isSubmitting.value && !ctx.isLoading.value && !ctx.isDisabled.value;
1517
1529
  }
1518
1530
  });
1519
1531
  const formState = (0, vue.computed)(() => formStateInternal);
@@ -1563,6 +1575,7 @@ function useForm(options) {
1563
1575
  }
1564
1576
  if (setValueOptions?.shouldValidate) validate(name);
1565
1577
  }
1578
+ onChangeHelpers.setValue = setValue;
1566
1579
  function reset(values, resetOptions) {
1567
1580
  const opts = resetOptions || {};
1568
1581
  ctx.validationCache.clear();
@@ -1828,25 +1841,28 @@ function useFormContext() {
1828
1841
  function useWatch(options = {}) {
1829
1842
  const { control, name, defaultValue } = options;
1830
1843
  const form = control ?? useFormContext();
1831
- return (0, vue.computed)(() => {
1832
- if (name === void 0) return form.getValues();
1833
- if (Array.isArray(name)) {
1834
- const result = {};
1835
- for (const fieldName of name) result[fieldName] = get(form.getValues(), fieldName) ?? defaultValue;
1844
+ if (name === void 0) return form.watch();
1845
+ if (Array.isArray(name)) {
1846
+ const watched$1 = form.watch(name);
1847
+ if (defaultValue === void 0) return watched$1;
1848
+ return (0, vue.computed)(() => {
1849
+ const result = { ...watched$1.value };
1850
+ for (const fieldName of name) if (result[fieldName] === void 0) result[fieldName] = defaultValue;
1836
1851
  return result;
1837
- }
1838
- return get(form.getValues(), name) ?? defaultValue;
1839
- });
1852
+ });
1853
+ }
1854
+ const watched = form.watch(name);
1855
+ if (defaultValue === void 0) return watched;
1856
+ return (0, vue.computed)(() => watched.value ?? defaultValue);
1840
1857
  }
1841
1858
  function useController(options) {
1842
1859
  const { name, control, defaultValue } = options;
1843
1860
  const form = control ?? useFormContext();
1844
1861
  const elementRef = (0, vue.ref)(null);
1845
1862
  if (defaultValue !== void 0 && form.getValues(name) === void 0) form.setValue(name, defaultValue);
1863
+ const watchedValue = form.watch(name);
1846
1864
  const value = (0, vue.computed)({
1847
- get: () => {
1848
- return form.getValues(name) ?? defaultValue;
1849
- },
1865
+ get: () => watchedValue.value ?? defaultValue,
1850
1866
  set: (newValue) => {
1851
1867
  form.setValue(name, newValue);
1852
1868
  }
@@ -1905,6 +1921,19 @@ function useFormState(options = {}) {
1905
1921
  return { [name]: fullState[name] };
1906
1922
  });
1907
1923
  }
1924
+ function useFieldError(options) {
1925
+ const form = options.control ?? useFormContext();
1926
+ return (0, vue.computed)(() => {
1927
+ const error = get(form.formState.value.errors, options.name);
1928
+ if (!error) return void 0;
1929
+ if (typeof error === "string") return error;
1930
+ if (Array.isArray(error)) {
1931
+ if (__DEV__) console.warn(`[vue-hook-form] useFieldError('${options.name}') resolved to an array of per-item errors.\nuseFieldError only returns scalar error messages. Use formState.value.errors['${options.name}'] directly for item-level errors.`);
1932
+ return;
1933
+ }
1934
+ if (typeof error === "object" && "message" in error) return error.message;
1935
+ });
1936
+ }
1908
1937
  function isFieldError(error) {
1909
1938
  return typeof error === "object" && error !== null && "type" in error && "message" in error && typeof error.type === "string" && typeof error.message === "string";
1910
1939
  }
@@ -1916,6 +1945,7 @@ exports.provideForm = provideForm;
1916
1945
  exports.set = set;
1917
1946
  exports.unset = unset;
1918
1947
  exports.useController = useController;
1948
+ exports.useFieldError = useFieldError;
1919
1949
  exports.useForm = useForm;
1920
1950
  exports.useFormContext = useFormContext;
1921
1951
  exports.useFormState = useFormState;