@volverjs/form-vue 1.0.0-beta.2 → 1.0.0-beta.20

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