Skip to content

Commit cc97360

Browse files
committed
feat(form): validateOnSubmit + pickOnlyChanged, closes #47
1 parent 787889d commit cc97360

File tree

3 files changed

+101
-13
lines changed

3 files changed

+101
-13
lines changed

packages/vue/src/api.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { createItem, createMany, deleteItem, deleteMany, findFirst, findMany, pe
88
import { pickNonSpecialProps } from '@rstore/shared'
99
import { tryOnScopeDispose } from '@vueuse/core'
1010
import { ref, toValue, watch } from 'vue'
11-
import { createFormObject, type VueCreateFormObject, type VueUpdateFormObject } from './form'
11+
import { createFormObject, type CreateFormObjectOptions, type VueCreateFormObject, type VueUpdateFormObject } from './form'
1212
import { createQuery } from './query'
1313

1414
export type QueryType = 'first' | 'many'
@@ -199,7 +199,8 @@ export interface VueCollectionApi<
199199
* @default collection.schema.create
200200
*/
201201
schema?: StandardSchemaV1
202-
} & Pick<CreateOptions<TCollection, TCollectionDefaults, TSchema>, 'optimistic'>,
202+
} & Pick<CreateOptions<TCollection, TCollectionDefaults, TSchema>, 'optimistic'>
203+
& Pick<CreateFormObjectOptions<ResolvedCollectionItem<TCollection, TCollectionDefaults, TSchema>, StandardSchemaV1, never>, 'resetOnSuccess' | 'validateOnSubmit' | 'transformData'>,
203204
) => VueCreateFormObject<TCollection, TCollectionDefaults, TSchema>
204205

205206
/**
@@ -237,7 +238,14 @@ export interface VueCollectionApi<
237238
* @default collection.schema.update
238239
*/
239240
schema?: StandardSchemaV1
240-
} & Pick<UpdateOptions<TCollection, TCollectionDefaults, TSchema>, 'optimistic'>,
241+
242+
/**
243+
* If `true`, only the changed properties will be sent to the `update` method. If `false`, all properties will be sent.
244+
* @default true
245+
*/
246+
pickOnlyChanged?: boolean
247+
} & Pick<UpdateOptions<TCollection, TCollectionDefaults, TSchema>, 'optimistic'>
248+
& Pick<CreateFormObjectOptions<ResolvedCollectionItem<TCollection, TCollectionDefaults, TSchema>, StandardSchemaV1, never>, 'resetOnSuccess' | 'validateOnSubmit' | 'transformData'>,
241249
) => Promise<VueUpdateFormObject<TCollection, TCollectionDefaults, TSchema>>
242250

243251
/**
@@ -485,6 +493,9 @@ export function createCollectionApi<
485493
submit: data => api.create(data, {
486494
optimistic: formOptions?.optimistic,
487495
}),
496+
resetOnSuccess: formOptions?.resetOnSuccess,
497+
validateOnSubmit: formOptions?.validateOnSubmit,
498+
transformData: formOptions?.transformData,
488499
}) as TReturn
489500
},
490501

@@ -526,16 +537,26 @@ export function createCollectionApi<
526537
resetDefaultValues: () => getFormData(),
527538
// Only use changed props
528539
transformData: (form) => {
529-
const data = {} as any
530-
for (const key in form.$changedProps) {
531-
data[key] = form[key]
540+
let data = {} as any
541+
if (formOptions?.pickOnlyChanged ?? true) {
542+
for (const key in form.$changedProps) {
543+
data[key] = form[key]
544+
}
545+
}
546+
else {
547+
data = { ...form }
548+
}
549+
if (formOptions?.transformData) {
550+
data = formOptions.transformData(data)
532551
}
533552
return data
534553
},
535554
submit: data => api.update(data, {
536555
key: collection.getKey(initialData),
537556
optimistic: formOptions?.optimistic,
538557
}),
558+
resetOnSuccess: formOptions?.resetOnSuccess,
559+
validateOnSubmit: formOptions?.validateOnSubmit,
539560
})
540561
return form
541562
},

packages/vue/src/form.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ export interface CreateFormObjectOptions<
4646
* @default true
4747
*/
4848
resetOnSuccess?: boolean
49+
/**
50+
* If `true`, the form will be validated using the `schema` when `$submit()` is called. If `false`, the form will not be validated automatically, and you will need to validate it manually if needed.
51+
* @default true
52+
*/
53+
validateOnSubmit?: boolean
4954
}
5055

5156
export type FormObjectChanged<TData> = {
@@ -134,15 +139,17 @@ export function createFormObject<
134139
form.$error = null
135140
try {
136141
const data = options?.transformData ? options.transformData(form as unknown as Partial<TData>) : pickNonSpecialProps(form, true) as Partial<TData>
137-
const { issues } = await this.$schema['~standard'].validate(data)
138-
if (issues) {
139-
const error = new Error(issues.map(i => i.message).join(', '))
140-
;(error as any).$issues = issues
141-
throw error
142+
if (options.validateOnSubmit ?? true) {
143+
const { issues } = await this.$schema['~standard'].validate(data)
144+
if (issues) {
145+
const error = new Error(issues.map(i => i.message).join(', '))
146+
;(error as any).$issues = issues
147+
throw error
148+
}
142149
}
143150
const item = await options.submit(data)
144151
onSuccess.trigger(item)
145-
if (options.resetOnSuccess || options.resetOnSuccess == null) {
152+
if (options.resetOnSuccess ?? true) {
146153
await form.$reset()
147154
}
148155
return item

packages/vue/test/form.spec.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it } from 'vitest'
1+
import { describe, expect, it, vi } from 'vitest'
22
import { createFormObject } from '../src'
33

44
describe('createFormObject', () => {
@@ -47,4 +47,64 @@ describe('createFormObject', () => {
4747

4848
expect(obj.name).toBe('Jane')
4949
})
50+
51+
it('validates the form before submit', async () => {
52+
const submit = vi.fn()
53+
const obj = createFormObject({
54+
defaultValues: () => ({ name: 'John' }),
55+
schema: {
56+
'~standard': {
57+
version: 1,
58+
vendor: 'test',
59+
validate: async (data: any) => {
60+
const issues = []
61+
if (!data.name) {
62+
issues.push({ message: 'Name is required' })
63+
}
64+
return { issues: issues.length ? issues : undefined, value: data }
65+
},
66+
},
67+
},
68+
submit,
69+
})
70+
71+
obj.name = ''
72+
73+
await expect(() => obj.$submit()).rejects.toThrow('Name is required')
74+
expect(submit).not.toHaveBeenCalled()
75+
76+
obj.name = 'Jane'
77+
78+
await obj.$submit()
79+
80+
expect(submit).toHaveBeenCalled()
81+
})
82+
83+
it('does not validate the form before submit when `validateOnSubmit` is false', async () => {
84+
const submit = vi.fn()
85+
const obj = createFormObject({
86+
defaultValues: () => ({ name: 'John' }),
87+
schema: {
88+
'~standard': {
89+
version: 1,
90+
vendor: 'test',
91+
validate: async (data: any) => {
92+
const issues = []
93+
if (!data.name) {
94+
issues.push({ message: 'Name is required' })
95+
}
96+
return { issues: issues.length ? issues : undefined, value: data }
97+
},
98+
},
99+
},
100+
submit,
101+
validateOnSubmit: false,
102+
})
103+
104+
obj.name = ''
105+
106+
await obj.$submit()
107+
108+
expect(submit).toHaveBeenCalledWith({ name: '' })
109+
})
50110
})

0 commit comments

Comments
 (0)