Skip to content

Commit def7302

Browse files
authored
[0.1.x] File improvements (#4)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip
1 parent 5f997d7 commit def7302

File tree

13 files changed

+231
-91
lines changed

13 files changed

+231
-91
lines changed

packages/alpine/src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,11 @@ export default function (Alpine: TAlpine) {
8484
const createForm = (): Data&Form<Data> => ({
8585
...cloneDeep(inputs),
8686
data() {
87+
const newForm = cloneDeep(form)
88+
8789
return originalInputs.reduce((carry, name) => ({
8890
...carry,
89-
[name]: cloneDeep(form[name]),
91+
[name]: newForm[name],
9092
}), {}) as Data
9193
},
9294
touched(name) {
@@ -113,6 +115,11 @@ export default function (Alpine: TAlpine) {
113115

114116
return form
115117
},
118+
forgetError(name) {
119+
validator.forgetError(name)
120+
121+
return form
122+
},
116123
reset(...names) {
117124
const original = cloneDeep(originalData)
118125

@@ -136,6 +143,11 @@ export default function (Alpine: TAlpine) {
136143
async submit(config = {}) {
137144
return client[method](url, form.data(), resolveSubmitConfig(config))
138145
},
146+
validateFiles() {
147+
validator.validateFiles()
148+
149+
return form
150+
},
139151
})
140152

141153
/**

packages/alpine/src/types.ts

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

33
export interface Form<Data extends Record<string, unknown>> {
44
processing: boolean,
@@ -11,7 +11,9 @@ export interface Form<Data extends Record<string, unknown>> {
1111
invalid(name: string): boolean,
1212
validate(name: string|NamedInputEvent): Data&Form<Data>,
1313
setErrors(errors: SimpleValidationErrors|ValidationErrors): Data&Form<Data>
14+
forgetError(name: string|NamedInputEvent): Data&Form<Data>
1415
setValidationTimeout(duration: number): Data&Form<Data>,
1516
submit(config?: Config): Promise<unknown>,
1617
reset(...keys: string[]): Data&Form<Data>,
18+
validateFiles(): Data&Form<Data>,
1719
}

packages/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"lodash.get": "^4.4.2",
3434
"lodash.isequal": "^4.0.8",
3535
"lodash.merge": "^4.6.2",
36+
"lodash.omit": "^4.5.0",
3637
"lodash.set": "^4.3.2"
3738
},
3839
"devDependencies": {
@@ -44,6 +45,7 @@
4445
"@types/lodash.get": "^4.4.7",
4546
"@types/lodash.isequal": "^4.0.7",
4647
"@types/lodash.merge": "^4.0.7",
48+
"@types/lodash.omit": "^4.5.7",
4749
"@types/lodash.set": "^4.3.7",
4850
"@types/node": "^20.1.0",
4951
"babel-jest": "^29.5.0",

packages/core/src/client.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ const resolveConfig = (config: Config): Config => ({
107107
: config.fingerprint,
108108
headers: {
109109
...config.headers,
110+
'Content-Type': resolveContentType(config),
110111
...config.precognitive !== false ? {
111112
Precognition: true,
112113
} : {},
@@ -178,3 +179,26 @@ const resolveStatusHandler = (config: Config, code: number): StatusHandler|undef
178179
422: config.onValidationError,
179180
423: config.onLocked,
180181
}[code])
182+
183+
/**
184+
* Resolve the request's "Content-Type" header.
185+
*/
186+
const resolveContentType = (config: Config): string => config.headers?.['Content-Type']
187+
?? config.headers?.['Content-type']
188+
?? config.headers?.['content-type']
189+
?? (hasFiles(config.data) ? 'multipart/form-data' : 'application/json')
190+
191+
/**
192+
* Determine if the payload has any files.
193+
*
194+
* @see https://github.com/inertiajs/inertia/blob/master/packages/core/src/files.ts
195+
*/
196+
const hasFiles = (data: unknown): boolean => isFile(data)
197+
|| (typeof data === 'object' && data !== null && Object.values(data).some((value) => hasFiles(value)))
198+
199+
/**
200+
* Determine if the value is a file.
201+
*/
202+
export const isFile = (value: unknown): boolean => (typeof File !== 'undefined' && value instanceof File)
203+
|| value instanceof Blob
204+
|| (typeof FileList !== 'undefined' && value instanceof FileList && value.length > 0)

packages/core/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ export interface Validator {
5353
errors(): ValidationErrors,
5454
setErrors(errors: ValidationErrors|SimpleValidationErrors): Validator,
5555
hasErrors(): boolean,
56+
forgetError(error: string|NamedInputEvent): Validator,
5657
reset(...names: string[]): Validator,
5758
setTimeout(duration: number): Validator,
5859
on(event: keyof ValidatorListeners, callback: () => void): Validator,
60+
validateFiles(): Validator,
5961
}
6062

6163
export interface ValidatorListeners {

packages/core/src/validator.ts

Lines changed: 139 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import isequal from 'lodash.isequal'
33
import get from 'lodash.get'
44
import set from 'lodash.set'
55
import { ValidationCallback, Config, NamedInputEvent, SimpleValidationErrors, ValidationErrors, Validator as TValidator, ValidatorListeners, ValidationConfig } from './types'
6-
import { client } from './client'
6+
import { client, isFile } from './client'
77
import { isAxiosError } from 'axios'
8+
import omit from 'lodash.omit'
9+
import merge from 'lodash.merge'
810

911
export const createValidator = (callback: ValidationCallback, initialData: Record<string, unknown> = {}): TValidator => {
1012
/**
@@ -16,6 +18,11 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
1618
validatingChanged: [],
1719
}
1820

21+
/**
22+
* Validate files state.
23+
*/
24+
let validateFiles = false
25+
1926
/**
2027
* Processing validation state.
2128
*/
@@ -57,8 +64,14 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
5764

5865
listeners.errorsChanged.forEach(callback => callback())
5966
}
67+
}
6068

61-
setTouched([...touched, ...Object.keys(errors)])
69+
const forgetError = (name: string|NamedInputEvent) => {
70+
const newErrors = { ...errors }
71+
72+
delete newErrors[resolveName(name)]
73+
74+
setErrors(newErrors)
6275
}
6376

6477
/**
@@ -96,11 +109,11 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
96109
*/
97110
const createValidator = () => debounce(() => {
98111
callback({
99-
get: (url, data = {}, config = {}) => client.get(url, data, resolveConfig(config, data)),
100-
post: (url, data = {}, config = {}) => client.post(url, data, resolveConfig(config, data)),
101-
patch: (url, data = {}, config = {}) => client.patch(url, data, resolveConfig(config, data)),
102-
put: (url, data = {}, config = {}) => client.put(url, data, resolveConfig(config, data)),
103-
delete: (url, data = {}, config = {}) => client.delete(url, data, resolveConfig(config, data)),
112+
get: (url, data = {}, config = {}) => client.get(url, parseData(data), resolveConfig(config, data)),
113+
post: (url, data = {}, config = {}) => client.post(url, parseData(data), resolveConfig(config, data)),
114+
patch: (url, data = {}, config = {}) => client.patch(url, parseData(data), resolveConfig(config, data)),
115+
put: (url, data = {}, config = {}) => client.put(url, parseData(data), resolveConfig(config, data)),
116+
delete: (url, data = {}, config = {}) => client.delete(url, parseData(data), resolveConfig(config, data)),
104117
})
105118
.catch(error => isAxiosError(error) ? null : Promise.reject(error))
106119
}, debounceTimeoutDuration, { leading: true, trailing: true })
@@ -113,61 +126,69 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
113126
/**
114127
* Resolve the configuration.
115128
*/
116-
const resolveConfig = (config: ValidationConfig, data: Record<string, unknown> = {}): Config => ({
117-
...config,
118-
timeout: config.timeout ?? 5000,
119-
validate: config.validate
120-
? config.validate
121-
: touched,
122-
onValidationError: (response, axiosError) => {
123-
setErrors(response.data.errors)
124-
125-
return config.onValidationError
126-
? config.onValidationError(response, axiosError)
127-
: Promise.reject(axiosError)
128-
},
129-
onPrecognitionSuccess: (response) => {
130-
setErrors({})
131-
132-
return config.onPrecognitionSuccess
133-
? config.onPrecognitionSuccess(response)
134-
: response
135-
},
136-
onBefore: () => {
137-
const beforeValidationResult = (config.onBeforeValidation ?? ((newRequest, oldRequest) => {
138-
return ! isequal(newRequest, oldRequest)
139-
}))({ data }, { data: oldData })
140-
141-
if (beforeValidationResult === false) {
142-
return false
143-
}
144-
145-
const beforeResult = (config.onBefore || (() => true))()
146-
147-
if (beforeResult === false) {
148-
return false
149-
}
150-
151-
oldData = data
152-
153-
return true
154-
},
155-
onStart: () => {
156-
setValidating(true);
157-
158-
(config.onStart ?? (() => null))()
159-
},
160-
onFinish: () => {
161-
setValidating(false);
162-
163-
(config.onFinish ?? (() => null))()
164-
},
165-
})
129+
const resolveConfig = (config: ValidationConfig, data: Record<string, unknown> = {}): Config => {
130+
const validate = Array.from(config.validate ?? touched)
131+
132+
return {
133+
...config,
134+
validate,
135+
timeout: config.timeout ?? 5000,
136+
onValidationError: (response, axiosError) => {
137+
setErrors(merge(omit({ ...errors }, validate), response.data.errors))
138+
139+
return config.onValidationError
140+
? config.onValidationError(response, axiosError)
141+
: Promise.reject(axiosError)
142+
},
143+
onPrecognitionSuccess: (response) => {
144+
setErrors(omit({ ...errors }, validate))
145+
146+
return config.onPrecognitionSuccess
147+
? config.onPrecognitionSuccess(response)
148+
: response
149+
},
150+
onBefore: () => {
151+
const beforeValidationResult = (config.onBeforeValidation ?? ((newRequest, oldRequest) => {
152+
return ! isequal(newRequest, oldRequest)
153+
}))({ data }, { data: oldData })
154+
155+
if (beforeValidationResult === false) {
156+
return false
157+
}
158+
159+
const beforeResult = (config.onBefore || (() => true))()
160+
161+
if (beforeResult === false) {
162+
return false
163+
}
164+
165+
oldData = data
166+
167+
return true
168+
},
169+
onStart: () => {
170+
setValidating(true);
171+
172+
(config.onStart ?? (() => null))()
173+
},
174+
onFinish: () => {
175+
setValidating(false);
176+
177+
(config.onFinish ?? (() => null))()
178+
},
179+
}
180+
}
166181

167182
/**
168183
* Validate the given input.
169184
*/
170185
const validate = (name: string|NamedInputEvent, value: unknown) => {
186+
if (isFile(value) && !validateFiles) {
187+
console.warn('Precognition file validation is not active. Call the "validateFiles" function on your form to enable it.')
188+
189+
return
190+
}
191+
171192
name = resolveName(name)
172193

173194
if (get(oldData, name) !== value) {
@@ -181,6 +202,13 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
181202
validator()
182203
}
183204

205+
/**
206+
* Parse the validated data.
207+
*/
208+
const parseData = (data: Record<string, unknown>): Record<string, unknown> => validateFiles === false
209+
? forgetFiles(data)
210+
: data
211+
184212
/**
185213
* The form validator instance.
186214
*/
@@ -200,6 +228,11 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
200228

201229
return this
202230
},
231+
forgetError(name) {
232+
forgetError(name)
233+
234+
return this
235+
},
203236
reset(...names) {
204237
if (names.length === 0) {
205238
setTouched([])
@@ -227,11 +260,19 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
227260
on(event, callback) {
228261
listeners[event].push(callback)
229262

263+
return this
264+
},
265+
validateFiles() {
266+
validateFiles = true
267+
230268
return this
231269
},
232270
}
233271
}
234272

273+
/**
274+
* Normalise the validation errors as Inertia formatted errors.
275+
*/
235276
export const toSimpleValidationErrors = (errors: ValidationErrors|SimpleValidationErrors): SimpleValidationErrors => {
236277
return Object.keys(errors).reduce((carry, key) => ({
237278
...carry,
@@ -241,16 +282,57 @@ export const toSimpleValidationErrors = (errors: ValidationErrors|SimpleValidati
241282
}), {})
242283
}
243284

285+
/**
286+
* Normalise the validation errors as Laravel formatted errors.
287+
*/
244288
export const toValidationErrors = (errors: ValidationErrors|SimpleValidationErrors): ValidationErrors => {
245289
return Object.keys(errors).reduce((carry, key) => ({
246290
...carry,
247291
[key]: typeof errors[key] === 'string' ? [errors[key]] : errors[key],
248292
}), {})
249293
}
250294

295+
/**
296+
* Resolve the input's "name" attribute.
297+
*/
251298
export const resolveName = (name: string|NamedInputEvent): string => {
252299
return typeof name !== 'string'
253300
? name.target.name
254301
: name
255302
}
256303

304+
/**
305+
* Forget any files from the payload.
306+
*/
307+
const forgetFiles = (data: Record<string, unknown>): Record<string, unknown> => {
308+
const newData = { ...data }
309+
310+
Object.keys(newData).forEach(name => {
311+
const value = newData[name]
312+
313+
if (value === null) {
314+
return
315+
}
316+
317+
if (isFile(value)) {
318+
delete newData[name]
319+
320+
return
321+
}
322+
323+
if (Array.isArray(value)) {
324+
newData[name] = value.filter(isFile)
325+
326+
return
327+
}
328+
329+
if (typeof value === 'object') {
330+
// @ts-expect-error
331+
newData[name] = forgetFiles(newData[name])
332+
333+
return
334+
}
335+
})
336+
337+
return newData
338+
}

0 commit comments

Comments
 (0)