Skip to content

Commit d0b8f9a

Browse files
authored
[0.3.x] Allow fields to be manually touched (#26)
* allow fields to be manually touched * Ensure old data and touched aren't updated till response is received * wip * wip * wip * wip * Allow validate without input
1 parent 9358771 commit d0b8f9a

File tree

12 files changed

+159
-81
lines changed

12 files changed

+159
-81
lines changed

packages/alpine/src/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,19 @@ export default function (Alpine: TAlpine) {
9292
touched(name) {
9393
return state.touched.includes(name)
9494
},
95+
touch(name) {
96+
validator.touch(name)
97+
98+
return form
99+
},
95100
validate(name) {
96-
name = resolveName(name)
101+
if (typeof name === 'undefined') {
102+
validator.validate()
103+
} else {
104+
name = resolveName(name)
97105

98-
validator.validate(name, get(form.data(), name))
106+
validator.validate(name, get(form.data(), name))
107+
}
99108

100109
return form
101110
},

packages/alpine/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ export interface Form<Data extends Record<string, unknown>> {
44
processing: boolean,
55
validating: boolean,
66
touched(name: string): boolean,
7+
touch(name: string|NamedInputEvent|Array<string>): Data&Form<Data>,
78
data(): Data,
89
errors: Record<string, string>,
910
hasErrors: boolean,
1011
valid(name: string): boolean,
1112
invalid(name: string): boolean,
12-
validate(name: string|NamedInputEvent): Data&Form<Data>,
13+
validate(name?: string|NamedInputEvent): Data&Form<Data>,
1314
setErrors(errors: SimpleValidationErrors|ValidationErrors): Data&Form<Data>
1415
forgetError(name: string|NamedInputEvent): Data&Form<Data>
1516
setValidationTimeout(duration: number): Data&Form<Data>,

packages/core/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type Config = AxiosRequestConfig&{
2525

2626
interface RevalidatePayload {
2727
data: Record<string, unknown>|null,
28+
touched: Array<string>,
2829
}
2930

3031
export type ValidationConfig = Config&{
@@ -49,7 +50,8 @@ export interface Client {
4950

5051
export interface Validator {
5152
touched(): Array<string>,
52-
validate(input: string|NamedInputEvent, value: unknown): Validator,
53+
validate(input?: string|NamedInputEvent, value?: unknown): Validator,
54+
touch(input: string|NamedInputEvent|Array<string>): Validator,
5355
validating(): boolean,
5456
valid(): Array<string>,
5557
errors(): ValidationErrors,

packages/core/src/validator.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
118118
*/
119119
let oldData = initialData
120120

121+
/**
122+
* The data currently being validated.
123+
*/
124+
let validatingData: null|Record<string, unknown> = null
125+
126+
/**
127+
* The old touched.
128+
*/
129+
let oldTouched: string[] = []
130+
131+
/**
132+
* The touched currently being validated.
133+
*/
134+
let validatingTouched: null|string[] = null
135+
121136
/**
122137
* Create a debounced validation callback.
123138
*/
@@ -167,9 +182,9 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
167182
: response
168183
},
169184
onBefore: () => {
170-
const beforeValidationResult = (config.onBeforeValidation ?? ((newRequest, oldRequest) => {
171-
return ! isequal(newRequest, oldRequest)
172-
}))({ data }, { data: oldData })
185+
const beforeValidationResult = (config.onBeforeValidation ?? ((previous, next) => {
186+
return ! isequal(previous, next)
187+
}))({ data, touched }, { data: oldData, touched: oldTouched })
173188

174189
if (beforeValidationResult === false) {
175190
return false
@@ -181,7 +196,9 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
181196
return false
182197
}
183198

184-
oldData = data
199+
validatingTouched = touched
200+
201+
validatingData = data
185202

186203
return true
187204
},
@@ -191,7 +208,13 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
191208
(config.onStart ?? (() => null))()
192209
},
193210
onFinish: () => {
194-
setValidating(false);
211+
setValidating(false)
212+
213+
oldTouched = validatingTouched!
214+
215+
oldData = validatingData!
216+
217+
validatingTouched = validatingData = null;
195218

196219
(config.onFinish ?? (() => null))()
197220
},
@@ -201,7 +224,13 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
201224
/**
202225
* Validate the given input.
203226
*/
204-
const validate = (name: string|NamedInputEvent, value: unknown) => {
227+
const validate = (name?: string|NamedInputEvent, value?: unknown) => {
228+
if (typeof name === 'undefined') {
229+
validator()
230+
231+
return
232+
}
233+
205234
if (isFile(value) && !validateFiles) {
206235
console.warn('Precognition file validation is not active. Call the "validateFiles" function on your form to enable it.')
207236

@@ -238,6 +267,15 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
238267

239268
return form
240269
},
270+
touch(input) {
271+
const inputs = Array.isArray(input)
272+
? input
273+
: [resolveName(input)]
274+
275+
setTouched([...touched, ...inputs])
276+
277+
return form
278+
},
241279
validating: () => validating,
242280
valid,
243281
errors: () => errors,

packages/core/tests/client.test.js

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { it, vi, expect, beforeEach, afterEach } from 'vitest'
22
import axios from 'axios'
33
import { client } from '../src/index'
4-
import { createValidator } from '../src/validator'
54

65
beforeEach(() => {
76
vi.mock('axios')
@@ -455,66 +454,6 @@ it('does not create an abort controller when a cancelToken is provided', async (
455454
expect(config.signal).toBeUndefined()
456455
})
457456

458-
it('revaldata when validate is called', async () => {
459-
expect.assertions(4)
460-
461-
let requests = 0
462-
axios.request.mockImplementation(() => {
463-
requests++
464-
465-
return Promise.resolve({ headers: { precognition: 'true' } })
466-
})
467-
let data
468-
const validator = createValidator((client) => client.post('/foo', data))
469-
470-
expect(requests).toBe(0)
471-
472-
data = { name: 'Tim' }
473-
validator.validate('name', 'Tim')
474-
expect(requests).toBe(1)
475-
vi.advanceTimersByTime(1500)
476-
477-
data = { name: 'Jess' }
478-
validator.validate('name', 'Jess')
479-
expect(requests).toBe(2)
480-
vi.advanceTimersByTime(1500)
481-
482-
data = { name: 'Taylor' }
483-
validator.validate('name', 'Taylor')
484-
expect(requests).toBe(3)
485-
vi.advanceTimersByTime(1500)
486-
})
487-
488-
it('does not revaldata when data is unchanged', async () => {
489-
expect.assertions(4)
490-
491-
let requests = 0
492-
axios.request.mockImplementation(() => {
493-
requests++
494-
495-
return Promise.resolve({ headers: { precognition: 'true' } })
496-
})
497-
let data = {}
498-
const validator = createValidator((client) => client.post('/foo', data))
499-
500-
expect(requests).toBe(0)
501-
502-
data = { first: true }
503-
validator.validate('name', true)
504-
expect(requests).toBe(1)
505-
vi.advanceTimersByTime(1500)
506-
507-
data = { first: true }
508-
validator.validate('name', true)
509-
expect(requests).toBe(1)
510-
vi.advanceTimersByTime(1500)
511-
512-
data = { second: true }
513-
validator.validate('name', true)
514-
expect(requests).toBe(2)
515-
vi.advanceTimersByTime(1500)
516-
})
517-
518457
it('overrides request method url with config url', async () => {
519458
expect.assertions(5)
520459

packages/core/tests/validator.test.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,19 @@ it('does not revalidate data when data is unchanged', async () => {
6060

6161
data = { first: true }
6262
validator.validate('name', true)
63+
await vi.runAllTimersAsync()
6364
expect(requests).toBe(1)
6465
vi.advanceTimersByTime(1500)
6566

6667
data = { first: true }
6768
validator.validate('name', true)
69+
await vi.runAllTimersAsync()
6870
expect(requests).toBe(1)
6971
vi.advanceTimersByTime(1500)
7072

7173
data = { second: true }
7274
validator.validate('name', true)
75+
await vi.runAllTimersAsync()
7376
expect(requests).toBe(2)
7477
vi.advanceTimersByTime(1500)
7578
})
@@ -377,3 +380,59 @@ it('does mark fields as validated on success status', async () => {
377380
expect(validator.valid()).toEqual(['app'])
378381
expect(onValidatedChangedCalledTimes).toEqual(1)
379382
})
383+
384+
it('can mark fields as touched', () => {
385+
const validator = createValidator((client) => client.post('/foo', data))
386+
387+
validator.touch('name')
388+
expect(validator.touched()).toEqual(['name'])
389+
390+
validator.touch(['foo', 'bar'])
391+
expect(validator.touched()).toEqual(['name', 'foo', 'bar'])
392+
})
393+
394+
it('revalidates when touched changes', async () => {
395+
expect.assertions(1)
396+
397+
let requests = 0
398+
let resolvers = []
399+
let promises = []
400+
let configs = []
401+
axios.request.mockImplementation((c) => {
402+
requests++
403+
configs.push(c)
404+
405+
const promise = new Promise(resolve => {
406+
resolvers.push(resolve)
407+
})
408+
409+
promises.push(promise)
410+
411+
return promise
412+
})
413+
let data = { version: '10' }
414+
const validator = createValidator((client) => client.post('/foo', data))
415+
416+
data = { app: 'Laravel' }
417+
validator.validate('app', 'Laravel')
418+
validator.touch('version')
419+
validator.validate('app', 'Laravel')
420+
vi.advanceTimersByTime(2000)
421+
expect(requests).toBe(2)
422+
})
423+
424+
it('can validate without needing to specify a field', async () => {
425+
expect.assertions(1)
426+
427+
let requests = 0
428+
axios.request.mockImplementation(() => {
429+
requests++
430+
431+
return Promise.resolve({ headers: { precognition: 'true' } })
432+
})
433+
let data = { name: 'Tim', framework: 'Laravel' }
434+
const validator = createValidator((client) => client.post('/foo', data))
435+
436+
validator.touch(['name', 'framework']).validate()
437+
expect(requests).toBe(1)
438+
})

packages/react-inertia/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ export const useForm = <Data extends Record<string, unknown>>(method: RequestMet
6565
const form = Object.assign(inertiaForm, {
6666
validating: precognitiveForm.validating,
6767
touched: precognitiveForm.touched,
68+
touch(name: Array<string>|string|NamedInputEvent) {
69+
precognitiveForm.touch(name)
70+
71+
return form
72+
},
6873
valid: precognitiveForm.valid,
6974
invalid: precognitiveForm.invalid,
7075
setData(key: any, value?: any) {
@@ -111,7 +116,7 @@ export const useForm = <Data extends Record<string, unknown>>(method: RequestMet
111116

112117
return form
113118
},
114-
validate(name: string|NamedInputEvent) {
119+
validate(name?: string|NamedInputEvent) {
115120
precognitiveForm.setData(inertiaForm.data)
116121

117122
precognitiveForm.validate(name)

packages/react/src/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,20 @@ export const useForm = <Data extends Record<string, unknown>>(method: RequestMet
138138
touched(name) {
139139
return touched.includes(name)
140140
},
141+
touch(name) {
142+
validator.current!.touch(name)
143+
144+
return form
145+
},
141146
validate(name) {
142-
// @ts-expect-error
143-
name = resolveName(name)
147+
if (typeof name === 'undefined') {
148+
validator.current!.validate()
149+
} else {
150+
// @ts-expect-error
151+
name = resolveName(name)
144152

145-
validator.current!.validate(name, get(payload.current, name))
153+
validator.current!.validate(name, get(payload.current, name))
154+
}
146155

147156
return form
148157
},

packages/react/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ export interface Form<Data extends Record<string, unknown>> {
44
processing: boolean,
55
validating: boolean,
66
touched(name: keyof Data): boolean,
7+
touch(name: string|NamedInputEvent|Array<string>): Form<Data>,
78
data: Data,
89
setData(key: Data|keyof Data, value?: unknown): Form<Data>,
910
errors: Partial<Record<keyof Data, string>>,
1011
hasErrors: boolean,
1112
valid(name: keyof Data): boolean,
1213
invalid(name: keyof Data): boolean,
13-
validate(name: keyof Data|NamedInputEvent): Form<Data>,
14+
validate(name?: keyof Data|NamedInputEvent): Form<Data>,
1415
setErrors(errors: Partial<Record<keyof Data, string|string[]>>): Form<Data>
1516
forgetError(string: keyof Data|NamedInputEvent): Form<Data>
1617
setValidationTimeout(duration: number): Form<Data>,

packages/vue-inertia/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ export const useForm = <Data extends Record<string, unknown>>(method: RequestMet
5353
const form = Object.assign(inertiaForm, {
5454
validating: precognitiveForm.validating,
5555
touched: precognitiveForm.touched,
56+
touch(name: Array<string>|string|NamedInputEvent) {
57+
precognitiveForm.touch(name)
58+
59+
return form
60+
},
5661
valid: precognitiveForm.valid,
5762
invalid: precognitiveForm.invalid,
5863
clearErrors(...names: string[]) {
@@ -92,7 +97,7 @@ export const useForm = <Data extends Record<string, unknown>>(method: RequestMet
9297

9398
return form
9499
},
95-
validate(name: string|NamedInputEvent) {
100+
validate(name?: string|NamedInputEvent) {
96101
precognitiveForm.setData(inertiaForm.data())
97102

98103
precognitiveForm.validate(name)

0 commit comments

Comments
 (0)