@teamnovu/kit-vue-forms 0.1.13 → 0.1.15
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.
- package/dist/components/FormFieldWrapper.vue.d.ts +1 -1
- package/dist/composables/useField.d.ts +5 -2
- package/dist/composables/useFieldRegistry.d.ts +13 -4
- package/dist/composables/useFormData.d.ts +5 -0
- package/dist/index.js +370 -324
- package/dist/types/form.d.ts +3 -1
- package/docs/reference.md +0 -2
- package/package.json +1 -1
- package/src/components/FormFieldWrapper.vue +8 -0
- package/src/composables/useField.ts +9 -3
- package/src/composables/useFieldRegistry.ts +80 -14
- package/src/composables/useForm.ts +53 -10
- package/src/composables/useFormData.ts +1 -1
- package/src/composables/useValidation.ts +1 -1
- package/src/types/form.ts +23 -8
- package/src/utils/path.ts +7 -1
- package/tests/subform.test.ts +17 -0
- package/tests/useField.test.ts +17 -0
- package/tests/useForm.test.ts +313 -201
- package/tests/useValidation.test.ts +73 -1
package/dist/types/form.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { Awaitable } from '@vueuse/core';
|
|
1
2
|
import { Ref } from 'vue';
|
|
2
3
|
import { DefineFieldOptions } from '../composables/useFieldRegistry';
|
|
3
4
|
import { SubformOptions } from '../composables/useSubform';
|
|
5
|
+
import { ValidatorOptions } from '../composables/useValidation';
|
|
4
6
|
import { EntityPaths, Paths, PickEntity, PickProps } from './util';
|
|
5
7
|
import { ErrorBag, ValidationErrorMessage, ValidationErrors, ValidationResult, Validator } from './validation';
|
|
6
|
-
import { ValidatorOptions } from '../composables/useValidation';
|
|
7
8
|
export type FormDataDefault = object;
|
|
8
9
|
export interface FormField<T, P extends string> {
|
|
9
10
|
data: Ref<T>;
|
|
@@ -42,5 +43,6 @@ export interface Form<T extends FormDataDefault> {
|
|
|
42
43
|
defineValidator: <TData extends T>(options: ValidatorOptions<TData> | Ref<Validator<TData>>) => Ref<Validator<TData> | undefined>;
|
|
43
44
|
reset: () => void;
|
|
44
45
|
validateForm: () => Promise<ValidationResult>;
|
|
46
|
+
submitHandler: (onSubmit: (data: T) => Awaitable<void>) => (event: SubmitEvent) => Promise<void>;
|
|
45
47
|
getSubForm: <P extends EntityPaths<T>>(path: P, options?: SubformOptions<PickEntity<T, P>>) => Form<PickEntity<T, P>>;
|
|
46
48
|
}
|
package/docs/reference.md
CHANGED
|
@@ -17,10 +17,8 @@ function useForm<T extends object>(options: {
|
|
|
17
17
|
// this is additional to the schema for custom validations
|
|
18
18
|
validateFn?: MaybeRef<ValidationFunction<T>>
|
|
19
19
|
// if the form data of a property should be reset or kept if all fields corresponding to this property are unmounted
|
|
20
|
-
// !! currently not implemented !!
|
|
21
20
|
keepValuesOnUnmount?: MaybeRef<boolean>
|
|
22
21
|
// when validation should be done (on touch, pre submit, etc.)
|
|
23
|
-
// !! currently not implemented !!
|
|
24
22
|
validationStrategy?: MaybeRef<ValidationStrategy>
|
|
25
23
|
}): Form<T>
|
|
26
24
|
```
|
package/package.json
CHANGED
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
@update:model-value="setData"
|
|
14
14
|
>
|
|
15
15
|
<slot />
|
|
16
|
+
|
|
17
|
+
<!-- https://vue-land.github.io/faq/forwarding-slots#passing-all-slots -->
|
|
18
|
+
<template
|
|
19
|
+
v-for="(_, slotName) in $slots"
|
|
20
|
+
#[slotName]="slotProps"
|
|
21
|
+
>
|
|
22
|
+
<slot :name="slotName" v-bind="slotProps ?? {}" />
|
|
23
|
+
</template>
|
|
16
24
|
</component>
|
|
17
25
|
</Field>
|
|
18
26
|
</template>
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { computed, reactive, shallowRef, toRefs, watch, type MaybeRef, type MaybeRefOrGetter, type Ref
|
|
1
|
+
import { computed, reactive, shallowRef, toRefs, watch, type MaybeRef, type MaybeRefOrGetter, type Ref } from 'vue'
|
|
2
2
|
import type { FormField } from '../types/form'
|
|
3
3
|
import type { ValidationErrorMessage, ValidationErrors } from '../types/validation'
|
|
4
4
|
import { cloneRefValue } from '../utils/general'
|
|
5
|
+
import type { Awaitable } from '@vueuse/core'
|
|
5
6
|
|
|
6
7
|
export interface UseFieldOptions<T, K extends string> {
|
|
7
8
|
value?: MaybeRef<T>
|
|
8
9
|
initialValue?: MaybeRefOrGetter<Readonly<T>>
|
|
9
10
|
path: K
|
|
10
|
-
errors?:
|
|
11
|
+
errors?: Ref<ValidationErrors>
|
|
12
|
+
onBlur?: () => Awaitable<void>
|
|
13
|
+
onFocus?: () => Awaitable<void>
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export function useField<T, K extends string>(options: UseFieldOptions<T, K>): FormField<T, K> {
|
|
@@ -40,10 +43,13 @@ export function useField<T, K extends string>(options: UseFieldOptions<T, K>): F
|
|
|
40
43
|
|
|
41
44
|
const onBlur = (): void => {
|
|
42
45
|
state.touched = true
|
|
46
|
+
state.errors = []
|
|
47
|
+
|
|
48
|
+
options.onBlur?.()
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
const onFocus = (): void => {
|
|
46
|
-
|
|
52
|
+
options.onFocus?.()
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
const reset = (): void => {
|
|
@@ -1,23 +1,56 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
onScopeDispose,
|
|
4
|
+
shallowReactive,
|
|
5
|
+
shallowRef,
|
|
6
|
+
toRef,
|
|
7
|
+
triggerRef,
|
|
8
|
+
unref,
|
|
9
|
+
watchEffect,
|
|
10
|
+
type MaybeRef,
|
|
11
|
+
} from 'vue'
|
|
2
12
|
import type { FieldsTuple, FormDataDefault, FormField } from '../types/form'
|
|
3
13
|
import type { Paths, PickProps } from '../types/util'
|
|
4
14
|
import { getLens, getNestedValue } from '../utils/path'
|
|
5
15
|
import { Rc } from '../utils/rc'
|
|
6
16
|
import { useField, type UseFieldOptions } from './useField'
|
|
7
17
|
import type { ValidationState } from './useValidation'
|
|
18
|
+
import type { Awaitable } from '@vueuse/core'
|
|
8
19
|
|
|
9
20
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
21
|
type FieldRegistryCache<T> = Map<Paths<T>, FormField<any, string>>
|
|
11
22
|
|
|
12
|
-
export type ResolvedFormField<T, K extends Paths<T>> = FormField<
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
export type ResolvedFormField<T, K extends Paths<T>> = FormField<
|
|
24
|
+
PickProps<T, K>,
|
|
25
|
+
K
|
|
26
|
+
>
|
|
27
|
+
|
|
28
|
+
export type DefineFieldOptions<F, K extends string> = Pick<
|
|
29
|
+
UseFieldOptions<F, K>,
|
|
30
|
+
'path'
|
|
31
|
+
> & {
|
|
32
|
+
onBlur?: () => void
|
|
33
|
+
onFocus?: () => void
|
|
34
|
+
}
|
|
15
35
|
|
|
16
|
-
interface FormState<
|
|
36
|
+
interface FormState<
|
|
37
|
+
T extends FormDataDefault,
|
|
38
|
+
TIn extends FormDataDefault = T,
|
|
39
|
+
> {
|
|
17
40
|
data: T
|
|
18
41
|
initialData: TIn
|
|
19
42
|
}
|
|
20
43
|
|
|
44
|
+
interface FieldRegistryOptions {
|
|
45
|
+
keepValuesOnUnmount?: MaybeRef<boolean>
|
|
46
|
+
onBlur?: (path: string) => Awaitable<void>
|
|
47
|
+
onFocus?: (path: string) => Awaitable<void>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const optionDefaults = {
|
|
51
|
+
keepValuesOnUnmount: true,
|
|
52
|
+
}
|
|
53
|
+
|
|
21
54
|
// A computed that always reflects the latest value from the getter
|
|
22
55
|
// This computed forces updates even if the value is the same (to trigger watchers)
|
|
23
56
|
function alwaysComputed<T>(getter: () => T) {
|
|
@@ -37,16 +70,26 @@ function alwaysComputed<T>(getter: () => T) {
|
|
|
37
70
|
export function useFieldRegistry<T extends FormDataDefault>(
|
|
38
71
|
formState: FormState<T>,
|
|
39
72
|
validationState: ValidationState<T>,
|
|
73
|
+
fieldRegistryOptions?: FieldRegistryOptions,
|
|
40
74
|
) {
|
|
41
75
|
const fieldReferenceCounter = new Map<Paths<T>, Rc>()
|
|
42
76
|
const fields = shallowReactive(new Map()) as FieldRegistryCache<T>
|
|
77
|
+
const registryOptions = {
|
|
78
|
+
...optionDefaults,
|
|
79
|
+
...fieldRegistryOptions,
|
|
80
|
+
}
|
|
43
81
|
|
|
44
|
-
const registerField = <K extends Paths<T>>(
|
|
82
|
+
const registerField = <K extends Paths<T>>(
|
|
83
|
+
field: ResolvedFormField<T, K>,
|
|
84
|
+
) => {
|
|
45
85
|
const path = unref(field.path) as Paths<T>
|
|
46
86
|
fields.set(path, field)
|
|
47
87
|
}
|
|
48
88
|
|
|
49
89
|
const deregisterField = (path: Paths<T>) => {
|
|
90
|
+
if (!registryOptions?.keepValuesOnUnmount) {
|
|
91
|
+
fields.get(path)?.reset()
|
|
92
|
+
}
|
|
50
93
|
fields.delete(path)
|
|
51
94
|
}
|
|
52
95
|
|
|
@@ -64,14 +107,17 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
64
107
|
}
|
|
65
108
|
}
|
|
66
109
|
|
|
67
|
-
const getField = <K extends Paths<T>>(
|
|
110
|
+
const getField = <K extends Paths<T>>(
|
|
111
|
+
options: DefineFieldOptions<PickProps<T, K>, K>,
|
|
112
|
+
): ResolvedFormField<T, K> => {
|
|
113
|
+
const { path } = options
|
|
114
|
+
|
|
68
115
|
if (!fields.has(path)) {
|
|
69
116
|
const field = useField({
|
|
70
117
|
path,
|
|
71
118
|
value: getLens(toRef(formState, 'data'), path),
|
|
72
|
-
initialValue: alwaysComputed(
|
|
73
|
-
|
|
74
|
-
),
|
|
119
|
+
initialValue: alwaysComputed(() =>
|
|
120
|
+
getNestedValue(formState.initialData, path)),
|
|
75
121
|
errors: computed({
|
|
76
122
|
get() {
|
|
77
123
|
return validationState.errors.value.propertyErrors[path] || []
|
|
@@ -80,6 +126,22 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
80
126
|
validationState.errors.value.propertyErrors[path] = newErrors
|
|
81
127
|
},
|
|
82
128
|
}),
|
|
129
|
+
onBlur: async () => {
|
|
130
|
+
await Promise.all(
|
|
131
|
+
[
|
|
132
|
+
registryOptions?.onBlur?.(unref(path)),
|
|
133
|
+
options.onBlur?.(),
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
},
|
|
137
|
+
onFocus: async () => {
|
|
138
|
+
await Promise.all(
|
|
139
|
+
[
|
|
140
|
+
registryOptions?.onFocus?.(unref(path)),
|
|
141
|
+
options.onFocus?.(),
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
},
|
|
83
145
|
})
|
|
84
146
|
|
|
85
147
|
registerField(field)
|
|
@@ -97,8 +159,10 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
97
159
|
return field
|
|
98
160
|
}
|
|
99
161
|
|
|
100
|
-
const defineField = <K extends Paths<T>>(
|
|
101
|
-
|
|
162
|
+
const defineField = <K extends Paths<T>>(
|
|
163
|
+
options: DefineFieldOptions<PickProps<T, K>, K>,
|
|
164
|
+
): ResolvedFormField<T, K> => {
|
|
165
|
+
const field = getField(options)
|
|
102
166
|
|
|
103
167
|
// TODO: If more options are ever needed than only the path we have to update the field
|
|
104
168
|
// here with the new options
|
|
@@ -108,11 +172,13 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
108
172
|
|
|
109
173
|
return {
|
|
110
174
|
fields: computed(() => [...fields.values()] as FieldsTuple<T>),
|
|
111
|
-
getField,
|
|
175
|
+
getField: <P extends Paths<T>>(path: P) => getField({ path }),
|
|
112
176
|
registerField,
|
|
113
177
|
deregisterField,
|
|
114
178
|
defineField,
|
|
115
179
|
}
|
|
116
180
|
}
|
|
117
181
|
|
|
118
|
-
export type FieldRegistry<T extends FormDataDefault> = ReturnType<
|
|
182
|
+
export type FieldRegistry<T extends FormDataDefault> = ReturnType<
|
|
183
|
+
typeof useFieldRegistry<T>
|
|
184
|
+
>
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { Awaitable } from '@vueuse/core'
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
reactive,
|
|
5
|
+
ref,
|
|
6
|
+
toRef,
|
|
7
|
+
unref,
|
|
8
|
+
watch,
|
|
9
|
+
type MaybeRef,
|
|
10
|
+
type MaybeRefOrGetter,
|
|
11
|
+
type Ref,
|
|
12
|
+
} from 'vue'
|
|
2
13
|
import type { AnyField, Form, FormDataDefault } from '../types/form'
|
|
3
14
|
import type { EntityPaths, PickEntity } from '../types/util'
|
|
4
15
|
import type { ValidationStrategy } from '../types/validation'
|
|
@@ -10,14 +21,16 @@ import { useValidation, type ValidationOptions } from './useValidation'
|
|
|
10
21
|
|
|
11
22
|
// TODO @Elias implement validation strategy handling
|
|
12
23
|
|
|
13
|
-
export interface UseFormOptions<T extends FormDataDefault>
|
|
24
|
+
export interface UseFormOptions<T extends FormDataDefault>
|
|
25
|
+
extends ValidationOptions<T> {
|
|
14
26
|
initialData: MaybeRefOrGetter<T>
|
|
15
27
|
validationStrategy?: MaybeRef<ValidationStrategy>
|
|
16
28
|
keepValuesOnUnmount?: MaybeRef<boolean>
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
20
|
-
const initialData = computed(() =>
|
|
32
|
+
const initialData = computed(() =>
|
|
33
|
+
Object.freeze(cloneRefValue(options.initialData)))
|
|
21
34
|
|
|
22
35
|
const data = ref<T>(cloneRefValue(initialData)) as Ref<T>
|
|
23
36
|
|
|
@@ -26,20 +39,45 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
|
26
39
|
data,
|
|
27
40
|
})
|
|
28
41
|
|
|
29
|
-
watch(
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
watch(
|
|
43
|
+
initialData,
|
|
44
|
+
(newValue) => {
|
|
45
|
+
state.data = cloneRefValue(newValue)
|
|
46
|
+
},
|
|
47
|
+
{ flush: 'sync' },
|
|
48
|
+
)
|
|
32
49
|
|
|
33
50
|
const validationState = useValidation(state, options)
|
|
34
|
-
const fieldRegistry = useFieldRegistry(state, validationState
|
|
51
|
+
const fieldRegistry = useFieldRegistry(state, validationState, {
|
|
52
|
+
keepValuesOnUnmount: options.keepValuesOnUnmount,
|
|
53
|
+
onBlur: async () => {
|
|
54
|
+
if (unref(options.validationStrategy) === 'onTouch') {
|
|
55
|
+
validationState.validateForm()
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
})
|
|
35
59
|
const formState = useFormState(fieldRegistry)
|
|
36
60
|
|
|
61
|
+
const submitHandler = (onSubmit: (data: T) => Awaitable<void>) => {
|
|
62
|
+
return async (event: SubmitEvent) => {
|
|
63
|
+
event.preventDefault()
|
|
64
|
+
|
|
65
|
+
if (unref(options.validationStrategy) !== 'none') {
|
|
66
|
+
await validationState.validateForm()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!validationState.isValid.value) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await onSubmit(state.data)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
37
77
|
const reset = () => {
|
|
38
78
|
data.value = cloneRefValue(initialData)
|
|
39
79
|
validationState.reset()
|
|
40
|
-
fieldRegistry.fields.value.forEach(
|
|
41
|
-
(field: AnyField<T>) => field.reset(),
|
|
42
|
-
)
|
|
80
|
+
fieldRegistry.fields.value.forEach((field: AnyField<T>) => field.reset())
|
|
43
81
|
}
|
|
44
82
|
|
|
45
83
|
function getSubForm<K extends EntityPaths<T>>(
|
|
@@ -55,9 +93,14 @@ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
|
|
|
55
93
|
...formState,
|
|
56
94
|
reset,
|
|
57
95
|
getSubForm,
|
|
96
|
+
submitHandler,
|
|
58
97
|
initialData: toRef(state, 'initialData') as Form<T>['initialData'],
|
|
59
98
|
data: toRef(state, 'data') as Form<T>['data'],
|
|
60
99
|
}
|
|
61
100
|
|
|
101
|
+
if (unref(options.validationStrategy) === 'onFormOpen') {
|
|
102
|
+
validationState.validateForm()
|
|
103
|
+
}
|
|
104
|
+
|
|
62
105
|
return formInterface
|
|
63
106
|
}
|
|
@@ -4,7 +4,7 @@ import type { FormDataDefault } from '../types/form'
|
|
|
4
4
|
export function useFormData<T extends FormDataDefault>(
|
|
5
5
|
initialData: Ref<T>,
|
|
6
6
|
) {
|
|
7
|
-
const data = ref(unref(initialData))
|
|
7
|
+
const data = ref(unref(initialData)) as Ref<T>
|
|
8
8
|
|
|
9
9
|
watch(initialData, (newData) => {
|
|
10
10
|
if (newData !== data.value) {
|
|
@@ -157,7 +157,7 @@ export function useValidation<T extends FormDataDefault>(
|
|
|
157
157
|
)
|
|
158
158
|
|
|
159
159
|
// Watch for changes in form data to trigger validation
|
|
160
|
-
watch(() => formState.data, () => {
|
|
160
|
+
watch([() => formState.data, () => unref(options.schema)], () => {
|
|
161
161
|
if (validationState.isValidated) {
|
|
162
162
|
validateForm()
|
|
163
163
|
}
|
package/src/types/form.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import type { Awaitable } from '@vueuse/core'
|
|
1
2
|
import type { Ref } from 'vue'
|
|
2
3
|
import type { DefineFieldOptions } from '../composables/useFieldRegistry'
|
|
3
4
|
import type { SubformOptions } from '../composables/useSubform'
|
|
4
|
-
import type { EntityPaths, Paths, PickEntity, PickProps } from './util'
|
|
5
|
-
import type { ErrorBag, ValidationErrorMessage, ValidationErrors, ValidationResult, Validator } from './validation'
|
|
6
5
|
import type { ValidatorOptions } from '../composables/useValidation'
|
|
6
|
+
import type { EntityPaths, Paths, PickEntity, PickProps } from './util'
|
|
7
|
+
import type {
|
|
8
|
+
ErrorBag,
|
|
9
|
+
ValidationErrorMessage,
|
|
10
|
+
ValidationErrors,
|
|
11
|
+
ValidationResult,
|
|
12
|
+
Validator,
|
|
13
|
+
} from './validation'
|
|
7
14
|
|
|
8
15
|
export type FormDataDefault = object
|
|
9
16
|
|
|
@@ -27,13 +34,13 @@ export interface FormField<T, P extends string> {
|
|
|
27
34
|
clearErrors: () => void
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
export type FieldsTuple<T, TPaths = Paths<T>> = [
|
|
31
|
-
TPaths extends infer P
|
|
37
|
+
export type FieldsTuple<T, TPaths = Paths<T>> = [
|
|
38
|
+
...(TPaths extends infer P
|
|
32
39
|
? P extends string
|
|
33
40
|
? FormField<PickProps<T, P>, P>
|
|
34
41
|
: never
|
|
35
|
-
: never
|
|
36
|
-
|
|
42
|
+
: never)[],
|
|
43
|
+
]
|
|
37
44
|
|
|
38
45
|
export type AnyField<T> = FormField<PickProps<T, Paths<T>>, Paths<T>>
|
|
39
46
|
|
|
@@ -45,7 +52,9 @@ export interface Form<T extends FormDataDefault> {
|
|
|
45
52
|
fields: Ref<FieldsTuple<T>>
|
|
46
53
|
|
|
47
54
|
// Field operations
|
|
48
|
-
defineField: <P extends Paths<T>>(
|
|
55
|
+
defineField: <P extends Paths<T>>(
|
|
56
|
+
options: DefineFieldOptions<PickProps<T, P>, P>,
|
|
57
|
+
) => FormField<PickProps<T, P>, P>
|
|
49
58
|
getField: <P extends Paths<T>>(path: P) => FormField<PickProps<T, P>, P>
|
|
50
59
|
|
|
51
60
|
// State properties
|
|
@@ -55,12 +64,18 @@ export interface Form<T extends FormDataDefault> {
|
|
|
55
64
|
isValidated: Ref<boolean>
|
|
56
65
|
errors: Ref<ErrorBag>
|
|
57
66
|
|
|
58
|
-
defineValidator: <TData extends T>(
|
|
67
|
+
defineValidator: <TData extends T>(
|
|
68
|
+
options: ValidatorOptions<TData> | Ref<Validator<TData>>,
|
|
69
|
+
) => Ref<Validator<TData> | undefined>
|
|
59
70
|
|
|
60
71
|
// Operations
|
|
61
72
|
reset: () => void
|
|
62
73
|
validateForm: () => Promise<ValidationResult>
|
|
63
74
|
|
|
75
|
+
submitHandler: (
|
|
76
|
+
onSubmit: (data: T) => Awaitable<void>,
|
|
77
|
+
) => (event: SubmitEvent) => Promise<void>
|
|
78
|
+
|
|
64
79
|
// Nested subforms
|
|
65
80
|
getSubForm: <P extends EntityPaths<T>>(
|
|
66
81
|
path: P,
|
package/src/utils/path.ts
CHANGED
|
@@ -33,7 +33,13 @@ export function setNestedValue<T, K extends Paths<T>>(obj: MaybeRef<T>, path: K
|
|
|
33
33
|
const target = keys
|
|
34
34
|
.slice(0, -1)
|
|
35
35
|
.reduce(
|
|
36
|
-
(current, key) =>
|
|
36
|
+
(current, key) => {
|
|
37
|
+
if (current?.[key] === undefined) {
|
|
38
|
+
// Create the nested object if it doesn't exist
|
|
39
|
+
current[key] = {}
|
|
40
|
+
}
|
|
41
|
+
return current?.[key]
|
|
42
|
+
},
|
|
37
43
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
44
|
unref(obj) as Record<string, any>,
|
|
39
45
|
)
|
package/tests/subform.test.ts
CHANGED
|
@@ -1031,6 +1031,23 @@ describe('Subform Implementation', () => {
|
|
|
1031
1031
|
expect(result.isValid).toBe(false)
|
|
1032
1032
|
expect(result.errors.general).toContain('Data is undefined')
|
|
1033
1033
|
})
|
|
1034
|
+
|
|
1035
|
+
it('can create subforms on non-existent paths', async () => {
|
|
1036
|
+
const form = useForm<{
|
|
1037
|
+
test: string[]
|
|
1038
|
+
nonExistentPath?: { name: string }
|
|
1039
|
+
}>({
|
|
1040
|
+
initialData: { test: ['item1', 'item2'] },
|
|
1041
|
+
})
|
|
1042
|
+
|
|
1043
|
+
const subform = form.getSubForm('nonExistentPath')
|
|
1044
|
+
|
|
1045
|
+
const nameField = subform.defineField({ path: 'name' })
|
|
1046
|
+
|
|
1047
|
+
nameField.setData('Test Name')
|
|
1048
|
+
|
|
1049
|
+
expect(nameField.data.value).toEqual('Test Name')
|
|
1050
|
+
})
|
|
1034
1051
|
})
|
|
1035
1052
|
|
|
1036
1053
|
describe('Path Edge Cases', () => {
|
package/tests/useField.test.ts
CHANGED
|
@@ -207,4 +207,21 @@ describe('useField', () => {
|
|
|
207
207
|
expect(field.initialValue.value).toBe('bar')
|
|
208
208
|
expect(field.data.value).toBe('modified')
|
|
209
209
|
})
|
|
210
|
+
|
|
211
|
+
it('it should reset errors on a field on blur', () => {
|
|
212
|
+
const errors = ref(['Initial error'])
|
|
213
|
+
|
|
214
|
+
const field = useField({
|
|
215
|
+
initialValue: 'foo',
|
|
216
|
+
value: 'foo',
|
|
217
|
+
path: 'name',
|
|
218
|
+
errors,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
expect(field.errors.value).toEqual(['Initial error'])
|
|
222
|
+
|
|
223
|
+
field.onBlur()
|
|
224
|
+
|
|
225
|
+
expect(field.errors.value).toEqual([])
|
|
226
|
+
})
|
|
210
227
|
})
|