@teamnovu/kit-vue-forms 0.1.21 → 0.1.23
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 +12 -2
- package/dist/composables/useSubform.d.ts +1 -1
- package/dist/index.js +410 -351
- package/dist/types/form.d.ts +17 -1
- package/dist/{composables/useSubmitHandler.d.ts → utils/submitHandler.d.ts} +1 -1
- package/package.json +1 -1
- package/src/composables/useFieldArray.ts +151 -8
- package/src/composables/useForm.ts +14 -20
- package/src/composables/useSubform.ts +66 -70
- package/src/types/form.ts +28 -1
- package/src/{composables/useSubmitHandler.ts → utils/submitHandler.ts} +2 -2
- package/tests/useFieldArray.test.ts +243 -0
package/dist/types/form.d.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { Awaitable } from '@vueuse/core';
|
|
2
|
-
import { Ref } from 'vue';
|
|
2
|
+
import { Ref, ShallowRef } from 'vue';
|
|
3
3
|
import { DefineFieldOptions } from '../composables/useFieldRegistry';
|
|
4
4
|
import { SubformOptions } from '../composables/useSubform';
|
|
5
5
|
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 FieldItem<Item, Path extends string> {
|
|
14
|
+
id: string;
|
|
15
|
+
item: Item;
|
|
16
|
+
path: `${Path}.${number}`;
|
|
17
|
+
}
|
|
18
|
+
export interface FieldArray<Item, Path extends string> {
|
|
19
|
+
items: ShallowRef<FieldItem<Item, Path>[]>;
|
|
20
|
+
push: (item: Item) => FieldItem<Item, Path>;
|
|
21
|
+
remove: (id: string) => void;
|
|
22
|
+
field: FormField<Item[], Path>;
|
|
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
|
@@ -1,15 +1,158 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { shallowRef, watch } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
FieldArray,
|
|
4
|
+
FieldArrayOptions,
|
|
5
|
+
FieldItem,
|
|
6
|
+
Form,
|
|
7
|
+
FormDataDefault,
|
|
8
|
+
FormField,
|
|
9
|
+
HashFn,
|
|
10
|
+
} from '../types/form'
|
|
11
|
+
import type { Paths, PickProps } from '../types/util'
|
|
12
|
+
|
|
13
|
+
export class HashStore<T, Item = unknown> {
|
|
14
|
+
private weakMap = new WeakMap<WeakKey, T>()
|
|
15
|
+
private map = new Map<unknown, T>()
|
|
16
|
+
private hashFn: HashFn<unknown, Item>
|
|
17
|
+
|
|
18
|
+
constructor(hashFn: HashFn<unknown, Item> = item => item) {
|
|
19
|
+
this.hashFn = hashFn
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private isReferenceType(value: unknown): value is WeakKey {
|
|
23
|
+
return typeof value === 'object' && value !== null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
has(item: Item) {
|
|
27
|
+
const hash = this.hashFn(item)
|
|
28
|
+
if (this.isReferenceType(hash)) {
|
|
29
|
+
return this.weakMap.has(hash)
|
|
30
|
+
} else {
|
|
31
|
+
return this.map.has(hash)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get(item: Item) {
|
|
36
|
+
const hash = this.hashFn(item)
|
|
37
|
+
if (this.isReferenceType(hash)) {
|
|
38
|
+
return this.weakMap.get(hash)
|
|
39
|
+
} else {
|
|
40
|
+
return this.map.get(hash)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
set(item: Item, value: T) {
|
|
45
|
+
const hash = this.hashFn(item)
|
|
46
|
+
if (this.isReferenceType(hash)) {
|
|
47
|
+
this.weakMap.set(hash, value)
|
|
48
|
+
} else {
|
|
49
|
+
this.map.set(hash, value)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mapIds<Item, Path extends string>(
|
|
55
|
+
hashStore: HashStore<string[],
|
|
56
|
+
Item>,
|
|
57
|
+
items: Item[],
|
|
58
|
+
basePath: Path,
|
|
59
|
+
): FieldItem<Item, Path>[] {
|
|
60
|
+
const mappedIds = new Set<string>()
|
|
61
|
+
|
|
62
|
+
return items.map((item, i) => {
|
|
63
|
+
const storeIds = [...(hashStore.get(item) ?? [])]
|
|
64
|
+
|
|
65
|
+
// Remove all used ids
|
|
66
|
+
const firstNotUsedId = storeIds.findIndex(id => !mappedIds.has(id))
|
|
67
|
+
const ids = firstNotUsedId === -1 ? [] : storeIds.slice(firstNotUsedId)
|
|
68
|
+
|
|
69
|
+
const matchingId = ids[0]
|
|
70
|
+
|
|
71
|
+
// If we have an id that is not used yet, use it
|
|
72
|
+
if (matchingId) {
|
|
73
|
+
mappedIds.add(matchingId)
|
|
74
|
+
return {
|
|
75
|
+
id: matchingId,
|
|
76
|
+
item,
|
|
77
|
+
path: `${basePath}.${i}`,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Otherwise create a new id
|
|
82
|
+
const newId = crypto.randomUUID()
|
|
83
|
+
hashStore.set(item, storeIds.concat([newId]))
|
|
84
|
+
|
|
85
|
+
mappedIds.add(newId)
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
id: newId,
|
|
89
|
+
item,
|
|
90
|
+
path: `${basePath}.${i}`,
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
3
94
|
|
|
4
95
|
export function useFieldArray<T extends FormDataDefault, K extends Paths<T>>(
|
|
5
96
|
form: Form<T>,
|
|
6
97
|
path: PickProps<T, K> extends unknown[] ? K : never,
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
|
|
98
|
+
options?: FieldArrayOptions<PickProps<T, K> extends (infer U)[] ? U : never>,
|
|
99
|
+
): FieldArray<PickProps<T, K> extends (infer U)[] ? U : never, typeof path> {
|
|
100
|
+
type Items = PickProps<T, K>
|
|
101
|
+
type Item = Items extends (infer U)[] ? U : never
|
|
102
|
+
type Id = string
|
|
103
|
+
type Path = typeof path
|
|
104
|
+
type Field = {
|
|
105
|
+
id: Id
|
|
106
|
+
item: Item
|
|
107
|
+
path: `${Path}.${number}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const hashStore = new HashStore<string[], Item>(options?.hashFn)
|
|
111
|
+
|
|
112
|
+
// We only cast to unknown because we know that the constriant holds true
|
|
113
|
+
const arrayField = form.getField(path) as unknown as FormField<Item[], Path>
|
|
114
|
+
|
|
115
|
+
const items = shallowRef<Field[]>([])
|
|
116
|
+
|
|
117
|
+
watch(
|
|
118
|
+
arrayField.data,
|
|
119
|
+
(newItems) => {
|
|
120
|
+
items.value = mapIds(hashStore, newItems, path) as Field[]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
immediate: true,
|
|
124
|
+
flush: 'sync',
|
|
125
|
+
},
|
|
126
|
+
)
|
|
11
127
|
|
|
12
128
|
const push = (item: Item) => {
|
|
13
|
-
|
|
14
|
-
|
|
129
|
+
const current = (arrayField.data.value ?? []) as Item[]
|
|
130
|
+
arrayField.setData([...current, item] as Items)
|
|
131
|
+
|
|
132
|
+
return items.value.at(-1)!
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const remove = (id: Id) => {
|
|
136
|
+
const currentData = (arrayField.data.value ?? []) as Item[]
|
|
137
|
+
const currentItem = items.value.findIndex(
|
|
138
|
+
({ id: itemId }) => itemId === id,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if (currentItem === -1) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
arrayField.setData(
|
|
146
|
+
currentData
|
|
147
|
+
.slice(0, currentItem)
|
|
148
|
+
.concat(currentData.slice(currentItem + 1)) as Items,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
items,
|
|
154
|
+
push,
|
|
155
|
+
remove,
|
|
156
|
+
field: arrayField,
|
|
157
|
+
}
|
|
15
158
|
}
|
|
@@ -10,13 +10,13 @@ import {
|
|
|
10
10
|
type Ref,
|
|
11
11
|
} from 'vue'
|
|
12
12
|
import type { Form, FormDataDefault } from '../types/form'
|
|
13
|
-
import type { EntityPaths, PickEntity } from '../types/util'
|
|
14
13
|
import type { ValidationStrategy } from '../types/validation'
|
|
15
14
|
import { cloneRefValue } from '../utils/general'
|
|
15
|
+
import { makeSubmitHandler } from '../utils/submitHandler'
|
|
16
|
+
import { useFieldArray } from './useFieldArray'
|
|
16
17
|
import { useFieldRegistry } from './useFieldRegistry'
|
|
17
18
|
import { useFormState } from './useFormState'
|
|
18
|
-
import { createSubformInterface
|
|
19
|
-
import { useSubmitHandler } from './useSubmitHandler'
|
|
19
|
+
import { createSubformInterface } from './useSubform'
|
|
20
20
|
import { useValidation, type ValidationOptions } from './useValidation'
|
|
21
21
|
|
|
22
22
|
export interface UseFormOptions<T extends FormDataDefault>
|
|
@@ -65,31 +65,25 @@ export function useForm<T extends FormDataDefault>(
|
|
|
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
|
-
getSubForm,
|
|
81
77
|
initialData: toRef(state, 'initialData') as Form<T>['initialData'],
|
|
82
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
|
+
},
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (unref(options.validationStrategy) === 'onFormOpen') {
|
|
88
|
-
validationState.validateForm()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
...formInterface,
|
|
93
|
-
submitHandler,
|
|
94
|
-
}
|
|
88
|
+
return form
|
|
95
89
|
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import { computed, isRef, unref, type Ref } from
|
|
1
|
+
import { computed, isRef, unref, type Ref } from 'vue'
|
|
2
2
|
import type {
|
|
3
3
|
FieldsTuple,
|
|
4
4
|
Form,
|
|
5
5
|
FormDataDefault,
|
|
6
6
|
FormField,
|
|
7
|
-
} from
|
|
8
|
-
import type { EntityPaths, Paths, PickEntity, PickProps } from
|
|
9
|
-
import type { ValidationResult, Validator } from
|
|
7
|
+
} from '../types/form'
|
|
8
|
+
import type { EntityPaths, Paths, PickEntity, PickProps } from '../types/util'
|
|
9
|
+
import type { ValidationResult, Validator } from '../types/validation'
|
|
10
10
|
import {
|
|
11
11
|
filterErrorsForPath,
|
|
12
12
|
getLens,
|
|
13
13
|
getNestedValue,
|
|
14
14
|
joinPath,
|
|
15
|
-
} from
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
15
|
+
} from '../utils/path'
|
|
16
|
+
import { makeSubmitHandler } from '../utils/submitHandler'
|
|
17
|
+
import { useFieldArray } from './useFieldArray'
|
|
18
|
+
import type { DefineFieldOptions } from './useFieldRegistry'
|
|
19
|
+
import type { UseFormOptions } from './useForm'
|
|
19
20
|
import {
|
|
20
21
|
createValidator,
|
|
21
22
|
SuccessValidationResult,
|
|
22
23
|
type ValidatorOptions,
|
|
23
|
-
} from
|
|
24
|
+
} from './useValidation'
|
|
24
25
|
|
|
25
26
|
export interface SubformOptions<_T extends FormDataDefault> {
|
|
26
27
|
// Additional subform-specific options can be added here
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
class NestedValidator<T extends FormDataDefault, P extends Paths<T>>
|
|
30
|
-
|
|
31
|
-
{
|
|
31
|
+
implements Validator<T> {
|
|
32
32
|
constructor(
|
|
33
33
|
private path: P,
|
|
34
34
|
private validator: Validator<PickEntity<T, P>> | undefined,
|
|
35
35
|
) {}
|
|
36
36
|
|
|
37
37
|
async validate(data: T): Promise<ValidationResult> {
|
|
38
|
-
const subFormData = getNestedValue(data, this.path) as PickEntity<T, P
|
|
38
|
+
const subFormData = getNestedValue(data, this.path) as PickEntity<T, P>
|
|
39
39
|
|
|
40
40
|
if (!this.validator) {
|
|
41
|
-
return SuccessValidationResult
|
|
41
|
+
return SuccessValidationResult
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const validationResult = await this.validator.validate(subFormData)
|
|
44
|
+
const validationResult = await this.validator.validate(subFormData)
|
|
45
45
|
|
|
46
46
|
return {
|
|
47
47
|
isValid: validationResult.isValid,
|
|
@@ -55,7 +55,7 @@ class NestedValidator<T extends FormDataDefault, P extends Paths<T>>
|
|
|
55
55
|
)
|
|
56
56
|
: {},
|
|
57
57
|
},
|
|
58
|
-
}
|
|
58
|
+
}
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -63,22 +63,22 @@ export function createSubformInterface<
|
|
|
63
63
|
T extends FormDataDefault,
|
|
64
64
|
K extends EntityPaths<T>,
|
|
65
65
|
>(
|
|
66
|
-
mainForm:
|
|
66
|
+
mainForm: Form<T>,
|
|
67
67
|
path: K,
|
|
68
68
|
formOptions?: UseFormOptions<T>,
|
|
69
69
|
_options?: SubformOptions<PickEntity<T, K>>,
|
|
70
70
|
): Form<PickEntity<T, K>> {
|
|
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
|
|
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>
|
|
75
75
|
|
|
76
76
|
// Create reactive data scoped to subform path
|
|
77
|
-
const data = getLens(mainForm.data, path) as Ref<ST
|
|
77
|
+
const data = getLens(mainForm.data, path) as Ref<ST>
|
|
78
78
|
|
|
79
79
|
const initialData = computed(() => {
|
|
80
|
-
return getNestedValue(mainForm.initialData.value, path) as ST
|
|
81
|
-
})
|
|
80
|
+
return getNestedValue(mainForm.initialData.value, path) as ST
|
|
81
|
+
})
|
|
82
82
|
|
|
83
83
|
const adaptMainFormField = <S extends SP>(
|
|
84
84
|
field: FormField<PickProps<T, ScopedMainPaths>, ScopedMainPaths>,
|
|
@@ -86,37 +86,37 @@ export function createSubformInterface<
|
|
|
86
86
|
// Where P ist the full path in the main form, we need to adapt it to the subform's path
|
|
87
87
|
return {
|
|
88
88
|
...field,
|
|
89
|
-
path: computed(() => unref(field.path).replace(path +
|
|
89
|
+
path: computed(() => unref(field.path).replace(path + '.', '')),
|
|
90
90
|
setData: (newData: PickProps<ST, S>) => {
|
|
91
|
-
field.setData(newData as PickProps<T, ScopedMainPaths>)
|
|
91
|
+
field.setData(newData as PickProps<T, ScopedMainPaths>)
|
|
92
92
|
},
|
|
93
|
-
} as unknown as FormField<PickProps<ST, S>, S
|
|
94
|
-
}
|
|
93
|
+
} as unknown as FormField<PickProps<ST, S>, S>
|
|
94
|
+
}
|
|
95
95
|
|
|
96
96
|
const getField = <P extends SP>(fieldPath: P) => {
|
|
97
|
-
const fullPath = joinPath(path, fieldPath)
|
|
98
|
-
const mainFormField = mainForm.getField(fullPath as ScopedMainPaths)
|
|
97
|
+
const fullPath = joinPath(path, fieldPath)
|
|
98
|
+
const mainFormField = mainForm.getField(fullPath as ScopedMainPaths)
|
|
99
99
|
|
|
100
100
|
if (!mainFormField) {
|
|
101
|
-
return {} as FormField<PickProps<ST, P>, P
|
|
101
|
+
return {} as FormField<PickProps<ST, P>, P>
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
return adaptMainFormField<P>(mainFormField)
|
|
105
|
-
}
|
|
104
|
+
return adaptMainFormField<P>(mainFormField)
|
|
105
|
+
}
|
|
106
106
|
|
|
107
107
|
// Field operations with path transformation
|
|
108
108
|
const defineField = <P extends SP>(
|
|
109
109
|
fieldOptions: DefineFieldOptions<ST, P>,
|
|
110
110
|
) => {
|
|
111
|
-
const fullPath = joinPath(path, fieldOptions.path)
|
|
111
|
+
const fullPath = joinPath(path, fieldOptions.path)
|
|
112
112
|
|
|
113
113
|
const mainField = mainForm.defineField({
|
|
114
114
|
...fieldOptions,
|
|
115
115
|
path: fullPath as ScopedMainPaths,
|
|
116
|
-
})
|
|
116
|
+
})
|
|
117
117
|
|
|
118
|
-
return adaptMainFormField<P>(mainField)
|
|
119
|
-
}
|
|
118
|
+
return adaptMainFormField<P>(mainField)
|
|
119
|
+
}
|
|
120
120
|
|
|
121
121
|
const fields = computed(<P extends SP>() => {
|
|
122
122
|
return (
|
|
@@ -126,11 +126,11 @@ export function createSubformInterface<
|
|
|
126
126
|
>[]
|
|
127
127
|
)
|
|
128
128
|
.filter((field) => {
|
|
129
|
-
const fieldPath = field.path.value
|
|
130
|
-
return fieldPath.startsWith(path +
|
|
129
|
+
const fieldPath = field.path.value
|
|
130
|
+
return fieldPath.startsWith(path + '.') || fieldPath === path
|
|
131
131
|
})
|
|
132
|
-
.map(
|
|
133
|
-
})
|
|
132
|
+
.map(field => adaptMainFormField(field)) as FieldsTuple<ST, P>
|
|
133
|
+
})
|
|
134
134
|
|
|
135
135
|
// Helper function to get all fields without type parameter
|
|
136
136
|
const getAllSubformFields = () => {
|
|
@@ -140,57 +140,54 @@ export function createSubformInterface<
|
|
|
140
140
|
ScopedMainPaths
|
|
141
141
|
>[]
|
|
142
142
|
).filter((field) => {
|
|
143
|
-
const fieldPath = field.path.value
|
|
144
|
-
return fieldPath.startsWith(path +
|
|
145
|
-
})
|
|
146
|
-
}
|
|
143
|
+
const fieldPath = field.path.value
|
|
144
|
+
return fieldPath.startsWith(path + '.') || fieldPath === path
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
147
|
|
|
148
148
|
// State computed from main form with path filtering
|
|
149
149
|
const isDirty = computed(() =>
|
|
150
|
-
getAllSubformFields().some(
|
|
151
|
-
);
|
|
150
|
+
getAllSubformFields().some(field => field.dirty.value))
|
|
152
151
|
const isTouched = computed(() =>
|
|
153
|
-
getAllSubformFields().some(
|
|
154
|
-
);
|
|
152
|
+
getAllSubformFields().some(field => field.touched.value))
|
|
155
153
|
|
|
156
154
|
// Validation delegates to main form
|
|
157
|
-
const isValid = computed(() => mainForm.isValid.value)
|
|
158
|
-
const isValidated = computed(() => mainForm.isValidated.value)
|
|
155
|
+
const isValid = computed(() => mainForm.isValid.value)
|
|
156
|
+
const isValidated = computed(() => mainForm.isValidated.value)
|
|
159
157
|
const errors = computed(() =>
|
|
160
|
-
filterErrorsForPath(unref(mainForm.errors), path)
|
|
161
|
-
);
|
|
158
|
+
filterErrorsForPath(unref(mainForm.errors), path))
|
|
162
159
|
|
|
163
|
-
const validateForm = () => mainForm.validateForm()
|
|
160
|
+
const validateForm = () => mainForm.validateForm()
|
|
164
161
|
|
|
165
162
|
// Nested subforms
|
|
166
163
|
const getSubForm = <P extends EntityPaths<ST>>(
|
|
167
164
|
subPath: P,
|
|
168
165
|
subOptions?: SubformOptions<PickEntity<ST, P>>,
|
|
169
166
|
) => {
|
|
170
|
-
const fullPath = joinPath(path, subPath) as EntityPaths<T
|
|
167
|
+
const fullPath = joinPath(path, subPath) as EntityPaths<T>
|
|
171
168
|
return mainForm.getSubForm(
|
|
172
169
|
fullPath,
|
|
173
170
|
subOptions as SubformOptions<PickEntity<T, typeof fullPath>>,
|
|
174
|
-
) as Form<PickEntity<ST, P
|
|
175
|
-
}
|
|
171
|
+
) as Form<PickEntity<ST, P>>
|
|
172
|
+
}
|
|
176
173
|
|
|
177
174
|
// Reset scoped to this subform
|
|
178
|
-
const reset = () => getAllSubformFields().forEach(
|
|
175
|
+
const reset = () => getAllSubformFields().forEach(field => field.reset())
|
|
179
176
|
|
|
180
177
|
const defineValidator = (
|
|
181
178
|
options: ValidatorOptions<ST> | Ref<Validator<ST>>,
|
|
182
179
|
) => {
|
|
183
|
-
const subValidator = isRef(options) ? options : createValidator(options)
|
|
180
|
+
const subValidator = isRef(options) ? options : createValidator(options)
|
|
184
181
|
const validator = computed(
|
|
185
182
|
() => new NestedValidator<T, K>(path, unref(subValidator)),
|
|
186
|
-
)
|
|
183
|
+
)
|
|
187
184
|
|
|
188
|
-
mainForm.defineValidator(validator)
|
|
185
|
+
mainForm.defineValidator(validator)
|
|
189
186
|
|
|
190
|
-
return subValidator
|
|
191
|
-
}
|
|
187
|
+
return subValidator
|
|
188
|
+
}
|
|
192
189
|
|
|
193
|
-
const
|
|
190
|
+
const subForm: Form<ST> = {
|
|
194
191
|
data: data,
|
|
195
192
|
fields,
|
|
196
193
|
initialData,
|
|
@@ -205,12 +202,11 @@ export function createSubformInterface<
|
|
|
205
202
|
reset,
|
|
206
203
|
validateForm,
|
|
207
204
|
getSubForm,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
submitHandler: onSubmit => makeSubmitHandler(subForm, formOptions ?? {})(onSubmit),
|
|
206
|
+
useFieldArray: (fieldArrayPath, fieldArrayOptions) => {
|
|
207
|
+
return useFieldArray(subForm, fieldArrayPath, fieldArrayOptions)
|
|
208
|
+
},
|
|
209
|
+
}
|
|
211
210
|
|
|
212
|
-
return
|
|
213
|
-
...subFormInterface,
|
|
214
|
-
submitHandler,
|
|
215
|
-
};
|
|
211
|
+
return subForm
|
|
216
212
|
}
|
package/src/types/form.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Awaitable } from '@vueuse/core'
|
|
2
|
-
import type { Ref } from 'vue'
|
|
2
|
+
import type { Ref, ShallowRef } from 'vue'
|
|
3
3
|
import type { DefineFieldOptions } from '../composables/useFieldRegistry'
|
|
4
4
|
import type { SubformOptions } from '../composables/useSubform'
|
|
5
5
|
import type { ValidatorOptions } from '../composables/useValidation'
|
|
@@ -14,6 +14,25 @@ import type {
|
|
|
14
14
|
|
|
15
15
|
export type FormDataDefault = object
|
|
16
16
|
|
|
17
|
+
export type HashFn<H, I> = (item: I) => H
|
|
18
|
+
|
|
19
|
+
export interface FieldArrayOptions<Item> {
|
|
20
|
+
hashFn?: HashFn<unknown, Item>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FieldItem<Item, Path extends string> {
|
|
24
|
+
id: string
|
|
25
|
+
item: Item
|
|
26
|
+
path: `${Path}.${number}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FieldArray<Item, Path extends string> {
|
|
30
|
+
items: ShallowRef<FieldItem<Item, Path>[]>
|
|
31
|
+
push: (item: Item) => FieldItem<Item, Path>
|
|
32
|
+
remove: (id: string) => void
|
|
33
|
+
field: FormField<Item[], Path>
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
export interface FormField<T, P extends string> {
|
|
18
37
|
data: Ref<T>
|
|
19
38
|
path: Ref<P>
|
|
@@ -81,4 +100,12 @@ export interface Form<T extends FormDataDefault> {
|
|
|
81
100
|
path: P,
|
|
82
101
|
options?: SubformOptions<PickEntity<T, P>>,
|
|
83
102
|
) => Form<PickEntity<T, P>>
|
|
103
|
+
|
|
104
|
+
// Field arrays
|
|
105
|
+
useFieldArray: <K extends Paths<T>>(
|
|
106
|
+
path: PickProps<T, K> extends unknown[] ? K : never,
|
|
107
|
+
options?: FieldArrayOptions<
|
|
108
|
+
PickProps<T, K> extends (infer U)[] ? U : never
|
|
109
|
+
>,
|
|
110
|
+
) => FieldArray<PickProps<T, K> extends (infer U)[] ? U : never>
|
|
84
111
|
}
|
|
@@ -7,8 +7,8 @@ interface SubmitHandlerOptions {
|
|
|
7
7
|
validationStrategy?: MaybeRef<ValidationStrategy>
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export function
|
|
11
|
-
form:
|
|
10
|
+
export function makeSubmitHandler<T extends FormDataDefault>(
|
|
11
|
+
form: Form<T>,
|
|
12
12
|
options: SubmitHandlerOptions,
|
|
13
13
|
) {
|
|
14
14
|
return (onSubmit: (data: T) => Awaitable<void>) => {
|