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