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