@teamnovu/kit-vue-forms 0.1.20 → 0.1.22
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/composables/useFieldArray.d.ts +13 -0
- package/dist/composables/useFieldRegistry.d.ts +1 -1
- package/dist/composables/useSubform.d.ts +1 -1
- package/dist/index.js +406 -346
- package/dist/types/form.d.ts +16 -0
- package/dist/{composables/useSubmitHandler.d.ts → utils/submitHandler.d.ts} +1 -1
- package/package.json +1 -1
- package/src/composables/useField.ts +3 -1
- package/src/composables/useFieldArray.ts +144 -0
- package/src/composables/useFieldRegistry.ts +60 -60
- package/src/composables/useForm.ts +41 -47
- package/src/composables/useSubform.ts +66 -70
- package/src/types/form.ts +26 -0
- package/src/{composables/useSubmitHandler.ts → utils/submitHandler.ts} +2 -2
- package/tests/useFieldArray.test.ts +381 -0
- package/tests/useForm.test.ts +286 -257
package/dist/types/form.d.ts
CHANGED
|
@@ -6,6 +6,21 @@ import { ValidatorOptions } from '../composables/useValidation';
|
|
|
6
6
|
import { EntityPaths, Paths, PickEntity, PickProps } from './util';
|
|
7
7
|
import { ErrorBag, ValidationErrorMessage, ValidationErrors, ValidationResult, Validator } from './validation';
|
|
8
8
|
export type FormDataDefault = object;
|
|
9
|
+
export type HashFn<H, I> = (item: I) => H;
|
|
10
|
+
export interface FieldArrayOptions<Item> {
|
|
11
|
+
hashFn?: HashFn<unknown, Item>;
|
|
12
|
+
}
|
|
13
|
+
export interface FieldArray<Item> {
|
|
14
|
+
fields: Ref<{
|
|
15
|
+
id: string;
|
|
16
|
+
item: Item;
|
|
17
|
+
}[]>;
|
|
18
|
+
push: (item: Item) => void;
|
|
19
|
+
remove: (item: Item) => void;
|
|
20
|
+
removeByIndex: (index: number) => void;
|
|
21
|
+
errors: Ref<ValidationErrors>;
|
|
22
|
+
dirty: Ref<boolean>;
|
|
23
|
+
}
|
|
9
24
|
export interface FormField<T, P extends string> {
|
|
10
25
|
data: Ref<T>;
|
|
11
26
|
path: Ref<P>;
|
|
@@ -45,4 +60,5 @@ export interface Form<T extends FormDataDefault> {
|
|
|
45
60
|
validateForm: () => Promise<ValidationResult>;
|
|
46
61
|
submitHandler: (onSubmit: (data: T) => Awaitable<void>) => (event: SubmitEvent) => Promise<void>;
|
|
47
62
|
getSubForm: <P extends EntityPaths<T>>(path: P, options?: SubformOptions<PickEntity<T, P>>) => Form<PickEntity<T, P>>;
|
|
63
|
+
useFieldArray: <K extends Paths<T>>(path: PickProps<T, K> extends unknown[] ? K : never, options?: FieldArrayOptions<PickProps<T, K> extends (infer U)[] ? U : never>) => FieldArray<PickProps<T, K> extends (infer U)[] ? U : never>;
|
|
48
64
|
}
|
|
@@ -4,5 +4,5 @@ import { ValidationStrategy } from '../types/validation';
|
|
|
4
4
|
interface SubmitHandlerOptions {
|
|
5
5
|
validationStrategy?: MaybeRef<ValidationStrategy>;
|
|
6
6
|
}
|
|
7
|
-
export declare function
|
|
7
|
+
export declare function makeSubmitHandler<T extends FormDataDefault>(form: Form<T>, options: SubmitHandlerOptions): (onSubmit: (data: T) => Awaitable<void>) => (event: SubmitEvent) => Promise<void>;
|
|
8
8
|
export {};
|
package/package.json
CHANGED
|
@@ -37,7 +37,9 @@ export function useField<T, K extends string>(fieldOptions: UseFieldOptions<T, K
|
|
|
37
37
|
shallowRef(options.initialValue),
|
|
38
38
|
() => {
|
|
39
39
|
initialValue.value = Object.freeze(cloneRefValue(options.initialValue))
|
|
40
|
-
state.value
|
|
40
|
+
if (state.value !== unref(options.initialValue)) {
|
|
41
|
+
state.value = cloneRefValue(options.initialValue)
|
|
42
|
+
}
|
|
41
43
|
},
|
|
42
44
|
{ flush: 'sync' },
|
|
43
45
|
)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { shallowRef, unref, watch } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
FieldArray,
|
|
4
|
+
FieldArrayOptions,
|
|
5
|
+
Form,
|
|
6
|
+
FormDataDefault,
|
|
7
|
+
HashFn,
|
|
8
|
+
} from '../types/form'
|
|
9
|
+
import type { Paths, PickProps } from '../types/util'
|
|
10
|
+
|
|
11
|
+
export class HashStore<T, Item = unknown> {
|
|
12
|
+
private weakMap = new WeakMap<WeakKey, T>()
|
|
13
|
+
private map = new Map<unknown, T>()
|
|
14
|
+
private hashFn: HashFn<unknown, Item>
|
|
15
|
+
|
|
16
|
+
constructor(hashFn: HashFn<unknown, Item> = item => item) {
|
|
17
|
+
this.hashFn = hashFn
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private isReferenceType(value: unknown): value is WeakKey {
|
|
21
|
+
return typeof value === 'object' && value !== null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
has(item: Item) {
|
|
25
|
+
const hash = this.hashFn(item)
|
|
26
|
+
if (this.isReferenceType(hash)) {
|
|
27
|
+
return this.weakMap.has(hash)
|
|
28
|
+
} else {
|
|
29
|
+
return this.map.has(hash)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get(item: Item) {
|
|
34
|
+
const hash = this.hashFn(item)
|
|
35
|
+
if (this.isReferenceType(hash)) {
|
|
36
|
+
return this.weakMap.get(hash)
|
|
37
|
+
} else {
|
|
38
|
+
return this.map.get(hash)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
set(item: Item, value: T) {
|
|
43
|
+
const hash = this.hashFn(item)
|
|
44
|
+
if (this.isReferenceType(hash)) {
|
|
45
|
+
this.weakMap.set(hash, value)
|
|
46
|
+
} else {
|
|
47
|
+
this.map.set(hash, value)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function mapIds<Item>(
|
|
53
|
+
hashStore: HashStore<string[], Item>,
|
|
54
|
+
items: Item[],
|
|
55
|
+
) {
|
|
56
|
+
const mappedIds = new Set<string>()
|
|
57
|
+
|
|
58
|
+
return items.map((item) => {
|
|
59
|
+
const storeIds = [...(hashStore.get(item) ?? [])]
|
|
60
|
+
|
|
61
|
+
// Remove all used ids
|
|
62
|
+
const firstNotUsedId = storeIds.findIndex(id => !mappedIds.has(id))
|
|
63
|
+
const ids = firstNotUsedId === -1 ? [] : storeIds.slice(firstNotUsedId)
|
|
64
|
+
|
|
65
|
+
const matchingId = ids[0]
|
|
66
|
+
|
|
67
|
+
// If we have an id that is not used yet, use it
|
|
68
|
+
if (matchingId) {
|
|
69
|
+
mappedIds.add(matchingId)
|
|
70
|
+
return {
|
|
71
|
+
id: matchingId,
|
|
72
|
+
item,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Otherwise create a new id
|
|
77
|
+
const newId = crypto.randomUUID()
|
|
78
|
+
hashStore.set(item, storeIds.concat([newId]))
|
|
79
|
+
|
|
80
|
+
mappedIds.add(newId)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
id: newId,
|
|
84
|
+
item,
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function useFieldArray<T extends FormDataDefault, K extends Paths<T>>(
|
|
90
|
+
form: Form<T>,
|
|
91
|
+
path: PickProps<T, K> extends unknown[] ? K : never,
|
|
92
|
+
options?: FieldArrayOptions<PickProps<T, K> extends (infer U)[] ? U : never>,
|
|
93
|
+
): FieldArray<PickProps<T, K> extends (infer U)[] ? U : never> {
|
|
94
|
+
type Items = PickProps<T, K>
|
|
95
|
+
type Item = Items extends (infer U)[] ? U : never
|
|
96
|
+
type Id = string
|
|
97
|
+
type Field = {
|
|
98
|
+
id: Id
|
|
99
|
+
item: Item
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hashStore = new HashStore<string[], Item>(options?.hashFn)
|
|
103
|
+
|
|
104
|
+
const arrayField = form.getField(path)
|
|
105
|
+
|
|
106
|
+
const fields = shallowRef<Field[]>([])
|
|
107
|
+
|
|
108
|
+
watch(
|
|
109
|
+
arrayField.data,
|
|
110
|
+
(newItems) => {
|
|
111
|
+
fields.value = mapIds(hashStore, newItems) as Field[]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
immediate: true,
|
|
115
|
+
flush: 'sync',
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const push = (item: Item) => {
|
|
120
|
+
const current = (arrayField.data.value ?? []) as Item[]
|
|
121
|
+
arrayField.setData([...current, item] as Items)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const remove = (value: Item) => {
|
|
125
|
+
const current = (arrayField.data.value ?? []) as Item[]
|
|
126
|
+
arrayField.setData(
|
|
127
|
+
(current?.filter(item => item !== unref(value)) ?? []) as Items,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const removeByIndex = (index: number) => {
|
|
132
|
+
const current = (arrayField.data.value ?? []) as Item[]
|
|
133
|
+
arrayField.setData((current?.filter((_, i) => i !== index) ?? []) as Items)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
fields,
|
|
138
|
+
push,
|
|
139
|
+
remove,
|
|
140
|
+
removeByIndex,
|
|
141
|
+
errors: arrayField.errors,
|
|
142
|
+
dirty: arrayField.dirty,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Awaitable } from
|
|
1
|
+
import type { Awaitable } from '@vueuse/core'
|
|
2
2
|
import {
|
|
3
3
|
computed,
|
|
4
4
|
onScopeDispose,
|
|
@@ -9,47 +9,47 @@ import {
|
|
|
9
9
|
unref,
|
|
10
10
|
watch,
|
|
11
11
|
type MaybeRef,
|
|
12
|
-
} from
|
|
13
|
-
import type { FieldsTuple, FormDataDefault, FormField } from
|
|
14
|
-
import type { Paths, PickProps } from
|
|
15
|
-
import { existsPath, getLens, getNestedValue } from
|
|
16
|
-
import { Rc } from
|
|
17
|
-
import { useField, type UseFieldOptions } from
|
|
18
|
-
import type { ValidationState } from
|
|
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
|
-
|
|
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)
|
|
@@ -57,19 +57,19 @@ function initialDataSync<T extends FormDataDefault>(
|
|
|
57
57
|
formState: FormState<T>,
|
|
58
58
|
path: Paths<T>,
|
|
59
59
|
) {
|
|
60
|
-
const getNewValue = () => getNestedValue(formState.initialData, path)
|
|
61
|
-
const initialValueRef = shallowRef(getNewValue())
|
|
60
|
+
const getNewValue = () => getNestedValue(formState.initialData, path)
|
|
61
|
+
const initialValueRef = shallowRef(getNewValue())
|
|
62
62
|
|
|
63
63
|
watch(
|
|
64
64
|
() => formState.initialData,
|
|
65
65
|
() => {
|
|
66
|
-
initialValueRef.value = getNewValue()
|
|
67
|
-
triggerRef(initialValueRef)
|
|
66
|
+
initialValueRef.value = getNewValue()
|
|
67
|
+
triggerRef(initialValueRef)
|
|
68
68
|
},
|
|
69
|
-
{ flush:
|
|
70
|
-
)
|
|
69
|
+
{ flush: 'sync' },
|
|
70
|
+
)
|
|
71
71
|
|
|
72
|
-
return initialValueRef
|
|
72
|
+
return initialValueRef
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export function useFieldRegistry<T extends FormDataDefault>(
|
|
@@ -77,99 +77,99 @@ export function useFieldRegistry<T extends FormDataDefault>(
|
|
|
77
77
|
validationState: ValidationState<T>,
|
|
78
78
|
fieldRegistryOptions?: FieldRegistryOptions,
|
|
79
79
|
) {
|
|
80
|
-
const fieldReferenceCounter = new Map<Paths<T>, Rc>()
|
|
81
|
-
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>
|
|
82
82
|
const registryOptions = {
|
|
83
83
|
...optionDefaults,
|
|
84
84
|
...fieldRegistryOptions,
|
|
85
|
-
}
|
|
85
|
+
}
|
|
86
86
|
|
|
87
87
|
const registerField = <K extends Paths<T>>(
|
|
88
88
|
field: ResolvedFormField<T, K>,
|
|
89
89
|
) => {
|
|
90
|
-
const path = unref(field.path) as Paths<T
|
|
91
|
-
fields.set(path, field)
|
|
92
|
-
}
|
|
90
|
+
const path = unref(field.path) as Paths<T>
|
|
91
|
+
fields.set(path, field)
|
|
92
|
+
}
|
|
93
93
|
|
|
94
94
|
const deregisterField = (path: Paths<T>) => {
|
|
95
95
|
if (!registryOptions?.keepValuesOnUnmount) {
|
|
96
|
-
fields.get(path)?.reset()
|
|
96
|
+
fields.get(path)?.reset()
|
|
97
97
|
}
|
|
98
|
-
fields.delete(path)
|
|
99
|
-
}
|
|
98
|
+
fields.delete(path)
|
|
99
|
+
}
|
|
100
100
|
|
|
101
101
|
const track = (path: Paths<T>) => {
|
|
102
102
|
if (!fieldReferenceCounter.has(path)) {
|
|
103
|
-
fieldReferenceCounter.set(path, new Rc(() => deregisterField(path)))
|
|
103
|
+
fieldReferenceCounter.set(path, new Rc(() => deregisterField(path)))
|
|
104
104
|
} else {
|
|
105
|
-
fieldReferenceCounter.get(path)?.inc()
|
|
105
|
+
fieldReferenceCounter.get(path)?.inc()
|
|
106
106
|
}
|
|
107
|
-
}
|
|
107
|
+
}
|
|
108
108
|
|
|
109
109
|
const untrack = (path: Paths<T>) => {
|
|
110
110
|
if (fieldReferenceCounter.has(path)) {
|
|
111
|
-
fieldReferenceCounter.get(path)?.dec()
|
|
111
|
+
fieldReferenceCounter.get(path)?.dec()
|
|
112
112
|
}
|
|
113
|
-
}
|
|
113
|
+
}
|
|
114
114
|
|
|
115
115
|
const getField = <K extends Paths<T>>(
|
|
116
116
|
options: DefineFieldOptions<PickProps<T, K>, K>,
|
|
117
117
|
): ResolvedFormField<T, K> => {
|
|
118
|
-
const { path } = options
|
|
118
|
+
const { path } = options
|
|
119
119
|
|
|
120
120
|
if (!fields.has(path)) {
|
|
121
121
|
const field = useField({
|
|
122
122
|
path,
|
|
123
|
-
value: getLens(toRef(formState,
|
|
123
|
+
value: getLens(toRef(formState, 'data'), path),
|
|
124
124
|
initialValue: initialDataSync(formState, path),
|
|
125
125
|
existsInForm: computed(() => existsPath(formState.data, unref(path))),
|
|
126
126
|
errors: computed({
|
|
127
127
|
get() {
|
|
128
|
-
return validationState.errors.value.propertyErrors[path] || []
|
|
128
|
+
return validationState.errors.value.propertyErrors[path] || []
|
|
129
129
|
},
|
|
130
130
|
set(newErrors) {
|
|
131
|
-
validationState.errors.value.propertyErrors[path] = newErrors
|
|
131
|
+
validationState.errors.value.propertyErrors[path] = newErrors
|
|
132
132
|
},
|
|
133
133
|
}),
|
|
134
134
|
onBlur: async () => {
|
|
135
135
|
await Promise.all([
|
|
136
136
|
registryOptions?.onBlur?.(unref(path)),
|
|
137
137
|
options.onBlur?.(),
|
|
138
|
-
])
|
|
138
|
+
])
|
|
139
139
|
},
|
|
140
140
|
onFocus: async () => {
|
|
141
141
|
await Promise.all([
|
|
142
142
|
registryOptions?.onFocus?.(unref(path)),
|
|
143
143
|
options.onFocus?.(),
|
|
144
|
-
])
|
|
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
|
+
>
|
|
@@ -8,88 +8,82 @@ import {
|
|
|
8
8
|
type MaybeRef,
|
|
9
9
|
type MaybeRefOrGetter,
|
|
10
10
|
type Ref,
|
|
11
|
-
} from
|
|
12
|
-
import type { Form, FormDataDefault } from
|
|
13
|
-
import type {
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { useValidation, type ValidationOptions } from
|
|
11
|
+
} from 'vue'
|
|
12
|
+
import type { Form, FormDataDefault } from '../types/form'
|
|
13
|
+
import type { ValidationStrategy } from '../types/validation'
|
|
14
|
+
import { cloneRefValue } from '../utils/general'
|
|
15
|
+
import { makeSubmitHandler } from '../utils/submitHandler'
|
|
16
|
+
import { useFieldArray } from './useFieldArray'
|
|
17
|
+
import { useFieldRegistry } from './useFieldRegistry'
|
|
18
|
+
import { useFormState } from './useFormState'
|
|
19
|
+
import { createSubformInterface } from './useSubform'
|
|
20
|
+
import { useValidation, type ValidationOptions } from './useValidation'
|
|
21
21
|
|
|
22
22
|
export interface UseFormOptions<T extends FormDataDefault>
|
|
23
23
|
extends ValidationOptions<T> {
|
|
24
|
-
initialData: MaybeRefOrGetter<T
|
|
25
|
-
validationStrategy?: MaybeRef<ValidationStrategy
|
|
26
|
-
keepValuesOnUnmount?: MaybeRef<boolean
|
|
24
|
+
initialData: MaybeRefOrGetter<T>
|
|
25
|
+
validationStrategy?: MaybeRef<ValidationStrategy>
|
|
26
|
+
keepValuesOnUnmount?: MaybeRef<boolean>
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function useForm<T extends FormDataDefault>(
|
|
30
30
|
options: UseFormOptions<T>,
|
|
31
31
|
): Form<T> {
|
|
32
|
-
const initialData = computed(() => cloneRefValue(options.initialData))
|
|
32
|
+
const initialData = computed(() => cloneRefValue(options.initialData))
|
|
33
33
|
|
|
34
|
-
const data = ref<T>(cloneRefValue(initialData)) as Ref<T
|
|
34
|
+
const data = ref<T>(cloneRefValue(initialData)) as Ref<T>
|
|
35
35
|
|
|
36
36
|
const state = reactive({
|
|
37
37
|
initialData,
|
|
38
38
|
data,
|
|
39
|
-
})
|
|
39
|
+
})
|
|
40
40
|
|
|
41
41
|
watch(
|
|
42
42
|
initialData,
|
|
43
43
|
(newValue) => {
|
|
44
|
-
state.data = cloneRefValue(newValue)
|
|
44
|
+
state.data = cloneRefValue(newValue)
|
|
45
45
|
},
|
|
46
|
-
{ flush:
|
|
47
|
-
)
|
|
46
|
+
{ flush: 'sync' },
|
|
47
|
+
)
|
|
48
48
|
|
|
49
|
-
const validationState = useValidation(state, options)
|
|
49
|
+
const validationState = useValidation(state, options)
|
|
50
50
|
const fieldRegistry = useFieldRegistry(state, validationState, {
|
|
51
51
|
keepValuesOnUnmount: options.keepValuesOnUnmount,
|
|
52
52
|
onBlur: async (path: string) => {
|
|
53
|
-
if (unref(options.validationStrategy) ===
|
|
54
|
-
validationState.validateField(path)
|
|
53
|
+
if (unref(options.validationStrategy) === 'onTouch') {
|
|
54
|
+
validationState.validateField(path)
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
-
})
|
|
58
|
-
const formState = useFormState(fieldRegistry)
|
|
57
|
+
})
|
|
58
|
+
const formState = useFormState(fieldRegistry)
|
|
59
59
|
|
|
60
60
|
const reset = () => {
|
|
61
|
-
data.value = cloneRefValue(initialData)
|
|
62
|
-
validationState.reset()
|
|
61
|
+
data.value = cloneRefValue(initialData)
|
|
62
|
+
validationState.reset()
|
|
63
63
|
for (const field of fieldRegistry.fields.value) {
|
|
64
|
-
field.reset()
|
|
64
|
+
field.reset()
|
|
65
65
|
}
|
|
66
|
-
}
|
|
66
|
+
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
subformOptions?: SubformOptions<PickEntity<T, K>>,
|
|
71
|
-
): Form<PickEntity<T, K>> {
|
|
72
|
-
return createSubformInterface(formInterface, path, options, subformOptions);
|
|
68
|
+
if (unref(options.validationStrategy) === 'onFormOpen') {
|
|
69
|
+
validationState.validateForm()
|
|
73
70
|
}
|
|
74
71
|
|
|
75
|
-
const
|
|
72
|
+
const form: Form<T> = {
|
|
76
73
|
...fieldRegistry,
|
|
77
74
|
...validationState,
|
|
78
75
|
...formState,
|
|
79
76
|
reset,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
initialData: toRef(state, 'initialData') as Form<T>['initialData'],
|
|
78
|
+
data: toRef(state, 'data') as Form<T>['data'],
|
|
79
|
+
submitHandler: onSubmit => makeSubmitHandler(form, options)(onSubmit),
|
|
80
|
+
getSubForm: (path, subformOptions) => {
|
|
81
|
+
return createSubformInterface(form, path, options, subformOptions)
|
|
82
|
+
},
|
|
83
|
+
useFieldArray: (path, fieldArrayOptions) => {
|
|
84
|
+
return useFieldArray(form, path, fieldArrayOptions)
|
|
85
|
+
},
|
|
89
86
|
}
|
|
90
87
|
|
|
91
|
-
return
|
|
92
|
-
...formInterface,
|
|
93
|
-
submitHandler,
|
|
94
|
-
};
|
|
88
|
+
return form
|
|
95
89
|
}
|