@volverjs/form-vue 0.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.
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/dist/VvForm.d.ts +29 -0
- package/dist/VvFormField.d.ts +3 -0
- package/dist/VvFormWrapper.d.ts +33 -0
- package/dist/enums.d.ts +23 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.es.js +473 -0
- package/dist/index.umd.js +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/utils.d.ts +2 -0
- package/package.json +91 -0
- package/src/VvForm.ts +125 -0
- package/src/VvFormField.ts +299 -0
- package/src/VvFormWrapper.ts +122 -0
- package/src/enums.ts +23 -0
- package/src/index.ts +97 -0
- package/src/shims.d.ts +1 -0
- package/src/types.d.ts +30 -0
- package/src/utils.ts +22 -0
package/src/VvForm.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type InjectionKey,
|
|
3
|
+
withModifiers,
|
|
4
|
+
defineComponent,
|
|
5
|
+
ref,
|
|
6
|
+
provide,
|
|
7
|
+
readonly,
|
|
8
|
+
watch,
|
|
9
|
+
h,
|
|
10
|
+
toRaw,
|
|
11
|
+
isProxy,
|
|
12
|
+
} from 'vue'
|
|
13
|
+
import { watchThrottled } from '@vueuse/core'
|
|
14
|
+
|
|
15
|
+
import type { AnyZodObject } from 'zod'
|
|
16
|
+
import type { InjectedFormData } from './types'
|
|
17
|
+
import { defaultObjectBySchema } from './utils'
|
|
18
|
+
|
|
19
|
+
export enum FormStatus {
|
|
20
|
+
invalid = 'invalid',
|
|
21
|
+
valid = 'valid',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const defineForm = (
|
|
25
|
+
schema: AnyZodObject,
|
|
26
|
+
provideKey: InjectionKey<InjectedFormData>,
|
|
27
|
+
options?: {
|
|
28
|
+
updateThrottle?: number
|
|
29
|
+
},
|
|
30
|
+
) => {
|
|
31
|
+
return defineComponent({
|
|
32
|
+
name: 'FormComponent',
|
|
33
|
+
props: {
|
|
34
|
+
modelValue: {
|
|
35
|
+
type: Object,
|
|
36
|
+
default: () => ({}),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
emits: ['invalid', 'valid', 'submit', 'update:modelValue'],
|
|
40
|
+
expose: ['submit', 'errors', 'status'],
|
|
41
|
+
setup(props, { emit }) {
|
|
42
|
+
const localModelValue = ref(
|
|
43
|
+
defaultObjectBySchema(schema, props.modelValue),
|
|
44
|
+
)
|
|
45
|
+
watch(
|
|
46
|
+
() => props.modelValue,
|
|
47
|
+
(newValue) => {
|
|
48
|
+
if (newValue) {
|
|
49
|
+
const original = isProxy(newValue)
|
|
50
|
+
? toRaw(newValue)
|
|
51
|
+
: newValue
|
|
52
|
+
localModelValue.value =
|
|
53
|
+
typeof original?.clone === 'function'
|
|
54
|
+
? original.clone()
|
|
55
|
+
: JSON.parse(JSON.stringify(original))
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{ deep: true },
|
|
59
|
+
)
|
|
60
|
+
// v-model
|
|
61
|
+
watchThrottled(
|
|
62
|
+
localModelValue,
|
|
63
|
+
(newValue) => {
|
|
64
|
+
if (errors.value) {
|
|
65
|
+
parseModelValue()
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
!newValue ||
|
|
69
|
+
!props.modelValue ||
|
|
70
|
+
JSON.stringify(newValue) !==
|
|
71
|
+
JSON.stringify(props.modelValue)
|
|
72
|
+
) {
|
|
73
|
+
emit('update:modelValue', newValue)
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{ deep: true, throttle: options?.updateThrottle ?? 500 },
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// validation
|
|
80
|
+
const errors = ref()
|
|
81
|
+
const status = ref()
|
|
82
|
+
const parseModelValue = (value = localModelValue.value) => {
|
|
83
|
+
const parseResult = schema.safeParse(value)
|
|
84
|
+
if (!parseResult.success) {
|
|
85
|
+
errors.value = parseResult.error.format()
|
|
86
|
+
status.value = FormStatus.invalid
|
|
87
|
+
emit('invalid', errors.value)
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
errors.value = undefined
|
|
91
|
+
status.value = FormStatus.valid
|
|
92
|
+
localModelValue.value = parseResult.data
|
|
93
|
+
emit('valid', parseResult.data)
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// submit
|
|
98
|
+
const submit = () => {
|
|
99
|
+
if (!parseModelValue()) {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
emit('submit', localModelValue.value)
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// provide
|
|
107
|
+
provide(provideKey, {
|
|
108
|
+
modelValue: localModelValue,
|
|
109
|
+
submit,
|
|
110
|
+
errors: readonly(errors),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return { submit }
|
|
114
|
+
},
|
|
115
|
+
render() {
|
|
116
|
+
return h(
|
|
117
|
+
'form',
|
|
118
|
+
{
|
|
119
|
+
onSubmit: withModifiers(this.submit, ['prevent']),
|
|
120
|
+
},
|
|
121
|
+
this.$slots,
|
|
122
|
+
)
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { get, set } from 'ts-dot-prop'
|
|
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
|
+
} from 'vue'
|
|
20
|
+
import { FormFieldType } from './enums'
|
|
21
|
+
import type {
|
|
22
|
+
InjectedFormData,
|
|
23
|
+
InjectedFormWrapperData,
|
|
24
|
+
InjectedFormFieldData,
|
|
25
|
+
FormComposableOptions,
|
|
26
|
+
} from './types'
|
|
27
|
+
|
|
28
|
+
export const defineFormField = (
|
|
29
|
+
formProvideKey: InjectionKey<InjectedFormData>,
|
|
30
|
+
wrapperProvideKey: InjectionKey<InjectedFormWrapperData>,
|
|
31
|
+
formFieldInjectionKey: InjectionKey<InjectedFormFieldData>,
|
|
32
|
+
options: FormComposableOptions = {},
|
|
33
|
+
): Component => {
|
|
34
|
+
// define component
|
|
35
|
+
return defineComponent({
|
|
36
|
+
name: 'FieldComponent',
|
|
37
|
+
props: {
|
|
38
|
+
type: {
|
|
39
|
+
type: String as PropType<`${FormFieldType}`>,
|
|
40
|
+
validator: (value: FormFieldType) => {
|
|
41
|
+
return Object.values(FormFieldType).includes(value)
|
|
42
|
+
},
|
|
43
|
+
default: FormFieldType.custom,
|
|
44
|
+
},
|
|
45
|
+
is: {
|
|
46
|
+
type: [Object, String] as PropType<Component>,
|
|
47
|
+
default: undefined,
|
|
48
|
+
},
|
|
49
|
+
name: {
|
|
50
|
+
type: [String, Number, Boolean, Symbol],
|
|
51
|
+
required: true,
|
|
52
|
+
},
|
|
53
|
+
props: {
|
|
54
|
+
type: [Object, Function] as PropType<
|
|
55
|
+
| Record<string, unknown>
|
|
56
|
+
| ((
|
|
57
|
+
formData?: Ref<ObjectConstructor>,
|
|
58
|
+
) => Record<string, unknown>)
|
|
59
|
+
>,
|
|
60
|
+
default: () => ({}),
|
|
61
|
+
},
|
|
62
|
+
showValid: {
|
|
63
|
+
type: Boolean,
|
|
64
|
+
default: false,
|
|
65
|
+
},
|
|
66
|
+
defaultValue: {
|
|
67
|
+
type: [String, Number, Boolean, Array, Object],
|
|
68
|
+
default: undefined,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
emits: ['invalid', 'valid', 'update:formData', 'update:modelValue'],
|
|
72
|
+
expose: ['invalid', 'invalidLabel', 'errors'],
|
|
73
|
+
setup(props, { slots, emit }) {
|
|
74
|
+
// v-model
|
|
75
|
+
const modelValue = computed({
|
|
76
|
+
get() {
|
|
77
|
+
if (!formProvided?.modelValue) return
|
|
78
|
+
return get(
|
|
79
|
+
Object(formProvided.modelValue.value),
|
|
80
|
+
String(props.name),
|
|
81
|
+
)
|
|
82
|
+
},
|
|
83
|
+
set(value) {
|
|
84
|
+
if (!formProvided?.modelValue) return
|
|
85
|
+
set(
|
|
86
|
+
Object(formProvided.modelValue.value),
|
|
87
|
+
String(props.name),
|
|
88
|
+
value,
|
|
89
|
+
)
|
|
90
|
+
emit('update:modelValue', {
|
|
91
|
+
newValue: modelValue.value,
|
|
92
|
+
formData: formProvided?.modelValue,
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
onMounted(() => {
|
|
97
|
+
if (
|
|
98
|
+
modelValue.value === undefined &&
|
|
99
|
+
props.defaultValue !== undefined
|
|
100
|
+
) {
|
|
101
|
+
modelValue.value = props.defaultValue
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// inject data from parent form wrapper
|
|
106
|
+
const wrapperProvided = inject(wrapperProvideKey, undefined)
|
|
107
|
+
if (wrapperProvided) {
|
|
108
|
+
wrapperProvided.fields.value.add(props.name as string)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// inject data from parent form
|
|
112
|
+
const formProvided = inject(formProvideKey)
|
|
113
|
+
const { props: fieldProps, name: fieldName } = toRefs(props)
|
|
114
|
+
|
|
115
|
+
const errors = computed(() => {
|
|
116
|
+
if (!formProvided?.errors.value) {
|
|
117
|
+
return undefined
|
|
118
|
+
}
|
|
119
|
+
return get(formProvided.errors.value, String(props.name))
|
|
120
|
+
})
|
|
121
|
+
const invalidLabel = computed(() => {
|
|
122
|
+
return errors.value?._errors
|
|
123
|
+
})
|
|
124
|
+
const invalid = computed(() => {
|
|
125
|
+
return errors.value !== undefined
|
|
126
|
+
})
|
|
127
|
+
watch(invalid, () => {
|
|
128
|
+
if (invalid.value) {
|
|
129
|
+
emit('invalid', invalidLabel.value)
|
|
130
|
+
if (wrapperProvided) {
|
|
131
|
+
wrapperProvided.errors.value.set(props.name as string, {
|
|
132
|
+
_errors: invalidLabel.value,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
emit('valid', modelValue.value)
|
|
137
|
+
if (wrapperProvided) {
|
|
138
|
+
wrapperProvided.errors.value.delete(
|
|
139
|
+
props.name as string,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
watch(
|
|
145
|
+
() => formProvided?.modelValue,
|
|
146
|
+
() => {
|
|
147
|
+
emit('update:formData', formProvided?.modelValue)
|
|
148
|
+
},
|
|
149
|
+
{ deep: true },
|
|
150
|
+
)
|
|
151
|
+
const onUpdate = (value: unknown) => {
|
|
152
|
+
modelValue.value = value
|
|
153
|
+
}
|
|
154
|
+
const hasFieldProps = computed(() => {
|
|
155
|
+
if (typeof fieldProps.value === 'function') {
|
|
156
|
+
return fieldProps.value(formProvided?.modelValue)
|
|
157
|
+
}
|
|
158
|
+
return fieldProps.value
|
|
159
|
+
})
|
|
160
|
+
const hasProps = computed(() => ({
|
|
161
|
+
...hasFieldProps.value,
|
|
162
|
+
name: hasFieldProps.value.name ?? props.name,
|
|
163
|
+
invalid: invalid.value,
|
|
164
|
+
valid: props.showValid
|
|
165
|
+
? Boolean(!invalid.value && modelValue.value)
|
|
166
|
+
: undefined,
|
|
167
|
+
type: ((type: FormFieldType) => {
|
|
168
|
+
if (
|
|
169
|
+
[
|
|
170
|
+
FormFieldType.text,
|
|
171
|
+
FormFieldType.number,
|
|
172
|
+
FormFieldType.email,
|
|
173
|
+
FormFieldType.password,
|
|
174
|
+
FormFieldType.tel,
|
|
175
|
+
FormFieldType.url,
|
|
176
|
+
FormFieldType.search,
|
|
177
|
+
FormFieldType.date,
|
|
178
|
+
FormFieldType.time,
|
|
179
|
+
FormFieldType.datetimeLocal,
|
|
180
|
+
FormFieldType.month,
|
|
181
|
+
FormFieldType.week,
|
|
182
|
+
FormFieldType.color,
|
|
183
|
+
].includes(type)
|
|
184
|
+
) {
|
|
185
|
+
return type
|
|
186
|
+
}
|
|
187
|
+
return undefined
|
|
188
|
+
})(props.type as FormFieldType),
|
|
189
|
+
invalidLabel: invalidLabel.value,
|
|
190
|
+
modelValue: modelValue.value,
|
|
191
|
+
errors: props.is ? errors.value : undefined,
|
|
192
|
+
'onUpdate:modelValue': onUpdate,
|
|
193
|
+
}))
|
|
194
|
+
|
|
195
|
+
provide(formFieldInjectionKey, {
|
|
196
|
+
name: readonly(fieldName as Ref<string>),
|
|
197
|
+
errors: readonly(errors),
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const component = computed(() => {
|
|
201
|
+
if (props.type === FormFieldType.custom) {
|
|
202
|
+
return {
|
|
203
|
+
render() {
|
|
204
|
+
return (
|
|
205
|
+
slots.default?.({
|
|
206
|
+
modelValue: modelValue.value,
|
|
207
|
+
onUpdate,
|
|
208
|
+
invalid: invalid.value,
|
|
209
|
+
invalidLabel: invalidLabel.value,
|
|
210
|
+
formData: formProvided?.modelValue.value,
|
|
211
|
+
formErrors: formProvided?.errors.value,
|
|
212
|
+
errors: errors.value,
|
|
213
|
+
}) ?? slots.defalut
|
|
214
|
+
)
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!options.lazyLoad) {
|
|
219
|
+
let component: string | ConcreteComponent
|
|
220
|
+
switch (props.type) {
|
|
221
|
+
case FormFieldType.select:
|
|
222
|
+
component = resolveComponent('VvSelect')
|
|
223
|
+
break
|
|
224
|
+
case FormFieldType.checkbox:
|
|
225
|
+
component = resolveComponent('VvCheckbox')
|
|
226
|
+
break
|
|
227
|
+
case FormFieldType.radio:
|
|
228
|
+
component = resolveComponent('VvRadio')
|
|
229
|
+
break
|
|
230
|
+
case FormFieldType.textarea:
|
|
231
|
+
component = resolveComponent('VvTextarea')
|
|
232
|
+
break
|
|
233
|
+
case FormFieldType.radioGroup:
|
|
234
|
+
component = resolveComponent('VvRadioGroup')
|
|
235
|
+
break
|
|
236
|
+
case FormFieldType.checkboxGroup:
|
|
237
|
+
component = resolveComponent('VvCheckboxGroup')
|
|
238
|
+
break
|
|
239
|
+
case FormFieldType.combobox:
|
|
240
|
+
component = resolveComponent('VvCombobox')
|
|
241
|
+
break
|
|
242
|
+
default:
|
|
243
|
+
component = resolveComponent('VvInputText')
|
|
244
|
+
}
|
|
245
|
+
if (typeof component !== 'string') {
|
|
246
|
+
return component
|
|
247
|
+
} else {
|
|
248
|
+
console.warn(
|
|
249
|
+
`[form-vue warn]: ${component} not found, the component will be loaded asynchronously. To avoid this warning, please set "lazyLoad" option.`,
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return defineAsyncComponent(async () => {
|
|
254
|
+
if (options.sideEffects) {
|
|
255
|
+
await Promise.resolve(options.sideEffects(props.type))
|
|
256
|
+
}
|
|
257
|
+
switch (props.type) {
|
|
258
|
+
case FormFieldType.textarea:
|
|
259
|
+
return import(
|
|
260
|
+
'@volverjs/ui-vue/vv-textarea'
|
|
261
|
+
) as Component
|
|
262
|
+
case FormFieldType.radio:
|
|
263
|
+
return import(
|
|
264
|
+
'@volverjs/ui-vue/vv-radio'
|
|
265
|
+
) as Component
|
|
266
|
+
case FormFieldType.radioGroup:
|
|
267
|
+
return import(
|
|
268
|
+
'@volverjs/ui-vue/vv-radio-group'
|
|
269
|
+
) as Component
|
|
270
|
+
case FormFieldType.checkbox:
|
|
271
|
+
return import(
|
|
272
|
+
'@volverjs/ui-vue/vv-checkbox'
|
|
273
|
+
) as Component
|
|
274
|
+
case FormFieldType.checkboxGroup:
|
|
275
|
+
return import(
|
|
276
|
+
'@volverjs/ui-vue/vv-checkbox-group'
|
|
277
|
+
) as Component
|
|
278
|
+
case FormFieldType.combobox:
|
|
279
|
+
return import(
|
|
280
|
+
'@volverjs/ui-vue/vv-combobox'
|
|
281
|
+
) as Component
|
|
282
|
+
}
|
|
283
|
+
return import('@volverjs/ui-vue/vv-input-text') as Component
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
return { component, hasProps, invalid }
|
|
288
|
+
},
|
|
289
|
+
render() {
|
|
290
|
+
if (this.is) {
|
|
291
|
+
return h(this.is, this.hasProps, this.$slots)
|
|
292
|
+
}
|
|
293
|
+
if (this.type === FormFieldType.custom) {
|
|
294
|
+
return h(this.component as Component, null, this.$slots)
|
|
295
|
+
}
|
|
296
|
+
return h(this.component as Component, this.hasProps, this.$slots)
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type InjectionKey,
|
|
3
|
+
type Ref,
|
|
4
|
+
computed,
|
|
5
|
+
defineComponent,
|
|
6
|
+
inject,
|
|
7
|
+
provide,
|
|
8
|
+
readonly,
|
|
9
|
+
ref,
|
|
10
|
+
toRefs,
|
|
11
|
+
watch,
|
|
12
|
+
h,
|
|
13
|
+
} from 'vue'
|
|
14
|
+
import type { InjectedFormData, InjectedFormWrapperData } from './types'
|
|
15
|
+
|
|
16
|
+
export const defineFormWrapper = (
|
|
17
|
+
formProvideKey: InjectionKey<InjectedFormData>,
|
|
18
|
+
wrapperProvideKey: InjectionKey<InjectedFormWrapperData>,
|
|
19
|
+
) => {
|
|
20
|
+
return defineComponent({
|
|
21
|
+
name: 'WrapperComponent',
|
|
22
|
+
props: {
|
|
23
|
+
name: {
|
|
24
|
+
type: String,
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
tag: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: undefined,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
emits: ['invalid', 'valid'],
|
|
33
|
+
expose: ['fields', 'invalid'],
|
|
34
|
+
setup(props, { emit }) {
|
|
35
|
+
const formProvided = inject(formProvideKey)
|
|
36
|
+
const wrapperProvided = inject(wrapperProvideKey, undefined)
|
|
37
|
+
const fields = ref(new Set<string>())
|
|
38
|
+
const errors: Ref<
|
|
39
|
+
Map<string, Record<string, { _errors: string[] }>>
|
|
40
|
+
> = ref(new Map())
|
|
41
|
+
const { name } = toRefs(props)
|
|
42
|
+
|
|
43
|
+
// provide data to child fields
|
|
44
|
+
provide(wrapperProvideKey, {
|
|
45
|
+
name: readonly(name),
|
|
46
|
+
errors,
|
|
47
|
+
fields,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// add fields to parent wrapper
|
|
51
|
+
watch(
|
|
52
|
+
fields,
|
|
53
|
+
(newValue) => {
|
|
54
|
+
if (wrapperProvided?.fields) {
|
|
55
|
+
newValue.forEach((field) => {
|
|
56
|
+
wrapperProvided?.fields.value.add(field)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{ deep: true },
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// add fields to parent wrapper
|
|
64
|
+
watch(
|
|
65
|
+
() => new Map(errors.value),
|
|
66
|
+
(newValue, oldValue) => {
|
|
67
|
+
if (wrapperProvided?.errors) {
|
|
68
|
+
Array.from(oldValue.keys()).forEach((key) => {
|
|
69
|
+
wrapperProvided.errors.value.delete(key)
|
|
70
|
+
})
|
|
71
|
+
Array.from(newValue.keys()).forEach((key) => {
|
|
72
|
+
const value = newValue.get(key)
|
|
73
|
+
if (value) {
|
|
74
|
+
wrapperProvided.errors.value.set(key, value)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{ deep: true },
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const invalid = computed(() => {
|
|
83
|
+
if (!formProvided?.errors.value) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
return errors.value.size > 0
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
watch(invalid, () => {
|
|
90
|
+
if (invalid.value) {
|
|
91
|
+
emit('invalid')
|
|
92
|
+
} else {
|
|
93
|
+
emit('valid')
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return { formProvided, invalid, fields, errors }
|
|
98
|
+
},
|
|
99
|
+
render() {
|
|
100
|
+
if (this.tag) {
|
|
101
|
+
return h(
|
|
102
|
+
this.tag,
|
|
103
|
+
null,
|
|
104
|
+
this.$slots.default?.({
|
|
105
|
+
invalid: this.invalid,
|
|
106
|
+
formData: this.formProvided?.modelValue,
|
|
107
|
+
errors: this.formProvided?.errors,
|
|
108
|
+
fieldsErrors: this.errors,
|
|
109
|
+
}) ?? this.$slots.defalut,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
return (
|
|
113
|
+
this.$slots.default?.({
|
|
114
|
+
invalid: this.invalid,
|
|
115
|
+
formData: this.formProvided?.modelValue,
|
|
116
|
+
errors: this.formProvided?.errors,
|
|
117
|
+
fieldsErrors: this.errors,
|
|
118
|
+
}) ?? this.$slots.defalut
|
|
119
|
+
)
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
}
|
package/src/enums.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export enum FormFieldType {
|
|
2
|
+
text = 'text',
|
|
3
|
+
number = 'number',
|
|
4
|
+
email = 'email',
|
|
5
|
+
password = 'password',
|
|
6
|
+
tel = 'tel',
|
|
7
|
+
url = 'url',
|
|
8
|
+
search = 'search',
|
|
9
|
+
date = 'date',
|
|
10
|
+
time = 'time',
|
|
11
|
+
datetimeLocal = 'datetimeLocal',
|
|
12
|
+
month = 'month',
|
|
13
|
+
week = 'week',
|
|
14
|
+
color = 'color',
|
|
15
|
+
select = 'select',
|
|
16
|
+
checkbox = 'checkbox',
|
|
17
|
+
radio = 'radio',
|
|
18
|
+
textarea = 'textarea',
|
|
19
|
+
radioGroup = 'radioGroup',
|
|
20
|
+
checkboxGroup = 'checkboxGroup',
|
|
21
|
+
combobox = 'combobox',
|
|
22
|
+
custom = 'custom',
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type App, inject, type InjectionKey, type Plugin } from 'vue'
|
|
2
|
+
import type { AnyZodObject } from 'zod'
|
|
3
|
+
import { defineFormField } from './VvFormField'
|
|
4
|
+
import { defineForm } from './VvForm'
|
|
5
|
+
import { defineFormWrapper } from './VvFormWrapper'
|
|
6
|
+
import type {
|
|
7
|
+
InjectedFormData,
|
|
8
|
+
InjectedFormWrapperData,
|
|
9
|
+
InjectedFormFieldData,
|
|
10
|
+
FormComposableOptions,
|
|
11
|
+
FormPluginOptions,
|
|
12
|
+
} from './types'
|
|
13
|
+
|
|
14
|
+
export const formFactory = (
|
|
15
|
+
schema: AnyZodObject,
|
|
16
|
+
options: FormComposableOptions = {},
|
|
17
|
+
) => {
|
|
18
|
+
// create injection keys form provide/inject
|
|
19
|
+
const formInjectionKey = Symbol() as InjectionKey<InjectedFormData>
|
|
20
|
+
const formWrapperInjectionKey =
|
|
21
|
+
Symbol() as InjectionKey<InjectedFormWrapperData>
|
|
22
|
+
|
|
23
|
+
const formFieldInjectionKey =
|
|
24
|
+
Symbol() as InjectionKey<InjectedFormFieldData>
|
|
25
|
+
|
|
26
|
+
// create components
|
|
27
|
+
const VvForm = defineForm(schema, formInjectionKey, options)
|
|
28
|
+
const VvFormWrapper = defineFormWrapper(
|
|
29
|
+
formInjectionKey,
|
|
30
|
+
formWrapperInjectionKey,
|
|
31
|
+
)
|
|
32
|
+
const VvFormField = defineFormField(
|
|
33
|
+
formInjectionKey,
|
|
34
|
+
formWrapperInjectionKey,
|
|
35
|
+
formFieldInjectionKey,
|
|
36
|
+
options,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
VvForm,
|
|
41
|
+
VvFormWrapper,
|
|
42
|
+
VvFormField,
|
|
43
|
+
formInjectionKey,
|
|
44
|
+
formWrapperInjectionKey,
|
|
45
|
+
formFieldInjectionKey,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const pluginInjectionKey = Symbol() as InjectionKey<FormPluginOptions>
|
|
50
|
+
|
|
51
|
+
export const createForm = (
|
|
52
|
+
options: FormPluginOptions,
|
|
53
|
+
): Plugin & Partial<ReturnType<typeof useForm>> => {
|
|
54
|
+
let toReturn: Partial<ReturnType<typeof useForm>> = {}
|
|
55
|
+
if (options.schema) {
|
|
56
|
+
toReturn = formFactory(options.schema, options)
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...toReturn,
|
|
60
|
+
install(app: App, { global = false } = {}) {
|
|
61
|
+
app.provide(pluginInjectionKey, options)
|
|
62
|
+
|
|
63
|
+
if (global) {
|
|
64
|
+
app.config.globalProperties.$vvForm = options
|
|
65
|
+
|
|
66
|
+
if (toReturn?.VvForm) {
|
|
67
|
+
app.component('VvForm', toReturn.VvForm)
|
|
68
|
+
}
|
|
69
|
+
if (toReturn?.VvFormWrapper) {
|
|
70
|
+
app.component('VvFormWrapper', toReturn.VvFormWrapper)
|
|
71
|
+
}
|
|
72
|
+
if (toReturn?.VvFormField) {
|
|
73
|
+
app.component('VvFormField', toReturn.VvFormField)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const useForm = (
|
|
81
|
+
schema: AnyZodObject,
|
|
82
|
+
options: FormComposableOptions = {},
|
|
83
|
+
) => {
|
|
84
|
+
const hasOptions = { ...inject(pluginInjectionKey, {}), ...options }
|
|
85
|
+
return formFactory(schema, hasOptions)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { FormFieldType } from './enums'
|
|
89
|
+
export { defaultObjectBySchema } from './utils'
|
|
90
|
+
|
|
91
|
+
export type {
|
|
92
|
+
InjectedFormData,
|
|
93
|
+
InjectedFormWrapperData,
|
|
94
|
+
InjectedFormFieldData,
|
|
95
|
+
FormComposableOptions,
|
|
96
|
+
FormPluginOptions,
|
|
97
|
+
}
|
package/src/shims.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module '@volverjs/style/*'
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import type { ZodFormattedError } from 'zod'
|
|
3
|
+
import type { FormFieldType } from './enums'
|
|
4
|
+
|
|
5
|
+
export type FormComposableOptions = {
|
|
6
|
+
lazyLoad?: boolean
|
|
7
|
+
updateThrottle?: number
|
|
8
|
+
sideEffects?: (type: `${FormFieldType}`) => Promise | void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type FormPluginOptions = {
|
|
12
|
+
schema?: ZodSchema
|
|
13
|
+
} & FormComposableOptions
|
|
14
|
+
|
|
15
|
+
export type InjectedFormData<Type = Recrod<string | number, unknown>> = {
|
|
16
|
+
modelValue: Ref<Type>
|
|
17
|
+
errors: Ref<ZodFormattedError<Type>>
|
|
18
|
+
submit: () => boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type InjectedFormWrapperData = {
|
|
22
|
+
name: Ref<string>
|
|
23
|
+
fields: Ref<Set<string>>
|
|
24
|
+
errors: Ref<Map<string, Record<string, { _errors: string[] }>>>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type InjectedFormFieldData = {
|
|
28
|
+
name: Ref<string>
|
|
29
|
+
errors: Ref<Map<string, Record<string, { _errors: string[] }>>>
|
|
30
|
+
}
|