@volverjs/form-vue 1.0.0-beta.9 → 1.0.1
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/LICENSE +1 -1
- package/README.md +412 -245
- package/dist/VvForm.d.ts +135 -0
- package/dist/VvFormField.d.ts +130 -0
- package/dist/VvFormFieldsGroup.d.ts +109 -0
- package/dist/VvFormTemplate.d.ts +40 -0
- package/dist/VvFormWrapper.d.ts +65 -0
- package/dist/{src/enums.d.ts → enums.d.ts} +1 -0
- package/dist/index.d.ts +972 -1
- package/dist/index.es.js +948 -596
- package/dist/index.umd.js +1 -1
- package/dist/types.d.ts +93 -0
- package/dist/utils.d.ts +3 -0
- package/package.json +61 -60
- package/src/VvForm.ts +359 -300
- package/src/VvFormField.ts +366 -334
- package/src/VvFormFieldsGroup.ts +382 -0
- package/src/VvFormTemplate.ts +185 -171
- package/src/VvFormWrapper.ts +198 -161
- package/src/enums.ts +27 -26
- package/src/index.ts +157 -134
- package/src/types.ts +162 -125
- package/src/utils.ts +121 -100
- package/dist/src/VvForm.d.ts +0 -202
- package/dist/src/VvFormField.d.ts +0 -116
- package/dist/src/VvFormTemplate.d.ts +0 -60
- package/dist/src/VvFormWrapper.d.ts +0 -107
- package/dist/src/index.d.ts +0 -1498
- package/dist/src/types.d.ts +0 -70
- package/dist/src/utils.d.ts +0 -3
- package/dist/test-playwright/VvForm.spec.d.ts +0 -1
- package/dist/test-playwright/VvFormField.spec.d.ts +0 -1
- package/dist/test-playwright/VvFormWrapper.spec.d.ts +0 -1
- package/dist/test-vitest/defaultObjectBySchema.test.d.ts +0 -1
- package/dist/test-vitest/useForm.test.d.ts +0 -1
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import type { Component, DeepReadonly, InjectionKey, PropType, Ref, SlotsType } from 'vue'
|
|
2
|
+
import type { z } from 'zod'
|
|
3
|
+
import type {
|
|
4
|
+
FormSchema,
|
|
5
|
+
InjectedFormData,
|
|
6
|
+
InjectedFormFieldsGroupData,
|
|
7
|
+
InjectedFormWrapperData,
|
|
8
|
+
Path,
|
|
9
|
+
} from './types'
|
|
10
|
+
import { get, set } from 'ts-dot-prop'
|
|
11
|
+
import {
|
|
12
|
+
computed,
|
|
13
|
+
defineComponent,
|
|
14
|
+
h,
|
|
15
|
+
inject,
|
|
16
|
+
onBeforeUnmount,
|
|
17
|
+
onMounted,
|
|
18
|
+
provide,
|
|
19
|
+
readonly,
|
|
20
|
+
toRefs,
|
|
21
|
+
unref,
|
|
22
|
+
useId,
|
|
23
|
+
watch,
|
|
24
|
+
} from 'vue'
|
|
25
|
+
|
|
26
|
+
export function defineFormFieldsGroup<Schema extends FormSchema, Type = undefined>(formProvideKey: InjectionKey<InjectedFormData<Schema, Type>>, wrapperProvideKey: InjectionKey<InjectedFormWrapperData<Schema>>, formFieldsGroupInjectionKey: InjectionKey<InjectedFormFieldsGroupData<Schema>>) {
|
|
27
|
+
return defineComponent({
|
|
28
|
+
name: 'VvFormFieldsGroup',
|
|
29
|
+
props: {
|
|
30
|
+
is: {
|
|
31
|
+
type: [Object, String] as PropType<Component | string>,
|
|
32
|
+
default: undefined,
|
|
33
|
+
},
|
|
34
|
+
names: {
|
|
35
|
+
type: [Array, Object] as PropType<
|
|
36
|
+
Path<z.infer<Schema>>[] | Record<string, Path<z.infer<Schema>>>
|
|
37
|
+
>,
|
|
38
|
+
required: true,
|
|
39
|
+
},
|
|
40
|
+
props: {
|
|
41
|
+
type: [Object, Function] as PropType<
|
|
42
|
+
Partial<
|
|
43
|
+
| z.infer<Schema>
|
|
44
|
+
| undefined
|
|
45
|
+
| ((
|
|
46
|
+
formData?: Ref<ObjectConstructor>,
|
|
47
|
+
) => Partial<z.infer<Schema>> | undefined)
|
|
48
|
+
>
|
|
49
|
+
>,
|
|
50
|
+
default: () => ({}),
|
|
51
|
+
},
|
|
52
|
+
showValid: {
|
|
53
|
+
type: Boolean,
|
|
54
|
+
default: false,
|
|
55
|
+
},
|
|
56
|
+
defaultValues: {
|
|
57
|
+
type: [Object] as PropType<
|
|
58
|
+
Record<Path<z.infer<Schema>>, any>
|
|
59
|
+
>,
|
|
60
|
+
default: undefined,
|
|
61
|
+
},
|
|
62
|
+
readonly: {
|
|
63
|
+
type: Boolean,
|
|
64
|
+
default: undefined,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
emits: [
|
|
68
|
+
'invalid',
|
|
69
|
+
'update:formData',
|
|
70
|
+
'update:modelValue',
|
|
71
|
+
'valid',
|
|
72
|
+
],
|
|
73
|
+
expose: [
|
|
74
|
+
'component',
|
|
75
|
+
'errors',
|
|
76
|
+
'hasProps',
|
|
77
|
+
'invalid',
|
|
78
|
+
'invalidLabels',
|
|
79
|
+
'is',
|
|
80
|
+
],
|
|
81
|
+
slots: Object as SlotsType<{
|
|
82
|
+
[key: string]: any
|
|
83
|
+
default: {
|
|
84
|
+
errors?: Record<Path<z.infer<Schema>>, z.inferFormattedError<Schema>>
|
|
85
|
+
formData?: undefined extends Type ? Partial<z.infer<Schema>> : Type
|
|
86
|
+
formErrors?: DeepReadonly<z.inferFormattedError<Schema>>
|
|
87
|
+
invalid: boolean
|
|
88
|
+
invalids: Record<string, boolean>
|
|
89
|
+
invalidLabels?: Record<string, string[]>
|
|
90
|
+
modelValue: Record<string, any>
|
|
91
|
+
onUpdate: (value: Record<string, any>) => void
|
|
92
|
+
onUpdateField: (name: string, value: any) => void
|
|
93
|
+
readonly: boolean
|
|
94
|
+
submit?: InjectedFormData<Schema, Type>['submit']
|
|
95
|
+
validate?: InjectedFormData<Schema, Type>['validate']
|
|
96
|
+
}
|
|
97
|
+
}>,
|
|
98
|
+
setup(props, { slots, emit }) {
|
|
99
|
+
const { props: fieldProps, names: fieldsNames, defaultValues } = toRefs(props)
|
|
100
|
+
const fieldGroupId = useId()
|
|
101
|
+
const names = computed<Path<z.infer<Schema>>[]>(() => {
|
|
102
|
+
if (Array.isArray(fieldsNames.value)) {
|
|
103
|
+
return fieldsNames.value
|
|
104
|
+
}
|
|
105
|
+
return Object.values(fieldsNames.value)
|
|
106
|
+
})
|
|
107
|
+
const namesKeys = computed(() => {
|
|
108
|
+
if (Array.isArray(fieldsNames.value)) {
|
|
109
|
+
return fieldsNames.value
|
|
110
|
+
}
|
|
111
|
+
return Object.keys(fieldsNames.value)
|
|
112
|
+
})
|
|
113
|
+
const namesMap = computed(() => {
|
|
114
|
+
if (Array.isArray(fieldsNames.value)) {
|
|
115
|
+
return fieldsNames.value.reduce<Record<string, Path<z.infer<Schema>>>>((
|
|
116
|
+
acc,
|
|
117
|
+
name,
|
|
118
|
+
) => {
|
|
119
|
+
acc[String(name)] = name
|
|
120
|
+
return acc
|
|
121
|
+
}, {})
|
|
122
|
+
}
|
|
123
|
+
return fieldsNames.value
|
|
124
|
+
})
|
|
125
|
+
const namesKeysMap = computed(() => {
|
|
126
|
+
return Object.keys(namesMap.value).reduce<Record<string, string>>((acc, key) => {
|
|
127
|
+
acc[String(namesMap.value[key])] = key
|
|
128
|
+
return acc
|
|
129
|
+
}, {})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// inject data from parent form wrapper
|
|
133
|
+
const injectedWrapperData = inject(wrapperProvideKey, undefined)
|
|
134
|
+
if (injectedWrapperData) {
|
|
135
|
+
names.value.forEach((name) => {
|
|
136
|
+
injectedWrapperData.fields.value.set(`${fieldGroupId}-${name}`, name as string)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// inject data from parent form
|
|
141
|
+
const injectedFormData = inject(formProvideKey)
|
|
142
|
+
|
|
143
|
+
// v-model
|
|
144
|
+
const modelValue = computed({
|
|
145
|
+
get() {
|
|
146
|
+
if (!injectedFormData?.formData) {
|
|
147
|
+
return {}
|
|
148
|
+
}
|
|
149
|
+
return namesKeys.value.reduce<Record<string, any>>((acc, nameKey) => {
|
|
150
|
+
acc[nameKey] = get(
|
|
151
|
+
new Object(injectedFormData.formData.value),
|
|
152
|
+
namesMap.value[nameKey],
|
|
153
|
+
)
|
|
154
|
+
return acc
|
|
155
|
+
}, {})
|
|
156
|
+
},
|
|
157
|
+
set(value) {
|
|
158
|
+
if (!injectedFormData?.formData) {
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
namesKeys.value.forEach((nameKey) => {
|
|
162
|
+
set(
|
|
163
|
+
new Object(injectedFormData.formData.value),
|
|
164
|
+
namesMap.value[nameKey],
|
|
165
|
+
value?.[nameKey],
|
|
166
|
+
)
|
|
167
|
+
})
|
|
168
|
+
emit('update:modelValue', {
|
|
169
|
+
newValue: modelValue.value,
|
|
170
|
+
formData: injectedFormData?.formData,
|
|
171
|
+
})
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
onMounted(() => {
|
|
175
|
+
if (
|
|
176
|
+
defaultValues.value
|
|
177
|
+
) {
|
|
178
|
+
names.value.forEach((name) => {
|
|
179
|
+
if (defaultValues.value?.[name] === undefined) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
if (modelValue.value[name] !== undefined) {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
modelValue.value = {
|
|
186
|
+
...modelValue.value,
|
|
187
|
+
[name]: defaultValues.value?.[name],
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
onBeforeUnmount(() => {
|
|
193
|
+
if (injectedWrapperData) {
|
|
194
|
+
names.value.forEach((name) => {
|
|
195
|
+
injectedWrapperData.fields.value.delete(
|
|
196
|
+
`${fieldGroupId}-${name}`,
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const errors = computed(() => {
|
|
203
|
+
if (!injectedFormData?.errors.value) {
|
|
204
|
+
return undefined
|
|
205
|
+
}
|
|
206
|
+
const toReturn = names.value.reduce<Record<string, z.inferFormattedError<Schema>>>((acc, name) => {
|
|
207
|
+
if (!injectedFormData.errors.value) {
|
|
208
|
+
return acc
|
|
209
|
+
}
|
|
210
|
+
const error = get(injectedFormData.errors.value, String(name))
|
|
211
|
+
if (error === undefined) {
|
|
212
|
+
return acc
|
|
213
|
+
}
|
|
214
|
+
acc[String(name)] = error
|
|
215
|
+
return acc
|
|
216
|
+
}, {})
|
|
217
|
+
if (Object.keys(toReturn).length === 0) {
|
|
218
|
+
return undefined
|
|
219
|
+
}
|
|
220
|
+
return toReturn
|
|
221
|
+
})
|
|
222
|
+
const invalidLabels = computed(() => {
|
|
223
|
+
if (!errors.value) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
const toReturn = Object.keys(errors.value).reduce<Record<string, string[]>>((acc, name) => {
|
|
227
|
+
if (!errors.value?.[name]) {
|
|
228
|
+
return acc
|
|
229
|
+
}
|
|
230
|
+
acc[namesKeysMap.value[name]] = errors.value[name]._errors
|
|
231
|
+
return acc
|
|
232
|
+
}, {})
|
|
233
|
+
if (Object.keys(toReturn).length === 0) {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
return toReturn
|
|
237
|
+
})
|
|
238
|
+
const invalid = computed(() => {
|
|
239
|
+
return errors.value !== undefined
|
|
240
|
+
})
|
|
241
|
+
const invalids = computed(() => {
|
|
242
|
+
return namesKeys.value.reduce<Record<string, boolean>>((acc, name) => {
|
|
243
|
+
acc[name] = Boolean(errors.value?.[namesKeysMap.value[name]])
|
|
244
|
+
return acc
|
|
245
|
+
}, {})
|
|
246
|
+
})
|
|
247
|
+
const unwatchInvalid = watch(invalid, () => {
|
|
248
|
+
if (invalid.value) {
|
|
249
|
+
emit('invalid', errors.value)
|
|
250
|
+
if (injectedWrapperData) {
|
|
251
|
+
names.value.forEach((name) => {
|
|
252
|
+
if (!errors.value?.[name]) {
|
|
253
|
+
injectedWrapperData.errors.value.delete(
|
|
254
|
+
name,
|
|
255
|
+
)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
injectedWrapperData.errors.value.set(
|
|
259
|
+
name,
|
|
260
|
+
errors.value?.[name],
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
emit('valid', modelValue.value)
|
|
267
|
+
if (injectedWrapperData) {
|
|
268
|
+
names.value.forEach((name) => {
|
|
269
|
+
injectedWrapperData.errors.value.delete(
|
|
270
|
+
name,
|
|
271
|
+
)
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
const unwatchInjectedFormData = watch(
|
|
276
|
+
() => injectedFormData?.formData,
|
|
277
|
+
() => {
|
|
278
|
+
emit('update:formData', injectedFormData?.formData)
|
|
279
|
+
},
|
|
280
|
+
{ deep: true },
|
|
281
|
+
)
|
|
282
|
+
onBeforeUnmount(() => {
|
|
283
|
+
unwatchInvalid()
|
|
284
|
+
unwatchInjectedFormData()
|
|
285
|
+
})
|
|
286
|
+
const onUpdate = (value: Record<string, any>) => {
|
|
287
|
+
modelValue.value = value
|
|
288
|
+
}
|
|
289
|
+
const onUpdateField = (name: string, value: unknown) => {
|
|
290
|
+
if (value instanceof InputEvent) {
|
|
291
|
+
value = (value.target as HTMLInputElement).value
|
|
292
|
+
}
|
|
293
|
+
if (!namesKeys.value.includes(name)) {
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
modelValue.value = {
|
|
297
|
+
...modelValue.value,
|
|
298
|
+
[name]: value,
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const hasFieldProps = computed(() => {
|
|
302
|
+
let toReturn = fieldProps.value
|
|
303
|
+
if (typeof toReturn === 'function') {
|
|
304
|
+
toReturn = toReturn(injectedFormData?.formData)
|
|
305
|
+
}
|
|
306
|
+
return Object.keys(toReturn).reduce<Record<string, unknown>>(
|
|
307
|
+
(acc, key) => {
|
|
308
|
+
acc[key] = unref(toReturn[key])
|
|
309
|
+
return acc
|
|
310
|
+
},
|
|
311
|
+
{},
|
|
312
|
+
)
|
|
313
|
+
})
|
|
314
|
+
const isReadonly = computed(() => {
|
|
315
|
+
if (injectedFormData?.readonly.value) {
|
|
316
|
+
return true
|
|
317
|
+
}
|
|
318
|
+
return (hasFieldProps.value.readonly ?? props.readonly) as boolean
|
|
319
|
+
})
|
|
320
|
+
const onUpdateEvents = computed(() => {
|
|
321
|
+
return namesKeys.value.reduce<Record<string, (value: any) => void>>((acc, name) => {
|
|
322
|
+
acc[`onUpdate:${name}`] = (value) => {
|
|
323
|
+
onUpdateField(name, value)
|
|
324
|
+
}
|
|
325
|
+
return acc
|
|
326
|
+
}, {
|
|
327
|
+
'onUpdate:modelValue': onUpdate,
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
const hasProps = computed(() => ({
|
|
331
|
+
...onUpdateEvents.value,
|
|
332
|
+
...hasFieldProps.value,
|
|
333
|
+
...modelValue.value,
|
|
334
|
+
modelValue: modelValue.value,
|
|
335
|
+
names: hasFieldProps.value.name ?? names.value,
|
|
336
|
+
invalid: invalid.value,
|
|
337
|
+
invalids: invalids.value,
|
|
338
|
+
valid: props.showValid
|
|
339
|
+
? Boolean(!invalid.value && modelValue.value)
|
|
340
|
+
: undefined,
|
|
341
|
+
invalidLabels: invalidLabels.value,
|
|
342
|
+
readonly: isReadonly.value,
|
|
343
|
+
}))
|
|
344
|
+
|
|
345
|
+
// provide data to children
|
|
346
|
+
provide(formFieldsGroupInjectionKey, {
|
|
347
|
+
names: readonly(fieldsNames) as DeepReadonly<Ref<Path<z.infer<Schema>>[]>>,
|
|
348
|
+
errors: readonly(errors),
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
// define component
|
|
352
|
+
const component = computed(() => ({
|
|
353
|
+
render() {
|
|
354
|
+
return (
|
|
355
|
+
slots.default?.({
|
|
356
|
+
errors: errors.value,
|
|
357
|
+
formData: injectedFormData?.formData.value,
|
|
358
|
+
formErrors: injectedFormData?.errors.value,
|
|
359
|
+
invalid: invalid.value,
|
|
360
|
+
invalids: invalids.value,
|
|
361
|
+
invalidLabels: invalidLabels.value,
|
|
362
|
+
modelValue: modelValue.value,
|
|
363
|
+
onUpdate,
|
|
364
|
+
onUpdateField,
|
|
365
|
+
readonly: isReadonly.value,
|
|
366
|
+
submit: injectedFormData?.submit,
|
|
367
|
+
validate: injectedFormData?.validate,
|
|
368
|
+
}) ?? slots.default
|
|
369
|
+
)
|
|
370
|
+
},
|
|
371
|
+
}))
|
|
372
|
+
|
|
373
|
+
return { component, hasProps, invalid }
|
|
374
|
+
},
|
|
375
|
+
render() {
|
|
376
|
+
if (this.is) {
|
|
377
|
+
return h(this.is, this.hasProps, this.$slots)
|
|
378
|
+
}
|
|
379
|
+
return h(this.component, null, this.$slots)
|
|
380
|
+
},
|
|
381
|
+
})
|
|
382
|
+
}
|