@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/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
+ }