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