@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.
@@ -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
+ }