@volverjs/form-vue 1.0.0-beta.3 → 1.0.0-beta.31

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 CHANGED
@@ -1,264 +1,357 @@
1
1
  import {
2
- type Component,
3
- type InjectionKey,
4
- type DeepReadonly,
5
- type Ref,
6
- type PropType,
7
- type WatchStopHandle,
8
- withModifiers,
9
- defineComponent,
10
- ref,
11
- provide,
12
- readonly,
13
- watch,
14
- h,
15
- toRaw,
16
- isProxy,
17
- computed,
2
+ type Component,
3
+ type InjectionKey,
4
+ type PropType,
5
+ type SlotsType,
6
+ type UnwrapRef,
7
+ computed,
8
+ defineComponent,
9
+ h,
10
+ isProxy,
11
+ onMounted,
12
+ provide,
13
+ readonly as makeReadonly,
14
+ ref,
15
+ toRaw,
16
+ watch,
17
+ withModifiers,
18
18
  } from 'vue'
19
19
  import {
20
- watchIgnorable,
21
- throttleFilter,
22
- type IgnoredUpdater,
20
+ throttleFilter,
21
+ watchIgnorable,
23
22
  } from '@vueuse/core'
24
- import { type z, type ZodFormattedError, type TypeOf } from 'zod'
23
+ import { type z, ZodError } from 'zod'
25
24
  import type {
26
- FormComponentOptions,
27
- FormSchema,
28
- FormTemplate,
29
- InjectedFormData,
25
+ FormComponentOptions,
26
+ FormSchema,
27
+ FormTemplate,
28
+ InjectedFormData,
29
+ InjectedFormWrapperData,
30
30
  } from './types'
31
31
  import { FormStatus } from './enums'
32
32
  import { defaultObjectBySchema } from './utils'
33
33
 
34
- export const defineForm = <Schema extends FormSchema>(
35
- schema: Schema,
36
- provideKey: InjectionKey<InjectedFormData<Schema>>,
37
- options?: FormComponentOptions<Schema>,
38
- VvFormTemplate?: Component,
39
- ) => {
40
- const errors = ref<z.inferFormattedError<Schema> | undefined>()
41
- const status = ref<FormStatus | undefined>()
42
- const invalid = computed(() => status.value === FormStatus.invalid)
43
- const formData = ref<Partial<z.infer<Schema> | undefined>>()
34
+ export function defineForm<Schema extends FormSchema, Type, FormTemplateComponent extends Component>(schema: Schema, provideKey: InjectionKey<InjectedFormData<Schema, Type>>, options: FormComponentOptions<Schema, Type>, VvFormTemplate: FormTemplateComponent, wrappers: Map<string, InjectedFormWrapperData<Schema>>) {
35
+ const errors = ref<z.inferFormattedError<Schema> | undefined>()
36
+ const status = ref<FormStatus | undefined>()
37
+ const invalid = computed(() => status.value === FormStatus.invalid)
38
+ const formData = ref<undefined extends Type ? Partial<z.infer<Schema>> : Type>()
39
+ const readonly = ref<boolean>(false)
40
+ let validationFields: Set<string> | undefined
44
41
 
45
- const validate = async (value = formData.value) => {
46
- const parseResult = await schema.safeParseAsync(value)
47
- if (!parseResult.success) {
48
- errors.value = parseResult.error.format() as ZodFormattedError<
49
- z.infer<Schema>
50
- >
51
- status.value = FormStatus.invalid
52
- return false
53
- }
54
- errors.value = undefined
55
- status.value = FormStatus.valid
56
- formData.value = parseResult.data
57
- return true
58
- }
42
+ const formDataAdapter = (data?: z.infer<Schema>): undefined extends Type ? Partial<z.infer<Schema>> : Type => {
43
+ const toReturn = defaultObjectBySchema(schema, data)
44
+ if (options?.class) {
45
+ const ClassObject = options.class
46
+ // @ts-expect-error - this is a class
47
+ return new ClassObject(toReturn)
48
+ }
49
+ // @ts-expect-error - this is a plain object
50
+ return toReturn
51
+ }
59
52
 
60
- const submit = async () => {
61
- if (!(await validate())) {
62
- return false
63
- }
64
- status.value = FormStatus.submitting
65
- return true
66
- }
53
+ const validate = async (value = formData.value, fields?: Set<string>) => {
54
+ validationFields = fields
55
+ if (readonly.value) {
56
+ return true
57
+ }
58
+ const parseResult = await schema.safeParseAsync(value)
59
+ if (!parseResult.success) {
60
+ status.value = FormStatus.invalid
61
+ if (!fields) {
62
+ errors.value = parseResult.error.format() as z.inferFormattedError<Schema>
63
+ return false
64
+ }
65
+ const fieldsIssues = parseResult.error.issues.filter(item => fields.has(item.path.join('.')))
66
+ if (!fieldsIssues.length) {
67
+ errors.value = undefined
68
+ return true
69
+ }
70
+ errors.value = new ZodError(fieldsIssues).format() as z.inferFormattedError<Schema>
71
+ return false
72
+ }
73
+ errors.value = undefined
74
+ status.value = FormStatus.valid
75
+ formData.value = formDataAdapter(parseResult.data)
76
+ return true
77
+ }
67
78
 
68
- const { ignoreUpdates, stop: stopUpdatesWatch } = watchIgnorable(
69
- formData,
70
- () => {
71
- status.value = FormStatus.updated
72
- },
73
- {
74
- deep: true,
75
- eventFilter: throttleFilter(options?.updateThrottle ?? 500),
76
- },
77
- )
79
+ const clear = () => {
80
+ errors.value = undefined
81
+ status.value = undefined
82
+ validationFields = undefined
83
+ }
78
84
 
79
- const component = defineComponent({
80
- name: 'VvForm',
81
- props: {
82
- continuosValidation: {
83
- type: Boolean,
84
- default: false,
85
- },
86
- modelValue: {
87
- type: Object,
88
- default: () => ({}),
89
- },
90
- tag: {
91
- type: String,
92
- default: 'form',
93
- },
94
- template: {
95
- type: [Array, Function] as PropType<FormTemplate<Schema>>,
96
- default: undefined,
97
- },
98
- },
99
- emits: ['invalid', 'valid', 'submit', 'update:modelValue'],
100
- expose: ['submit', 'validate', 'errors', 'status', 'valid', 'invalid'],
101
- setup(props, { emit }) {
102
- formData.value = defaultObjectBySchema(
103
- schema,
104
- toRaw(props.modelValue),
105
- )
85
+ const reset = () => {
86
+ formData.value = formDataAdapter()
87
+ clear()
88
+ status.value = FormStatus.reset
89
+ }
106
90
 
107
- watch(
108
- () => props.modelValue,
109
- (newValue) => {
110
- if (newValue) {
111
- const original = isProxy(newValue)
112
- ? toRaw(newValue)
113
- : newValue
114
- formData.value =
115
- typeof original?.clone === 'function'
116
- ? original.clone()
117
- : JSON.parse(JSON.stringify(original))
118
- }
119
- },
120
- { deep: true },
121
- )
91
+ const submit = async () => {
92
+ if (readonly.value) {
93
+ return false
94
+ }
95
+ if (!(await validate())) {
96
+ return false
97
+ }
98
+ status.value = FormStatus.submitting
99
+ return true
100
+ }
122
101
 
123
- watch(status, async (newValue) => {
124
- if (newValue === FormStatus.invalid) {
125
- const toReturn = toRaw(
126
- errors.value as ZodFormattedError<z.infer<Schema>>,
127
- )
128
- emit('invalid', toReturn)
129
- options?.onInvalid?.(toReturn)
130
- return
131
- }
132
- if (newValue === FormStatus.valid) {
133
- const toReturn = toRaw(formData.value as z.infer<Schema>)
134
- emit('valid', toReturn)
135
- options?.onValid?.(toReturn)
136
- emit('update:modelValue', toReturn)
137
- options?.onUpdate?.(toReturn)
138
- return
139
- }
140
- if (newValue === FormStatus.submitting) {
141
- const toReturn = toRaw(formData.value as z.infer<Schema>)
142
- emit('submit', toReturn)
143
- options?.onSubmit?.(toReturn)
144
- }
145
- if (newValue === FormStatus.updated) {
146
- if (
147
- errors.value ||
148
- options?.continuosValidation ||
149
- props.continuosValidation
150
- ) {
151
- await validate()
152
- }
153
- if (
154
- !formData.value ||
155
- !props.modelValue ||
156
- JSON.stringify(formData.value) !==
157
- JSON.stringify(props.modelValue)
158
- ) {
159
- const toReturn = toRaw(
160
- formData.value as z.infer<Schema>,
161
- )
162
- emit('update:modelValue', toReturn)
163
- options?.onUpdate?.(toReturn)
164
- }
165
- if (status.value === FormStatus.updated) {
166
- status.value = FormStatus.unknown
167
- }
168
- }
169
- })
102
+ const { ignoreUpdates, stop: stopUpdatesWatch } = watchIgnorable(
103
+ formData,
104
+ () => {
105
+ status.value = FormStatus.updated
106
+ },
107
+ {
108
+ deep: true,
109
+ eventFilter: throttleFilter(options?.updateThrottle ?? 500),
110
+ },
111
+ )
170
112
 
171
- provide(provideKey, {
172
- formData,
173
- submit,
174
- validate,
175
- ignoreUpdates,
176
- stopUpdatesWatch,
177
- errors: readonly(errors),
178
- status: readonly(status),
179
- invalid,
180
- })
113
+ const readonlyErrors = makeReadonly(errors)
114
+ const readonlyStatus = makeReadonly(status)
181
115
 
182
- return {
183
- formData,
184
- submit,
185
- validate,
186
- ignoreUpdates,
187
- stopUpdatesWatch,
188
- errors: readonly(errors),
189
- status: readonly(status),
190
- invalid,
191
- }
192
- },
193
- render() {
194
- const defaultSlot = () =>
195
- this.$slots?.default?.({
196
- formData: this.formData,
197
- submit: this.submit,
198
- validate: this.validate,
199
- ignoreUpdates: this.ignoreUpdates,
200
- stopUpdatesWatch: this.stopUpdatesWatch,
201
- errors: this.errors,
202
- status: this.status,
203
- invalid: this.invalid,
204
- }) ?? this.$slots.default
205
- return h(
206
- this.tag,
207
- {
208
- onSubmit: withModifiers(this.submit, ['prevent']),
209
- },
210
- (this.template ?? options?.template) && VvFormTemplate
211
- ? [
212
- h(
213
- VvFormTemplate,
214
- {
215
- schema: this.template ?? options?.template,
216
- },
217
- {
218
- default: defaultSlot,
219
- },
220
- ),
221
- ]
222
- : {
223
- default: defaultSlot,
224
- },
225
- )
226
- },
227
- })
228
- return {
229
- errors,
230
- status,
231
- invalid,
232
- formData,
233
- validate,
234
- submit,
235
- ignoreUpdates,
236
- stopUpdatesWatch,
237
- /**
238
- * An hack to add types to the default slot
239
- */
240
- VvForm: component as typeof component & {
241
- new (): {
242
- $slots: {
243
- default: (_: {
244
- formData: unknown extends
245
- | Partial<TypeOf<Schema>>
246
- | undefined
247
- ? undefined
248
- : Partial<TypeOf<Schema>> | undefined
249
- submit: () => Promise<boolean>
250
- validate: () => Promise<boolean>
251
- ignoreUpdates: IgnoredUpdater
252
- stopUpdatesWatch: WatchStopHandle
253
- errors: Readonly<
254
- Ref<DeepReadonly<z.inferFormattedError<Schema>>>
255
- >
256
- status: Ref<DeepReadonly<`${FormStatus}` | undefined>>
257
- invalid: Ref<DeepReadonly<boolean>>
258
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
259
- }) => any
260
- }
261
- }
262
- },
263
- }
116
+ const VvForm = defineComponent({
117
+ name: 'VvForm',
118
+ props: {
119
+ continuousValidation: {
120
+ type: Boolean,
121
+ default: false,
122
+ },
123
+ modelValue: {
124
+ type: Object,
125
+ default: () => ({}),
126
+ },
127
+ readonly: {
128
+ type: Boolean,
129
+ default: options?.readonly ?? false,
130
+ },
131
+ tag: {
132
+ type: String,
133
+ default: 'form',
134
+ },
135
+ template: {
136
+ type: [Array, Function] as PropType<FormTemplate<Schema, Type>>,
137
+ default: undefined,
138
+ },
139
+ },
140
+ emits: [
141
+ 'invalid',
142
+ 'submit',
143
+ 'update:modelValue',
144
+ 'update:readonly',
145
+ 'valid',
146
+ 'reset',
147
+ ],
148
+ expose: [
149
+ 'errors',
150
+ 'invalid',
151
+ 'readonly',
152
+ 'status',
153
+ 'submit',
154
+ 'tag',
155
+ 'template',
156
+ 'valid',
157
+ 'validate',
158
+ 'clear',
159
+ 'reset',
160
+ ],
161
+ slots: Object as SlotsType<{
162
+ default: {
163
+ errors: UnwrapRef<typeof readonlyErrors>
164
+ formData: UnwrapRef<typeof formData>
165
+ invalid: UnwrapRef<typeof invalid>
166
+ readonly: UnwrapRef<typeof readonly>
167
+ status: UnwrapRef<typeof readonlyStatus>
168
+ wrappers: typeof wrappers
169
+ clear: typeof clear
170
+ ignoreUpdates: typeof ignoreUpdates
171
+ reset: typeof reset
172
+ stopUpdatesWatch: typeof stopUpdatesWatch
173
+ submit: typeof submit
174
+ validate: typeof validate
175
+ }
176
+ }>,
177
+ setup(props, { emit }) {
178
+ formData.value = formDataAdapter(toRaw(props.modelValue))
179
+
180
+ watch(
181
+ () => props.modelValue,
182
+ (newValue) => {
183
+ if (newValue) {
184
+ const original = isProxy(newValue)
185
+ ? toRaw(newValue)
186
+ : newValue
187
+
188
+ if (
189
+ JSON.stringify(original)
190
+ === JSON.stringify(toRaw(formData.value))
191
+ ) {
192
+ return
193
+ }
194
+
195
+ formData.value = typeof original?.clone === 'function'
196
+ ? original.clone()
197
+ : JSON.parse(JSON.stringify(original))
198
+ }
199
+ },
200
+ { deep: true },
201
+ )
202
+
203
+ watch(status, async (newValue) => {
204
+ if (newValue === FormStatus.invalid) {
205
+ const toReturn = toRaw(errors.value)
206
+ emit('invalid', toReturn)
207
+ options?.onInvalid?.(
208
+ toReturn as z.inferFormattedError<Schema> | undefined,
209
+ )
210
+ return
211
+ }
212
+ if (newValue === FormStatus.valid) {
213
+ const toReturn = toRaw(formData.value)
214
+ emit('valid', toReturn)
215
+ options?.onValid?.(toReturn)
216
+ emit('update:modelValue', toReturn)
217
+ options?.onUpdate?.(toReturn)
218
+ return
219
+ }
220
+ if (newValue === FormStatus.submitting) {
221
+ const toReturn = toRaw(formData.value)
222
+ emit('submit', toReturn)
223
+ options?.onSubmit?.(toReturn)
224
+ return
225
+ }
226
+ if (newValue === FormStatus.reset) {
227
+ const toReturn = toRaw(formData.value)
228
+ emit('reset', toReturn)
229
+ options?.onReset?.(toReturn)
230
+ return
231
+ }
232
+ if (newValue === FormStatus.updated) {
233
+ if (
234
+ errors.value
235
+ || options?.continuousValidation
236
+ || props.continuousValidation
237
+ ) {
238
+ await validate(undefined, validationFields)
239
+ }
240
+ if (
241
+ !formData.value
242
+ || !props.modelValue
243
+ || JSON.stringify(formData.value)
244
+ !== JSON.stringify(props.modelValue)
245
+ ) {
246
+ const toReturn = toRaw(formData.value)
247
+ emit('update:modelValue', toReturn)
248
+ options?.onUpdate?.(toReturn)
249
+ }
250
+ if (status.value === FormStatus.updated) {
251
+ status.value = FormStatus.unknown
252
+ }
253
+ }
254
+ })
255
+
256
+ // readonly
257
+ onMounted(() => {
258
+ readonly.value = props.readonly
259
+ })
260
+ watch(
261
+ () => props.readonly,
262
+ (newValue) => {
263
+ readonly.value = newValue
264
+ },
265
+ )
266
+ watch(readonly, (newValue) => {
267
+ if (newValue !== props.readonly) {
268
+ emit('update:readonly', readonly.value)
269
+ }
270
+ })
271
+
272
+ provide(provideKey, {
273
+ clear,
274
+ errors: readonlyErrors,
275
+ formData,
276
+ ignoreUpdates,
277
+ invalid,
278
+ readonly,
279
+ reset,
280
+ status: readonlyStatus,
281
+ stopUpdatesWatch,
282
+ submit,
283
+ validate,
284
+ wrappers,
285
+ })
286
+
287
+ return {
288
+ clear,
289
+ errors: readonlyErrors,
290
+ formData,
291
+ ignoreUpdates,
292
+ invalid,
293
+ isReadonly: readonly,
294
+ reset,
295
+ status: readonlyStatus,
296
+ stopUpdatesWatch,
297
+ submit,
298
+ validate,
299
+ wrappers,
300
+ }
301
+ },
302
+ render() {
303
+ const defaultSlot = () =>
304
+ this.$slots?.default?.({
305
+ errors: readonlyErrors.value,
306
+ formData: formData.value,
307
+ invalid: invalid.value,
308
+ readonly: readonly.value,
309
+ status: readonlyStatus.value,
310
+ wrappers,
311
+ clear,
312
+ ignoreUpdates,
313
+ reset,
314
+ stopUpdatesWatch,
315
+ submit,
316
+ validate,
317
+ }) ?? this.$slots.default
318
+ return h(
319
+ this.tag,
320
+ {
321
+ onSubmit: withModifiers(this.submit, ['prevent']),
322
+ onReset: withModifiers(this.reset, ['prevent']),
323
+ },
324
+ (this.template ?? options?.template) && VvFormTemplate
325
+ ? [
326
+ h(
327
+ VvFormTemplate,
328
+ {
329
+ schema: this.template ?? options?.template,
330
+ },
331
+ {
332
+ default: defaultSlot,
333
+ },
334
+ ),
335
+ ]
336
+ : {
337
+ default: defaultSlot,
338
+ },
339
+ )
340
+ },
341
+ })
342
+ return {
343
+ clear,
344
+ errors,
345
+ formData,
346
+ ignoreUpdates,
347
+ invalid,
348
+ readonly,
349
+ reset,
350
+ status,
351
+ wrappers,
352
+ stopUpdatesWatch,
353
+ submit,
354
+ validate,
355
+ VvForm,
356
+ }
264
357
  }