@teamnovu/kit-vue-forms 0.1.16 → 0.1.18

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.
@@ -44,5 +44,5 @@ export interface Form<T extends FormDataDefault> {
44
44
  reset: () => void;
45
45
  validateForm: () => Promise<ValidationResult>;
46
46
  submitHandler: (onSubmit: (data: T) => Awaitable<void>) => (event: SubmitEvent) => Promise<void>;
47
- getSubForm: <P extends EntityPaths<T>>(path: P, options?: SubformOptions<PickEntity<T, P>>) => Form<PickEntity<T, P>>;
47
+ getSubForm: <P extends EntityPaths<T>>(path: P, options?: SubformOptions<PickEntity<T, P>>) => Omit<Form<PickEntity<T, P>>, 'submitHandler'>;
48
48
  }
@@ -8,7 +8,7 @@ export declare function existsFieldPath<T, K extends Paths<T>>(field: FormField<
8
8
  export declare function getNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>): PickProps<T, K>;
9
9
  export declare function setNestedValue<T, K extends Paths<T>>(obj: MaybeRef<T>, path: K | SplitPath<K>, value: PickProps<T, K>): void;
10
10
  export declare const getLens: <T, K extends Paths<T>>(data: MaybeRef<T>, key: MaybeRef<K | SplitPath<K>>) => WritableComputedRef<PickProps<T, K>, PickProps<T, K>>;
11
- type JoinPath<Base extends string, Sub extends string> = `${Base}${Base extends "" ? "" : Sub extends "" ? "" : "."}${Sub}`;
11
+ type JoinPath<Base extends string, Sub extends string> = `${Base}${Base extends '' ? '' : Sub extends '' ? '' : '.'}${Sub}`;
12
12
  export declare function joinPath<Base extends string, Sub extends string>(basePath: Base, subPath: Sub): JoinPath<Base, Sub>;
13
13
  export declare function filterErrorsForPath(errors: ErrorBag, path: string): ErrorBag;
14
14
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamnovu/kit-vue-forms",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -26,7 +26,7 @@
26
26
  "@vitest/ui": "^2.0.0",
27
27
  "happy-dom": "^12.0.0",
28
28
  "vitest": "^2.0.0",
29
- "vue": "^3.5.13"
29
+ "vue": "^3.5.25"
30
30
  },
31
31
  "dependencies": {
32
32
  "@vueuse/core": "^13.5.0",
@@ -1,3 +1,4 @@
1
+ import type { Awaitable } from "@vueuse/core";
1
2
  import {
2
3
  computed,
3
4
  onScopeDispose,
@@ -6,65 +7,69 @@ import {
6
7
  toRef,
7
8
  triggerRef,
8
9
  unref,
9
- watchEffect,
10
+ watch,
10
11
  type MaybeRef,
11
- } from 'vue'
12
- import type { FieldsTuple, FormDataDefault, FormField } from '../types/form'
13
- import type { Paths, PickProps } from '../types/util'
14
- import { existsPath, getLens, getNestedValue } from '../utils/path'
15
- import { Rc } from '../utils/rc'
16
- import { useField, type UseFieldOptions } from './useField'
17
- import type { ValidationState } from './useValidation'
18
- import type { Awaitable } from '@vueuse/core'
12
+ } from "vue";
13
+ import type { FieldsTuple, FormDataDefault, FormField } from "../types/form";
14
+ import type { Paths, PickProps } from "../types/util";
15
+ import { existsPath, getLens, getNestedValue } from "../utils/path";
16
+ import { Rc } from "../utils/rc";
17
+ import { useField, type UseFieldOptions } from "./useField";
18
+ import type { ValidationState } from "./useValidation";
19
19
 
20
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
- type FieldRegistryCache<T> = Map<Paths<T>, FormField<any, string>>
21
+ type FieldRegistryCache<T> = Map<Paths<T>, FormField<any, string>>;
22
22
 
23
23
  export type ResolvedFormField<T, K extends Paths<T>> = FormField<
24
24
  PickProps<T, K>,
25
25
  K
26
- >
26
+ >;
27
27
 
28
28
  export type DefineFieldOptions<F, K extends string> = Pick<
29
29
  UseFieldOptions<F, K>,
30
- 'path'
30
+ "path"
31
31
  > & {
32
- onBlur?: () => void
33
- onFocus?: () => void
34
- }
32
+ onBlur?: () => void;
33
+ onFocus?: () => void;
34
+ };
35
35
 
36
36
  interface FormState<
37
37
  T extends FormDataDefault,
38
38
  TIn extends FormDataDefault = T,
39
39
  > {
40
- data: T
41
- initialData: TIn
40
+ data: T;
41
+ initialData: TIn;
42
42
  }
43
43
 
44
44
  interface FieldRegistryOptions {
45
- keepValuesOnUnmount?: MaybeRef<boolean>
46
- onBlur?: (path: string) => Awaitable<void>
47
- onFocus?: (path: string) => Awaitable<void>
45
+ keepValuesOnUnmount?: MaybeRef<boolean>;
46
+ onBlur?: (path: string) => Awaitable<void>;
47
+ onFocus?: (path: string) => Awaitable<void>;
48
48
  }
49
49
 
50
50
  const optionDefaults = {
51
51
  keepValuesOnUnmount: true,
52
- }
52
+ };
53
53
 
54
54
  // A computed that always reflects the latest value from the getter
55
55
  // This computed forces updates even if the value is the same (to trigger watchers)
56
- function alwaysComputed<T>(getter: () => T) {
57
- const initialValueRef = shallowRef(getter())
56
+ function initialDataSync<T extends FormDataDefault>(
57
+ formState: FormState<T>,
58
+ path: Paths<T>,
59
+ ) {
60
+ const getNewValue = () => getNestedValue(formState.initialData, path);
61
+ const initialValueRef = shallowRef(getNewValue());
58
62
 
59
- watchEffect(
63
+ watch(
64
+ () => formState.initialData,
60
65
  () => {
61
- initialValueRef.value = getter()
62
- triggerRef(initialValueRef)
66
+ initialValueRef.value = getNewValue();
67
+ triggerRef(initialValueRef);
63
68
  },
64
- { flush: 'sync' },
65
- )
69
+ { flush: "sync" },
70
+ );
66
71
 
67
- return initialValueRef
72
+ return initialValueRef;
68
73
  }
69
74
 
70
75
  export function useFieldRegistry<T extends FormDataDefault>(
@@ -72,104 +77,99 @@ export function useFieldRegistry<T extends FormDataDefault>(
72
77
  validationState: ValidationState<T>,
73
78
  fieldRegistryOptions?: FieldRegistryOptions,
74
79
  ) {
75
- const fieldReferenceCounter = new Map<Paths<T>, Rc>()
76
- const fields = shallowReactive(new Map()) as FieldRegistryCache<T>
80
+ const fieldReferenceCounter = new Map<Paths<T>, Rc>();
81
+ const fields = shallowReactive(new Map()) as FieldRegistryCache<T>;
77
82
  const registryOptions = {
78
83
  ...optionDefaults,
79
84
  ...fieldRegistryOptions,
80
- }
85
+ };
81
86
 
82
87
  const registerField = <K extends Paths<T>>(
83
88
  field: ResolvedFormField<T, K>,
84
89
  ) => {
85
- const path = unref(field.path) as Paths<T>
86
- fields.set(path, field)
87
- }
90
+ const path = unref(field.path) as Paths<T>;
91
+ fields.set(path, field);
92
+ };
88
93
 
89
94
  const deregisterField = (path: Paths<T>) => {
90
95
  if (!registryOptions?.keepValuesOnUnmount) {
91
- fields.get(path)?.reset()
96
+ fields.get(path)?.reset();
92
97
  }
93
- fields.delete(path)
94
- }
98
+ fields.delete(path);
99
+ };
95
100
 
96
101
  const track = (path: Paths<T>) => {
97
102
  if (!fieldReferenceCounter.has(path)) {
98
- fieldReferenceCounter.set(path, new Rc(() => deregisterField(path)))
103
+ fieldReferenceCounter.set(path, new Rc(() => deregisterField(path)));
99
104
  } else {
100
- fieldReferenceCounter.get(path)?.inc()
105
+ fieldReferenceCounter.get(path)?.inc();
101
106
  }
102
- }
107
+ };
103
108
 
104
109
  const untrack = (path: Paths<T>) => {
105
110
  if (fieldReferenceCounter.has(path)) {
106
- fieldReferenceCounter.get(path)?.dec()
111
+ fieldReferenceCounter.get(path)?.dec();
107
112
  }
108
- }
113
+ };
109
114
 
110
115
  const getField = <K extends Paths<T>>(
111
116
  options: DefineFieldOptions<PickProps<T, K>, K>,
112
117
  ): ResolvedFormField<T, K> => {
113
- const { path } = options
118
+ const { path } = options;
114
119
 
115
120
  if (!fields.has(path)) {
116
121
  const field = useField({
117
122
  path,
118
- value: getLens(toRef(formState, 'data'), path),
119
- initialValue: alwaysComputed(() =>
120
- getNestedValue(formState.initialData, path)),
123
+ value: getLens(toRef(formState, "data"), path),
124
+ initialValue: initialDataSync(formState, path),
121
125
  existsInForm: computed(() => existsPath(formState.data, unref(path))),
122
126
  errors: computed({
123
127
  get() {
124
- return validationState.errors.value.propertyErrors[path] || []
128
+ return validationState.errors.value.propertyErrors[path] || [];
125
129
  },
126
130
  set(newErrors) {
127
- validationState.errors.value.propertyErrors[path] = newErrors
131
+ validationState.errors.value.propertyErrors[path] = newErrors;
128
132
  },
129
133
  }),
130
134
  onBlur: async () => {
131
- await Promise.all(
132
- [
133
- registryOptions?.onBlur?.(unref(path)),
134
- options.onBlur?.(),
135
- ],
136
- )
135
+ await Promise.all([
136
+ registryOptions?.onBlur?.(unref(path)),
137
+ options.onBlur?.(),
138
+ ]);
137
139
  },
138
140
  onFocus: async () => {
139
- await Promise.all(
140
- [
141
- registryOptions?.onFocus?.(unref(path)),
142
- options.onFocus?.(),
143
- ],
144
- )
141
+ await Promise.all([
142
+ registryOptions?.onFocus?.(unref(path)),
143
+ options.onFocus?.(),
144
+ ]);
145
145
  },
146
- })
146
+ });
147
147
 
148
- registerField(field)
148
+ registerField(field);
149
149
  }
150
150
 
151
- const field = fields.get(path) as ResolvedFormField<T, K>
151
+ const field = fields.get(path) as ResolvedFormField<T, K>;
152
152
 
153
- track(path)
153
+ track(path);
154
154
 
155
155
  // Clean up field on unmount
156
156
  onScopeDispose(() => {
157
- untrack(path)
158
- })
157
+ untrack(path);
158
+ });
159
159
 
160
- return field
161
- }
160
+ return field;
161
+ };
162
162
 
163
163
  const defineField = <K extends Paths<T>>(
164
164
  options: DefineFieldOptions<PickProps<T, K>, K>,
165
165
  ): ResolvedFormField<T, K> => {
166
- const field = getField(options)
166
+ const field = getField(options);
167
167
 
168
168
  // TODO: If more options are ever needed than only the path we have to update the field
169
169
  // here with the new options
170
170
 
171
- return field
172
- }
171
+ return field;
172
+ };
173
173
 
174
174
  return {
175
175
  fields: computed(() => [...fields.values()] as FieldsTuple<T>),
@@ -177,9 +177,9 @@ export function useFieldRegistry<T extends FormDataDefault>(
177
177
  registerField,
178
178
  deregisterField,
179
179
  defineField,
180
- }
180
+ };
181
181
  }
182
182
 
183
183
  export type FieldRegistry<T extends FormDataDefault> = ReturnType<
184
184
  typeof useFieldRegistry<T>
185
- >
185
+ >;
@@ -18,8 +18,7 @@ import { useFieldRegistry } from "./useFieldRegistry";
18
18
  import { useFormState } from "./useFormState";
19
19
  import { createSubformInterface, type SubformOptions } from "./useSubform";
20
20
  import { useValidation, type ValidationOptions } from "./useValidation";
21
-
22
- // TODO @Elias implement validation strategy handling
21
+ import { useSubmitHandler } from "./useSubmitHandler";
23
22
 
24
23
  export interface UseFormOptions<T extends FormDataDefault>
25
24
  extends ValidationOptions<T> {
@@ -29,9 +28,7 @@ export interface UseFormOptions<T extends FormDataDefault>
29
28
  }
30
29
 
31
30
  export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
32
- const initialData = computed(() =>
33
- Object.freeze(cloneRefValue(options.initialData)),
34
- );
31
+ const initialData = computed(() => cloneRefValue(options.initialData));
35
32
 
36
33
  const data = ref<T>(cloneRefValue(initialData)) as Ref<T>;
37
34
 
@@ -53,29 +50,12 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
53
50
  keepValuesOnUnmount: options.keepValuesOnUnmount,
54
51
  onBlur: async (path: string) => {
55
52
  if (unref(options.validationStrategy) === "onTouch") {
56
- // TODO: Only validate the specific field that was touched
57
53
  validationState.validateField(path);
58
54
  }
59
55
  },
60
56
  });
61
57
  const formState = useFormState(fieldRegistry);
62
58
 
63
- const submitHandler = (onSubmit: (data: T) => Awaitable<void>) => {
64
- return async (event: SubmitEvent) => {
65
- event.preventDefault();
66
-
67
- if (unref(options.validationStrategy) !== "none") {
68
- await validationState.validateForm();
69
- }
70
-
71
- if (!validationState.isValid.value) {
72
- return;
73
- }
74
-
75
- await onSubmit(state.data);
76
- };
77
- };
78
-
79
59
  const reset = () => {
80
60
  data.value = cloneRefValue(initialData);
81
61
  validationState.reset();
@@ -86,25 +66,29 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
86
66
 
87
67
  function getSubForm<K extends EntityPaths<T>>(
88
68
  path: K,
89
- options?: SubformOptions<PickEntity<T, K>>,
90
- ): Form<PickEntity<T, K>> {
91
- return createSubformInterface(formInterface, path, options);
69
+ subformOptions?: SubformOptions<PickEntity<T, K>>,
70
+ ): Omit<Form<PickEntity<T, K>>, "submitHandler"> {
71
+ return createSubformInterface(formInterface, path, options, subformOptions);
92
72
  }
93
73
 
94
- const formInterface: Form<T> = {
74
+ const formInterface: Omit<Form<T>, "submitHandler"> = {
95
75
  ...fieldRegistry,
96
76
  ...validationState,
97
77
  ...formState,
98
78
  reset,
99
79
  getSubForm,
100
- submitHandler,
101
80
  initialData: toRef(state, "initialData") as Form<T>["initialData"],
102
81
  data: toRef(state, "data") as Form<T>["data"],
103
82
  };
104
83
 
84
+ const submitHandler = useSubmitHandler(formInterface, options);
85
+
105
86
  if (unref(options.validationStrategy) === "onFormOpen") {
106
87
  validationState.validateForm();
107
88
  }
108
89
 
109
- return formInterface;
90
+ return {
91
+ ...formInterface,
92
+ submitHandler,
93
+ };
110
94
  }
@@ -1,30 +1,47 @@
1
- import { computed, isRef, unref, type Ref } from 'vue'
2
- import type { FieldsTuple, Form, FormDataDefault, FormField } from '../types/form'
3
- import type { EntityPaths, Paths, PickEntity, PickProps } from '../types/util'
4
- import type { ValidationResult, Validator } from '../types/validation'
5
- import { filterErrorsForPath, getLens, getNestedValue, joinPath } from '../utils/path'
6
- import type { DefineFieldOptions } from './useFieldRegistry'
7
- import { createValidator, SuccessValidationResult, type ValidatorOptions } from './useValidation'
1
+ import { computed, isRef, unref, type Ref } from "vue";
2
+ import type {
3
+ FieldsTuple,
4
+ Form,
5
+ FormDataDefault,
6
+ FormField,
7
+ } from "../types/form";
8
+ import type { EntityPaths, Paths, PickEntity, PickProps } from "../types/util";
9
+ import type { ValidationResult, Validator } from "../types/validation";
10
+ import {
11
+ filterErrorsForPath,
12
+ getLens,
13
+ getNestedValue,
14
+ joinPath,
15
+ } from "../utils/path";
16
+ import type { DefineFieldOptions } from "./useFieldRegistry";
17
+ import type { UseFormOptions } from "./useForm";
18
+ import { useSubmitHandler } from "./useSubmitHandler";
19
+ import {
20
+ createValidator,
21
+ SuccessValidationResult,
22
+ type ValidatorOptions,
23
+ } from "./useValidation";
8
24
 
9
25
  export interface SubformOptions<_T extends FormDataDefault> {
10
26
  // Additional subform-specific options can be added here
11
27
  }
12
28
 
13
- class NestedValidator<T extends FormDataDefault, P extends Paths<T>> implements Validator<T> {
29
+ class NestedValidator<T extends FormDataDefault, P extends Paths<T>>
30
+ implements Validator<T>
31
+ {
14
32
  constructor(
15
33
  private path: P,
16
34
  private validator: Validator<PickEntity<T, P>> | undefined,
17
- ) {
18
- }
35
+ ) {}
19
36
 
20
37
  async validate(data: T): Promise<ValidationResult> {
21
- const subFormData = getNestedValue(data, this.path) as PickEntity<T, P>
38
+ const subFormData = getNestedValue(data, this.path) as PickEntity<T, P>;
22
39
 
23
40
  if (!this.validator) {
24
- return SuccessValidationResult
41
+ return SuccessValidationResult;
25
42
  }
26
43
 
27
- const validationResult = await this.validator.validate(subFormData)
44
+ const validationResult = await this.validator.validate(subFormData);
28
45
 
29
46
  return {
30
47
  isValid: validationResult.isValid,
@@ -32,14 +49,13 @@ class NestedValidator<T extends FormDataDefault, P extends Paths<T>> implements
32
49
  general: validationResult.errors.general || [],
33
50
  propertyErrors: validationResult.errors.propertyErrors
34
51
  ? Object.fromEntries(
35
- Object.entries(validationResult.errors.propertyErrors).map(([key, errors]) => [
36
- joinPath(this.path, key),
37
- errors,
38
- ]),
52
+ Object.entries(validationResult.errors.propertyErrors).map(
53
+ ([key, errors]) => [joinPath(this.path, key), errors],
54
+ ),
39
55
  )
40
56
  : {},
41
57
  },
42
- }
58
+ };
43
59
  }
44
60
  }
45
61
 
@@ -47,21 +63,22 @@ export function createSubformInterface<
47
63
  T extends FormDataDefault,
48
64
  K extends EntityPaths<T>,
49
65
  >(
50
- mainForm: Form<T>,
66
+ mainForm: Omit<Form<T>, "submitHandler">,
51
67
  path: K,
68
+ formOptions?: UseFormOptions<T>,
52
69
  _options?: SubformOptions<PickEntity<T, K>>,
53
70
  ): Form<PickEntity<T, K>> {
54
- type ST = PickEntity<T, K>
55
- type SP = Paths<ST>
56
- type MP<P extends SP> = `${K}.${P}`
57
- type ScopedMainPaths = Paths<T> & MP<SP>
71
+ type ST = PickEntity<T, K>;
72
+ type SP = Paths<ST>;
73
+ type MP<P extends SP> = `${K}.${P}`;
74
+ type ScopedMainPaths = Paths<T> & MP<SP>;
58
75
 
59
76
  // Create reactive data scoped to subform path
60
- const data = getLens(mainForm.data, path) as Ref<ST>
77
+ const data = getLens(mainForm.data, path) as Ref<ST>;
61
78
 
62
79
  const initialData = computed(() => {
63
- return getNestedValue(mainForm.initialData.value, path) as ST
64
- })
80
+ return getNestedValue(mainForm.initialData.value, path) as ST;
81
+ });
65
82
 
66
83
  const adaptMainFormField = <S extends SP>(
67
84
  field: FormField<PickProps<T, ScopedMainPaths>, ScopedMainPaths>,
@@ -69,92 +86,111 @@ export function createSubformInterface<
69
86
  // Where P ist the full path in the main form, we need to adapt it to the subform's path
70
87
  return {
71
88
  ...field,
72
- path: computed(() => unref(field.path).replace(path + '.', '')),
89
+ path: computed(() => unref(field.path).replace(path + ".", "")),
73
90
  setData: (newData: PickProps<ST, S>) => {
74
- field.setData(newData as PickProps<T, ScopedMainPaths>)
91
+ field.setData(newData as PickProps<T, ScopedMainPaths>);
75
92
  },
76
- } as unknown as FormField<PickProps<ST, S>, S>
77
- }
93
+ } as unknown as FormField<PickProps<ST, S>, S>;
94
+ };
78
95
 
79
96
  const getField = <P extends SP>(fieldPath: P) => {
80
- const fullPath = joinPath(path, fieldPath)
81
- const mainFormField = mainForm.getField(fullPath as ScopedMainPaths)
97
+ const fullPath = joinPath(path, fieldPath);
98
+ const mainFormField = mainForm.getField(fullPath as ScopedMainPaths);
82
99
 
83
100
  if (!mainFormField) {
84
- return {} as FormField<PickProps<ST, P>, P>
101
+ return {} as FormField<PickProps<ST, P>, P>;
85
102
  }
86
103
 
87
- return adaptMainFormField<P>(mainFormField)
88
- }
104
+ return adaptMainFormField<P>(mainFormField);
105
+ };
89
106
 
90
107
  // Field operations with path transformation
91
- const defineField = <P extends SP>(fieldOptions: DefineFieldOptions<ST, P>) => {
92
- const fullPath = joinPath(path, fieldOptions.path)
108
+ const defineField = <P extends SP>(
109
+ fieldOptions: DefineFieldOptions<ST, P>,
110
+ ) => {
111
+ const fullPath = joinPath(path, fieldOptions.path);
93
112
 
94
113
  const mainField = mainForm.defineField({
95
114
  ...fieldOptions,
96
115
  path: fullPath as ScopedMainPaths,
97
- })
116
+ });
98
117
 
99
- return adaptMainFormField<P>(mainField)
100
- }
118
+ return adaptMainFormField<P>(mainField);
119
+ };
101
120
 
102
121
  const fields = computed(<P extends SP>() => {
103
- return (mainForm.fields.value as FormField<PickProps<T, ScopedMainPaths>, ScopedMainPaths>[])
122
+ return (
123
+ mainForm.fields.value as FormField<
124
+ PickProps<T, ScopedMainPaths>,
125
+ ScopedMainPaths
126
+ >[]
127
+ )
104
128
  .filter((field) => {
105
- const fieldPath = field.path.value
106
- return fieldPath.startsWith(path + '.') || fieldPath === path
129
+ const fieldPath = field.path.value;
130
+ return fieldPath.startsWith(path + ".") || fieldPath === path;
107
131
  })
108
- .map(field => adaptMainFormField(field)) as FieldsTuple<ST, P>
109
- })
132
+ .map((field) => adaptMainFormField(field)) as FieldsTuple<ST, P>;
133
+ });
110
134
 
111
135
  // Helper function to get all fields without type parameter
112
136
  const getAllSubformFields = () => {
113
- return (mainForm.fields.value as FormField<PickProps<T, ScopedMainPaths>, ScopedMainPaths>[])
114
- .filter((field) => {
115
- const fieldPath = field.path.value
116
- return fieldPath.startsWith(path + '.') || fieldPath === path
117
- })
118
- }
137
+ return (
138
+ mainForm.fields.value as FormField<
139
+ PickProps<T, ScopedMainPaths>,
140
+ ScopedMainPaths
141
+ >[]
142
+ ).filter((field) => {
143
+ const fieldPath = field.path.value;
144
+ return fieldPath.startsWith(path + ".") || fieldPath === path;
145
+ });
146
+ };
119
147
 
120
148
  // State computed from main form with path filtering
121
- const isDirty = computed(() => getAllSubformFields().some(field => field.dirty.value))
122
- const isTouched = computed(() => getAllSubformFields().some(field => field.touched.value))
149
+ const isDirty = computed(() =>
150
+ getAllSubformFields().some((field) => field.dirty.value),
151
+ );
152
+ const isTouched = computed(() =>
153
+ getAllSubformFields().some((field) => field.touched.value),
154
+ );
123
155
 
124
156
  // Validation delegates to main form
125
- const isValid = computed(() => mainForm.isValid.value)
126
- const isValidated = computed(() => mainForm.isValidated.value)
127
- const errors = computed(() => filterErrorsForPath(unref(mainForm.errors), path))
157
+ const isValid = computed(() => mainForm.isValid.value);
158
+ const isValidated = computed(() => mainForm.isValidated.value);
159
+ const errors = computed(() =>
160
+ filterErrorsForPath(unref(mainForm.errors), path),
161
+ );
128
162
 
129
- const validateForm = () => mainForm.validateForm()
163
+ const validateForm = () => mainForm.validateForm();
130
164
 
131
165
  // Nested subforms
132
166
  const getSubForm = <P extends EntityPaths<ST>>(
133
167
  subPath: P,
134
168
  subOptions?: SubformOptions<PickEntity<ST, P>>,
135
169
  ) => {
136
- const fullPath = joinPath(path, subPath) as EntityPaths<T>
170
+ const fullPath = joinPath(path, subPath) as EntityPaths<T>;
137
171
  return mainForm.getSubForm(
138
172
  fullPath,
139
173
  subOptions as SubformOptions<PickEntity<T, typeof fullPath>>,
140
- ) as Form<PickEntity<ST, P>>
141
- }
174
+ ) as Form<PickEntity<ST, P>>;
175
+ };
142
176
 
143
177
  // Reset scoped to this subform
144
- const reset = () => getAllSubformFields().forEach(field => field.reset())
178
+ const reset = () => getAllSubformFields().forEach((field) => field.reset());
145
179
 
146
- const defineValidator = (options: ValidatorOptions<ST> | Ref<Validator<ST>>) => {
147
- const subValidator = isRef(options) ? options : createValidator(options)
180
+ const defineValidator = (
181
+ options: ValidatorOptions<ST> | Ref<Validator<ST>>,
182
+ ) => {
183
+ const subValidator = isRef(options) ? options : createValidator(options);
148
184
  const validator = computed(
149
185
  () => new NestedValidator<T, K>(path, unref(subValidator)),
150
- )
186
+ );
151
187
 
152
- mainForm.defineValidator(validator)
188
+ mainForm.defineValidator(validator);
153
189
 
154
- return subValidator
155
- }
190
+ return subValidator;
191
+ };
156
192
 
157
- return {
193
+ const subFormInterface: Omit<Form<ST>, "submitHandler"> = {
158
194
  data: data,
159
195
  fields,
160
196
  initialData,
@@ -169,5 +205,12 @@ export function createSubformInterface<
169
205
  reset,
170
206
  validateForm,
171
207
  getSubForm,
172
- }
208
+ };
209
+
210
+ const submitHandler = useSubmitHandler(subFormInterface, formOptions ?? {});
211
+
212
+ return {
213
+ ...subFormInterface,
214
+ submitHandler,
215
+ };
173
216
  }