Skip to content

Commit af7b35f

Browse files
authored
Async (#69)
1 parent fbf80b0 commit af7b35f

File tree

19 files changed

+377
-125
lines changed

19 files changed

+377
-125
lines changed

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
"watch": "npm run watch --workspace=packages/core& npm run watch --workspace=packages/react& npm run watch --workspace=packages/react-inertia& npm run watch --workspace=packages/vue& npm run watch --workspace=packages/vue-inertia& npm run watch --workspace=packages/alpine& wait;",
1313
"build": "npm run build --workspaces",
1414
"link": "npm link --workspaces",
15-
"lint": "eslint --ext .ts --ignore-pattern dist ./packages",
16-
"lint:fix": "eslint --fix --ext .ts --ignore-pattern dist ./packages",
15+
"typeCheck": "npm run typeCheck --workspaces",
16+
"lint": "eslint --ignore-pattern /packages/**/dist/** ./packages/**",
17+
"lint:fix": "eslint --fix --ignore-pattern /packages/**/dist/** ./packages/**",
1718
"test": "npm run test --workspaces --if-present"
1819
},
1920
"devDependencies": {
20-
"@typescript-eslint/eslint-plugin": "^5.21.0",
21-
"@typescript-eslint/parser": "^5.21.0",
22-
"eslint": "^8.14.0"
21+
"@typescript-eslint/eslint-plugin": "^7.10.0",
22+
"@typescript-eslint/parser": "^7.10.0",
23+
"eslint": "^8.56.0"
2324
}
2425
}

packages/alpine/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"scripts": {
2323
"watch": "rm -rf dist && tsc --watch",
2424
"build": "rm -rf dist && tsc",
25+
"typeCheck": "tsc --noEmit",
2526
"prepublishOnly": "npm run build",
2627
"version": "npm pkg set dependencies.laravel-precognition=$npm_package_version"
2728
},

packages/alpine/src/index.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,18 @@ export default function (Alpine: TAlpine) {
9595

9696
return form
9797
},
98-
validate(name) {
98+
validate(name, config) {
99+
if (typeof name === 'object' && !('target' in name)) {
100+
config = name
101+
name = undefined
102+
}
103+
99104
if (typeof name === 'undefined') {
100-
validator.validate()
105+
validator.validate(config)
101106
} else {
102107
name = resolveName(name)
103108

104-
validator.validate(name, get(form.data(), name))
109+
validator.validate(name, get(form.data(), name), config)
105110
}
106111

107112
return form

packages/alpine/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Config, NamedInputEvent, SimpleValidationErrors, ValidationErrors } from 'laravel-precognition'
1+
import { Config, NamedInputEvent, SimpleValidationErrors, ValidationConfig, ValidationErrors } from 'laravel-precognition'
22

33
export interface Form<Data extends Record<string, unknown>> {
44
processing: boolean,
@@ -10,7 +10,7 @@ export interface Form<Data extends Record<string, unknown>> {
1010
hasErrors: boolean,
1111
valid(name: string): boolean,
1212
invalid(name: string): boolean,
13-
validate(name?: string|NamedInputEvent): Data&Form<Data>,
13+
validate(name?: string|NamedInputEvent|ValidationConfig, config?: ValidationConfig): Data&Form<Data>,
1414
setErrors(errors: SimpleValidationErrors|ValidationErrors): Data&Form<Data>
1515
forgetError(name: string|NamedInputEvent): Data&Form<Data>
1616
setValidationTimeout(duration: number): Data&Form<Data>,

packages/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"scripts": {
2323
"watch": "rm -rf dist && tsc --watch",
2424
"build": "rm -rf dist && tsc",
25+
"typeCheck": "tsc --noEmit",
2526
"prepublishOnly": "npm run build",
2627
"test": "vitest run"
2728
},
@@ -36,6 +37,6 @@
3637
"@types/lodash-es": "^4.17.12",
3738
"@types/node": "^20.1.0",
3839
"typescript": "^5.0.0",
39-
"vitest": "^0.31.3"
40+
"vitest": "^1.6.0"
4041
}
4142
}

packages/core/src/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export interface Client {
5050

5151
export interface Validator {
5252
touched(): Array<string>,
53-
validate(input?: string|NamedInputEvent, value?: unknown): Validator,
53+
validate(input?: string|NamedInputEvent|ValidationConfig, value?: unknown, config?: ValidationConfig): Validator,
5454
touch(input: string|NamedInputEvent|Array<string>): Validator,
5555
validating(): boolean,
5656
valid(): Array<string>,
@@ -88,3 +88,7 @@ interface NamedEventTarget extends EventTarget {
8888
export interface NamedInputEvent extends InputEvent {
8989
readonly target: NamedEventTarget;
9090
}
91+
92+
declare module 'axios' {
93+
export function mergeConfig(config1: AxiosRequestConfig, config2: AxiosRequestConfig): AxiosRequestConfig
94+
}

packages/core/src/validator.ts

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { debounce, isEqual, get, set, omit, merge } from 'lodash-es'
22
import { ValidationCallback, Config, NamedInputEvent, SimpleValidationErrors, ValidationErrors, Validator as TValidator, ValidatorListeners, ValidationConfig } from './types.js'
33
import { client, isFile } from './client.js'
4-
import { isAxiosError } from 'axios'
4+
import { isAxiosError, isCancel, mergeConfig } from 'axios'
55

66
export const createValidator = (callback: ValidationCallback, initialData: Record<string, unknown> = {}): TValidator => {
77
/**
@@ -169,15 +169,33 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
169169
/**
170170
* Create a debounced validation callback.
171171
*/
172-
const createValidator = () => debounce(() => {
172+
const createValidator = () => debounce((instanceConfig: Config) => {
173173
callback({
174-
get: (url, data = {}, config = {}) => client.get(url, parseData(data), resolveConfig(config, data)),
175-
post: (url, data = {}, config = {}) => client.post(url, parseData(data), resolveConfig(config, data)),
176-
patch: (url, data = {}, config = {}) => client.patch(url, parseData(data), resolveConfig(config, data)),
177-
put: (url, data = {}, config = {}) => client.put(url, parseData(data), resolveConfig(config, data)),
178-
delete: (url, data = {}, config = {}) => client.delete(url, parseData(data), resolveConfig(config, data)),
174+
get: (url, data = {}, globalConfig = {}) => client.get(url, parseData(data), resolveConfig(globalConfig, instanceConfig, data)),
175+
post: (url, data = {}, globalConfig = {}) => client.post(url, parseData(data), resolveConfig(globalConfig, instanceConfig, data)),
176+
patch: (url, data = {}, globalConfig = {}) => client.patch(url, parseData(data), resolveConfig(globalConfig, instanceConfig, data)),
177+
put: (url, data = {}, globalConfig = {}) => client.put(url, parseData(data), resolveConfig(globalConfig, instanceConfig, data)),
178+
delete: (url, data = {}, globalConfig = {}) => client.delete(url, parseData(data), resolveConfig(globalConfig, instanceConfig, data)),
179+
}).catch((error) => {
180+
// Precognition can often cancel in-flight requests. Instead of
181+
// throwing an exception for this expected behaviour, we silently
182+
// discard cancelled request errors to not flood the console with
183+
// expected errors.
184+
if (isCancel(error)) {
185+
return null
186+
}
187+
188+
// Unlike other status codes, 422 responses are expected and
189+
// regularly occur with Precognition requests. We silently ignore
190+
// these so we do not flood the console with expected errors. If
191+
// needed, they can be intercepted by the `onValidationError`
192+
// config option instead.
193+
if (isAxiosError(error) && error.response?.status === 422) {
194+
return null
195+
}
196+
197+
return Promise.reject(error)
179198
})
180-
.catch(error => isAxiosError(error) ? null : Promise.reject(error))
181199
}, debounceTimeoutDuration, { leading: true, trailing: true })
182200

183201
/**
@@ -188,11 +206,24 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
188206
/**
189207
* Resolve the configuration.
190208
*/
191-
const resolveConfig = (config: ValidationConfig, data: Record<string, unknown> = {}): Config => {
209+
const resolveConfig = (
210+
globalConfig: ValidationConfig,
211+
instanceConfig: ValidationConfig,
212+
data: Record<string, unknown> = {}
213+
): Config => {
214+
const config: ValidationConfig = {
215+
...globalConfig,
216+
...instanceConfig,
217+
}
218+
192219
const validate = Array.from(config.validate ?? touched)
193220

194221
return {
195-
...config,
222+
...instanceConfig,
223+
// Axios has special rules for merging global and local config. We
224+
// use their merge function here to make sure things like headers
225+
// merge in an expected way.
226+
...mergeConfig(globalConfig, instanceConfig),
196227
validate,
197228
timeout: config.timeout ?? 5000,
198229
onValidationError: (response, axiosError) => {
@@ -205,8 +236,12 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
205236
? config.onValidationError(response, axiosError)
206237
: Promise.reject(axiosError)
207238
},
208-
onSuccess: () => {
239+
onSuccess: (response) => {
209240
setValidated([...validated, ...validate]).forEach(listener => listener())
241+
242+
return config.onSuccess
243+
? config.onSuccess(response)
244+
: response
210245
},
211246
onPrecognitionSuccess: (response) => {
212247
[
@@ -219,11 +254,11 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
219254
: response
220255
},
221256
onBefore: () => {
222-
const beforeValidationResult = (config.onBeforeValidation ?? ((previous, next) => {
223-
return ! isEqual(previous, next)
224-
}))({ data, touched }, { data: oldData, touched: oldTouched })
257+
const beforeValidationHandler = config.onBeforeValidation ?? ((newRequest, oldRequest) => {
258+
return newRequest.touched.length > 0 && ! isEqual(newRequest, oldRequest)
259+
})
225260

226-
if (beforeValidationResult === false) {
261+
if (beforeValidationHandler({ data, touched }, { data: oldData, touched: oldTouched }) === false) {
227262
return false
228263
}
229264

@@ -261,9 +296,9 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
261296
/**
262297
* Validate the given input.
263298
*/
264-
const validate = (name?: string|NamedInputEvent, value?: unknown) => {
299+
const validate = (name?: string|NamedInputEvent, value?: unknown, config?: Config): void => {
265300
if (typeof name === 'undefined') {
266-
validator()
301+
validator(config ?? {})
267302

268303
return
269304
}
@@ -280,11 +315,7 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
280315
setTouched([name, ...touched]).forEach(listener => listener())
281316
}
282317

283-
if (touched.length === 0) {
284-
return
285-
}
286-
287-
validator()
318+
validator(config ?? {})
288319
}
289320

290321
/**
@@ -299,8 +330,13 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
299330
*/
300331
const form: TValidator = {
301332
touched: () => touched,
302-
validate(input, value) {
303-
validate(input, value)
333+
validate(name, value, config) {
334+
if (typeof name === 'object' && ! ('target' in name)) {
335+
config = name
336+
name = value = undefined
337+
}
338+
339+
validate(name, value, config)
304340

305341
return form
306342
},

packages/core/tests/client.test.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ it('does not consider 204 response to be success without "Precognition-Success"
218218
},
219219
onSuccess() {
220220
responseSuccess = true
221-
}
221+
},
222222
})
223223

224224
expect(precognitionSucess).toBe(false)
@@ -229,7 +229,7 @@ it('throws an error if the precognition header is not present on an error respon
229229
expect.assertions(2)
230230

231231
axios.request.mockRejectedValueOnce({ response: { status: 500 } })
232-
axios.isAxiosError.mockReturnValue(true)
232+
axios.isAxiosError.mockReturnValueOnce(true)
233233

234234
await client.get('https://laravel.com').catch((e) => {
235235
expect(e).toBeInstanceOf(Error)
@@ -249,7 +249,7 @@ it('returns a non-axios error via a rejected promise', async () => {
249249
})
250250
})
251251

252-
it('returns a canceled request error va rejected promise', async () => {
252+
it('returns a cancelled request error va rejected promise', async () => {
253253
expect.assertions(1)
254254

255255
const error = { expected: 'error' }
@@ -519,29 +519,29 @@ it('overrides the request data with the config data', async () => {
519519
})
520520

521521
await client.get('https://laravel.com', { expected: false }, {
522-
data: { expected: true }
522+
data: { expected: true },
523523
})
524-
expect(config.data).toEqual({ expected: true})
524+
expect(config.data).toEqual({ expected: true })
525525

526526
await client.post('https://laravel.com', { expected: false }, {
527-
data: { expected: true }
527+
data: { expected: true },
528528
})
529-
expect(config.data).toEqual({ expected: true})
529+
expect(config.data).toEqual({ expected: true })
530530

531531
await client.patch('https://laravel.com', { expected: false }, {
532-
data: { expected: true }
532+
data: { expected: true },
533533
})
534-
expect(config.data).toEqual({ expected: true})
534+
expect(config.data).toEqual({ expected: true })
535535

536536
await client.put('https://laravel.com', { expected: false }, {
537-
data: { expected: true }
537+
data: { expected: true },
538538
})
539-
expect(config.data).toEqual({ expected: true})
539+
expect(config.data).toEqual({ expected: true })
540540

541541
await client.delete('https://laravel.com', { expected: false }, {
542-
data: { expected: true }
542+
data: { expected: true },
543543
})
544-
expect(config.data).toEqual({ expected: true})
544+
expect(config.data).toEqual({ expected: true })
545545
})
546546

547547
it('merges request data with config data', async () => {
@@ -554,28 +554,28 @@ it('merges request data with config data', async () => {
554554
})
555555

556556
await client.get('https://laravel.com', { request: true }, {
557-
data: { config: true }
557+
data: { config: true },
558558
})
559559
expect(config.data).toEqual({ config: true })
560560
expect(config.params).toEqual({ request: true })
561561

562562
await client.post('https://laravel.com', { request: true }, {
563-
data: { config: true }
563+
data: { config: true },
564564
})
565565
expect(config.data).toEqual({ request: true, config: true })
566566

567567
await client.patch('https://laravel.com', { request: true }, {
568-
data: { config: true }
568+
data: { config: true },
569569
})
570570
expect(config.data).toEqual({ request: true, config: true })
571571

572572
await client.put('https://laravel.com', { request: true }, {
573-
data: { config: true }
573+
data: { config: true },
574574
})
575575
expect(config.data).toEqual({ request: true, config: true })
576576

577577
await client.delete('https://laravel.com', { request: true }, {
578-
data: { config: true }
578+
data: { config: true },
579579
})
580580
expect(config.data).toEqual({ config: true })
581581
expect(config.params).toEqual({ request: true })
@@ -591,13 +591,13 @@ it('merges request data with config params for get and delete requests', async (
591591
})
592592

593593
await client.get('https://laravel.com', { data: true }, {
594-
params: { param: true }
594+
params: { param: true },
595595
})
596596
expect(config.params).toEqual({ data: true, param: true })
597597
expect(config.data).toBeUndefined()
598598

599599
await client.delete('https://laravel.com', { data: true }, {
600-
params: { param: true }
600+
params: { param: true },
601601
})
602602
expect(config.params).toEqual({ data: true, param: true })
603603
expect(config.data).toBeUndefined()

0 commit comments

Comments
 (0)