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