@volverjs/form-vue 1.0.0-beta.3 → 1.0.0-beta.30

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