@volverjs/form-vue 1.0.0-beta.2 → 1.0.0-beta.21

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