@volverjs/form-vue 1.0.0-beta.9 → 1.0.0

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