Skip to content

Commit 19b1152

Browse files
authored
Add defaults() to Core validator + React/Vue Inertia implementation (#135)
* Add `defaults()` to core validator * Refined Inertia `defaults()`/`setDefaults()` methods * fix typo * Port `vue-inertia` test to `react-inertia`
1 parent ab5b948 commit 19b1152

File tree

11 files changed

+451
-2
lines changed

11 files changed

+451
-2
lines changed

packages/core/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export interface Validator {
6565
on(event: keyof ValidatorListeners, callback: () => void): Validator,
6666
validateFiles(): Validator,
6767
withoutFileValidation(): Validator,
68+
defaults(data: Record<string, unknown>): Validator,
6869
}
6970

7071
export interface ValidatorListeners {

packages/core/src/validator.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,12 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
363363

364364
return form
365365
},
366+
defaults(data) {
367+
initialData = data
368+
oldData = data
369+
370+
return form
371+
},
366372
reset(...names) {
367373
if (names.length === 0) {
368374
setTouched([]).forEach((listener) => listener())

packages/core/tests/validator.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,3 +803,48 @@ it('marks fields as touched when the input has been included in validation', asy
803803

804804
await assertPendingValidateDebounceAndClear()
805805
})
806+
807+
it('can override the old data via the defaults function', () => {
808+
let requests = 0
809+
axios.request.mockImplementation(() => {
810+
requests++
811+
812+
return Promise.resolve(precognitionSuccessResponse())
813+
})
814+
815+
const validator = createValidator((client) => client.post('/foo', {}), {
816+
name: 'Tim',
817+
})
818+
819+
expect(validator.defaults({
820+
name: 'Jess',
821+
})).toBe(validator)
822+
823+
validator.validate('name', 'Jess')
824+
expect(requests).toBe(0)
825+
})
826+
827+
it('can override the initial data via the defaults function', async () => {
828+
expect.assertions(2)
829+
let requests = 0
830+
axios.request.mockImplementation(() => {
831+
requests++
832+
833+
return Promise.resolve(precognitionSuccessResponse())
834+
})
835+
836+
const validator = createValidator((client) => client.post('/foo', {}), {
837+
name: 'Tim',
838+
}).defaults({
839+
name: 'Jess',
840+
})
841+
842+
validator.validate('name', 'Taylor')
843+
expect(requests).toBe(1)
844+
845+
await vi.advanceTimersByTimeAsync(1500)
846+
847+
validator.reset('name')
848+
validator.validate('name', 'Jess')
849+
expect(requests).toBe(1)
850+
})

packages/react-inertia/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"build": "rm -rf dist && tsc",
2626
"typeCheck": "tsc --noEmit",
2727
"prepublishOnly": "npm run build",
28+
"test": "vitest run",
2829
"version": "npm pkg set dependencies.laravel-precognition=$npm_package_version && npm pkg set dependencies.laravel-precognition-react=$npm_package_version"
2930
},
3031
"peerDependencies": {
@@ -36,7 +37,10 @@
3637
"laravel-precognition-react": "0.7.3"
3738
},
3839
"devDependencies": {
40+
"@testing-library/react": "^16.3.0",
3941
"@types/react-dom": "^18.2.4 || ^19.0.0",
40-
"typescript": "^5.0.0"
42+
"jsdom": "^27.2.0",
43+
"typescript": "^5.0.0",
44+
"vitest": "^2.0.5"
4145
}
4246
}

packages/react-inertia/src/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export const useForm = <Data extends Record<string, FormDataConvertible>>(method
2121
*/
2222
const precognitiveForm = usePrecognitiveForm(method, url, inputs, config)
2323

24+
/**
25+
* The Inertia set defaults function.
26+
*/
27+
const inertiaSetDefaults = inertiaForm.setDefaults.bind(inertiaForm)
28+
2429
/**
2530
* The Inertia submit function.
2631
*/
@@ -103,6 +108,28 @@ export const useForm = <Data extends Record<string, FormDataConvertible>>(method
103108

104109
return form
105110
},
111+
setDefaults(field?: keyof Data | Partial<FormDataType<Data>> | ((previousData: FormDataType<Data>) => FormDataType<Data>), value?: Data[keyof Data]){
112+
const data = ((): Partial<FormDataType<Data>> => {
113+
if (typeof field === 'undefined') {
114+
return inertiaForm.data
115+
}
116+
117+
if (typeof field === 'function') {
118+
return field(inertiaForm.data)
119+
}
120+
121+
if (typeof field === 'object') {
122+
return field
123+
}
124+
125+
// @ts-ignore
126+
return { [field]: value }
127+
})()
128+
129+
inertiaSetDefaults(data)
130+
131+
precognitiveForm.validator().defaults(data)
132+
},
106133
reset(...names: FormDataKeys<FormDataType<Data>>[]) {
107134
inertiaReset(...names)
108135

packages/react-inertia/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export type Form<Data extends Record<string, FormDataConvertible>> = Omit<Precog
1717
withoutFileValidation(): Form<Data>,
1818
setData(data: Record<string, FormDataConvertible>): Form<Data>,
1919
validate(name?: (keyof Data | NamedInputEvent) | ValidationConfig, config?: ValidationConfig): Form<Data>,
20+
setDefaults(): void,
21+
setDefaults(defaults: Partial<Data>): void,
22+
setDefaults(defaults: (previousData: Data) => Data): void,
23+
setDefaults<K extends keyof Data>(field: K, value: Data[K]): void,
2024
}
2125

2226
// This type has been duplicated from @inertiajs/core to
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import { it, expect, beforeEach, afterEach, vi } from 'vitest'
2+
import { renderHook, act } from '@testing-library/react'
3+
import { useForm, client } from '../src/index'
4+
import axios from 'axios'
5+
import { Config } from 'laravel-precognition'
6+
7+
beforeEach(() => {
8+
vi.mock('axios')
9+
client.use(axios)
10+
})
11+
12+
afterEach(() => {
13+
vi.restoreAllMocks()
14+
})
15+
16+
it('can clear all errors via Inertia\'s clearErrors', () => {
17+
const { result: form } = renderHook(() => useForm('post', '/register', {
18+
name: '',
19+
}))
20+
21+
act(() => form.current.setErrors({
22+
name: 'xxxx',
23+
other: 'xxxx',
24+
}))
25+
26+
expect(form.current.errors).toEqual({
27+
name: 'xxxx',
28+
other: 'xxxx',
29+
})
30+
31+
act(() => form.current.clearErrors())
32+
33+
expect(form.current.errors).toEqual({})
34+
expect(form.current.validator().errors()).toEqual({})
35+
})
36+
37+
it('can clear specific errors via Inertia\'s clearErrors', () => {
38+
const { result: form } = renderHook(() => useForm('post', '/register', {
39+
name: '',
40+
}))
41+
42+
act(() => form.current.setErrors({
43+
name: 'xxxx',
44+
email: 'xxxx',
45+
other: 'xxxx',
46+
}))
47+
48+
expect(form.current.errors).toEqual({
49+
name: 'xxxx',
50+
email: 'xxxx',
51+
other: 'xxxx',
52+
})
53+
54+
act(() => form.current.clearErrors('name', 'email'))
55+
56+
expect(form.current.errors).toEqual({
57+
other: 'xxxx',
58+
})
59+
expect(form.current.validator().errors()).toEqual({
60+
other: ['xxxx'],
61+
})
62+
})
63+
64+
it('provides default data for validation requests', () => {
65+
const response = { headers: { precognition: 'true', 'precognition-success': 'true' }, status: 204, data: 'data' }
66+
67+
let config: Config
68+
axios.request.mockImplementation(async (c: Config) => {
69+
config = c
70+
71+
return response
72+
})
73+
74+
const { result: form } = renderHook(() => useForm('post', '/register', {
75+
emails: '',
76+
}))
77+
78+
act(() => form.current.setData('emails', '[email protected], [email protected]'))
79+
act(() => form.current.validate('emails'))
80+
81+
expect(config!.data.emails).toEqual('[email protected], [email protected]')
82+
expect(form.current.data.emails).toBe('[email protected], [email protected]')
83+
})
84+
85+
it('transforms data for validation requests', () => {
86+
const response = { headers: { precognition: 'true', 'precognition-success': 'true' }, status: 204, data: 'data' }
87+
88+
let config: Config
89+
axios.request.mockImplementation(async (c: Config) => {
90+
config = c
91+
92+
return response
93+
})
94+
95+
const { result: form } = renderHook(() => useForm('post', '/register', {
96+
emails: '',
97+
}))
98+
99+
act(() => form.current.transform((data) => ({
100+
emails: data.emails.split(',').map((email: string) => email.trim()),
101+
})))
102+
103+
act(() => form.current.setData('emails', '[email protected], [email protected]'))
104+
act(() => form.current.validate('emails'))
105+
106+
expect(config!.data.emails).toEqual(['[email protected]', '[email protected]'])
107+
expect(form.current.data.emails).toBe('[email protected], [email protected]')
108+
})
109+
110+
it('can set individual errors', function () {
111+
const { result: form } = renderHook(() => useForm('post', '/register', {
112+
name: '',
113+
}))
114+
115+
act(() => form.current.setError('name', 'The name is required.'))
116+
117+
expect(form.current.errors.name).toBe('The name is required.')
118+
})
119+
120+
it('can check that specific fields have been touched', () => {
121+
const { result: form } = renderHook(() => useForm('post', '/register', {
122+
name: '',
123+
email: '',
124+
}))
125+
126+
expect(form.current.touched('name')).toBe(false)
127+
expect(form.current.touched('email')).toBe(false)
128+
129+
act(() => form.current.touch('name'))
130+
131+
expect(form.current.touched('name')).toBe(true)
132+
expect(form.current.touched('email')).toBe(false)
133+
})
134+
135+
it('can check it any fields have been touched', () => {
136+
const { result: form } = renderHook(() => useForm('post', '/register', {
137+
name: '',
138+
email: '',
139+
}))
140+
141+
expect(form.current.touched()).toBe(false)
142+
143+
act(() => form.current.touch('name'))
144+
145+
expect(form.current.touched()).toBe(true)
146+
})
147+
148+
it('can set defaults with no arguments', () => {
149+
let requests = 0
150+
axios.request.mockImplementation(async () => {
151+
requests++
152+
})
153+
154+
const { result: form } = renderHook(() => useForm('post', '/register', {
155+
name: 'John',
156+
}))
157+
158+
act(() => form.current.setData('name', 'Jane'))
159+
act(() => form.current.setDefaults())
160+
161+
act(() => form.current.setData('name', 'John'))
162+
act(() => form.current.reset())
163+
expect(form.current.data.name).toBe('Jane')
164+
165+
act(() => form.current.validate('name'))
166+
expect(requests).toBe(0)
167+
})
168+
169+
it('can set defaults with an object', () => {
170+
let requests = 0
171+
axios.request.mockImplementation(async () => {
172+
requests++
173+
})
174+
175+
const { result: form } = renderHook(() => useForm('post', '/register', {
176+
name: 'John',
177+
}))
178+
179+
act(() => form.current.setDefaults({ name: 'Jane' }))
180+
181+
act(() => form.current.setData('name', 'John'))
182+
act(() => form.current.reset())
183+
expect(form.current.data.name).toBe('Jane')
184+
185+
act(() => form.current.validate('name'))
186+
expect(requests).toBe(0)
187+
})
188+
189+
it('can set defaults with a function', () => {
190+
let requests = 0
191+
axios.request.mockImplementation(async () => {
192+
requests++
193+
})
194+
195+
const { result: form } = renderHook(() => useForm('post', '/register', {
196+
name: 'John',
197+
}))
198+
199+
act(() => form.current.setDefaults((prevData: any) => {
200+
expect(prevData).toEqual({
201+
name: 'John',
202+
})
203+
204+
return {
205+
name: 'Jane',
206+
}
207+
}))
208+
209+
act(() => form.current.setData('name', 'John'))
210+
act(() => form.current.reset())
211+
expect(form.current.data.name).toBe('Jane')
212+
213+
act(() => form.current.validate('name'))
214+
expect(requests).toBe(0)
215+
})
216+
217+
it('can set defaults with a field and value', () => {
218+
let requests = 0
219+
axios.request.mockImplementation(async () => {
220+
requests++
221+
})
222+
223+
const { result: form } = renderHook(() => useForm('post', '/register', {
224+
name: 'John',
225+
}))
226+
227+
act(() => form.current.setDefaults('name', 'Jane'))
228+
229+
act(() => form.current.setData('name', 'John'))
230+
act(() => form.current.reset())
231+
expect(form.current.data.name).toBe('Jane')
232+
233+
act(() => form.current.validate('name'))
234+
expect(requests).toBe(0)
235+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from 'vitest/config'
2+
3+
export default defineConfig({
4+
test: {
5+
environment: 'jsdom',
6+
},
7+
})

0 commit comments

Comments
 (0)