Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface Validator {
on(event: keyof ValidatorListeners, callback: () => void): Validator,
validateFiles(): Validator,
withoutFileValidation(): Validator,
defaults(data: Record<string, unknown>): Validator,
}

export interface ValidatorListeners {
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor

return form
},
defaults(data) {
initialData = data
oldData = data

return form
},
reset(...names) {
if (names.length === 0) {
setTouched([]).forEach((listener) => listener())
Expand Down
45 changes: 45 additions & 0 deletions packages/core/tests/validator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -803,3 +803,48 @@ it('marks fields as touched when the input has been included in validation', asy

await assertPendingValidateDebounceAndClear()
})

it('can override the old data via the defaults function', () => {
let requests = 0
axios.request.mockImplementation(() => {
requests++

return Promise.resolve(precognitionSuccessResponse())
})

const validator = createValidator((client) => client.post('/foo', {}), {
name: 'Tim',
})

expect(validator.defaults({
name: 'Jess',
})).toBe(validator)

validator.validate('name', 'Jess')
expect(requests).toBe(0)
})

it('can override the initial data via the defaults function', async () => {
expect.assertions(2)
let requests = 0
axios.request.mockImplementation(() => {
requests++

return Promise.resolve(precognitionSuccessResponse())
})

const validator = createValidator((client) => client.post('/foo', {}), {
name: 'Tim',
}).defaults({
name: 'Jess',
})

validator.validate('name', 'Taylor')
expect(requests).toBe(1)

await vi.advanceTimersByTimeAsync(1500)

validator.reset('name')
validator.validate('name', 'Jess')
expect(requests).toBe(1)
})
6 changes: 5 additions & 1 deletion packages/react-inertia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"build": "rm -rf dist && tsc",
"typeCheck": "tsc --noEmit",
"prepublishOnly": "npm run build",
"test": "vitest run",
"version": "npm pkg set dependencies.laravel-precognition=$npm_package_version && npm pkg set dependencies.laravel-precognition-react=$npm_package_version"
},
"peerDependencies": {
Expand All @@ -36,7 +37,10 @@
"laravel-precognition-react": "0.7.3"
},
"devDependencies": {
"@testing-library/react": "^16.3.0",
"@types/react-dom": "^18.2.4 || ^19.0.0",
"typescript": "^5.0.0"
"jsdom": "^27.2.0",
"typescript": "^5.0.0",
"vitest": "^2.0.5"
}
}
27 changes: 27 additions & 0 deletions packages/react-inertia/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export const useForm = <Data extends Record<string, FormDataConvertible>>(method
*/
const precognitiveForm = usePrecognitiveForm(method, url, inputs, config)

/**
* The Inertia set defaults function.
*/
const inertiaSetDefaults = inertiaForm.setDefaults.bind(inertiaForm)

/**
* The Inertia submit function.
*/
Expand Down Expand Up @@ -103,6 +108,28 @@ export const useForm = <Data extends Record<string, FormDataConvertible>>(method

return form
},
setDefaults(field?: keyof Data | Partial<FormDataType<Data>> | ((previousData: FormDataType<Data>) => FormDataType<Data>), value?: Data[keyof Data]){
const data = ((): Partial<FormDataType<Data>> => {
if (typeof field === 'undefined') {
return inertiaForm.data
}

if (typeof field === 'function') {
return field(inertiaForm.data)
}

if (typeof field === 'object') {
return field
}

// @ts-ignore
return { [field]: value }
})()

inertiaSetDefaults(data)

precognitiveForm.validator().defaults(data)
},
reset(...names: FormDataKeys<FormDataType<Data>>[]) {
inertiaReset(...names)

Expand Down
4 changes: 4 additions & 0 deletions packages/react-inertia/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export type Form<Data extends Record<string, FormDataConvertible>> = Omit<Precog
withoutFileValidation(): Form<Data>,
setData(data: Record<string, FormDataConvertible>): Form<Data>,
validate(name?: (keyof Data | NamedInputEvent) | ValidationConfig, config?: ValidationConfig): Form<Data>,
setDefaults(): void,
setDefaults(defaults: Partial<Data>): void,
setDefaults(defaults: (previousData: Data) => Data): void,
setDefaults<K extends keyof Data>(field: K, value: Data[K]): void,
}

// This type has been duplicated from @inertiajs/core to
Expand Down
235 changes: 235 additions & 0 deletions packages/react-inertia/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { it, expect, beforeEach, afterEach, vi } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useForm, client } from '../src/index'
import axios from 'axios'
import { Config } from 'laravel-precognition'

beforeEach(() => {
vi.mock('axios')
client.use(axios)
})

afterEach(() => {
vi.restoreAllMocks()
})

it('can clear all errors via Inertia\'s clearErrors', () => {
const { result: form } = renderHook(() => useForm('post', '/register', {
name: '',
}))

act(() => form.current.setErrors({
name: 'xxxx',
other: 'xxxx',
}))

expect(form.current.errors).toEqual({
name: 'xxxx',
other: 'xxxx',
})

act(() => form.current.clearErrors())

expect(form.current.errors).toEqual({})
expect(form.current.validator().errors()).toEqual({})
})

it('can clear specific errors via Inertia\'s clearErrors', () => {
const { result: form } = renderHook(() => useForm('post', '/register', {
name: '',
}))

act(() => form.current.setErrors({
name: 'xxxx',
email: 'xxxx',
other: 'xxxx',
}))

expect(form.current.errors).toEqual({
name: 'xxxx',
email: 'xxxx',
other: 'xxxx',
})

act(() => form.current.clearErrors('name', 'email'))

expect(form.current.errors).toEqual({
other: 'xxxx',
})
expect(form.current.validator().errors()).toEqual({
other: ['xxxx'],
})
})

it('provides default data for validation requests', () => {
const response = { headers: { precognition: 'true', 'precognition-success': 'true' }, status: 204, data: 'data' }

let config: Config
axios.request.mockImplementation(async (c: Config) => {
config = c

return response
})

const { result: form } = renderHook(() => useForm('post', '/register', {
emails: '',
}))

act(() => form.current.setData('emails', 'taylor@laravel.com, tim@laravel.com'))
act(() => form.current.validate('emails'))

expect(config!.data.emails).toEqual('taylor@laravel.com, tim@laravel.com')
expect(form.current.data.emails).toBe('taylor@laravel.com, tim@laravel.com')
})

it('transforms data for validation requests', () => {
const response = { headers: { precognition: 'true', 'precognition-success': 'true' }, status: 204, data: 'data' }

let config: Config
axios.request.mockImplementation(async (c: Config) => {
config = c

return response
})

const { result: form } = renderHook(() => useForm('post', '/register', {
emails: '',
}))

act(() => form.current.transform((data) => ({
emails: data.emails.split(',').map((email: string) => email.trim()),
})))

act(() => form.current.setData('emails', 'taylor@laravel.com, tim@laravel.com'))
act(() => form.current.validate('emails'))

expect(config!.data.emails).toEqual(['taylor@laravel.com', 'tim@laravel.com'])
expect(form.current.data.emails).toBe('taylor@laravel.com, tim@laravel.com')
})

it('can set individual errors', function () {
const { result: form } = renderHook(() => useForm('post', '/register', {
name: '',
}))

act(() => form.current.setError('name', 'The name is required.'))

expect(form.current.errors.name).toBe('The name is required.')
})

it('can check that specific fields have been touched', () => {
const { result: form } = renderHook(() => useForm('post', '/register', {
name: '',
email: '',
}))

expect(form.current.touched('name')).toBe(false)
expect(form.current.touched('email')).toBe(false)

act(() => form.current.touch('name'))

expect(form.current.touched('name')).toBe(true)
expect(form.current.touched('email')).toBe(false)
})

it('can check it any fields have been touched', () => {
const { result: form } = renderHook(() => useForm('post', '/register', {
name: '',
email: '',
}))

expect(form.current.touched()).toBe(false)

act(() => form.current.touch('name'))

expect(form.current.touched()).toBe(true)
})

it('can set defaults with no arguments', () => {
let requests = 0
axios.request.mockImplementation(async () => {
requests++
})

const { result: form } = renderHook(() => useForm('post', '/register', {
name: 'John',
}))

act(() => form.current.setData('name', 'Jane'))
act(() => form.current.setDefaults())

act(() => form.current.setData('name', 'John'))
act(() => form.current.reset())
expect(form.current.data.name).toBe('Jane')

act(() => form.current.validate('name'))
expect(requests).toBe(0)
})

it('can set defaults with an object', () => {
let requests = 0
axios.request.mockImplementation(async () => {
requests++
})

const { result: form } = renderHook(() => useForm('post', '/register', {
name: 'John',
}))

act(() => form.current.setDefaults({ name: 'Jane' }))

act(() => form.current.setData('name', 'John'))
act(() => form.current.reset())
expect(form.current.data.name).toBe('Jane')

act(() => form.current.validate('name'))
expect(requests).toBe(0)
})

it('can set defaults with a function', () => {
let requests = 0
axios.request.mockImplementation(async () => {
requests++
})

const { result: form } = renderHook(() => useForm('post', '/register', {
name: 'John',
}))

act(() => form.current.setDefaults((prevData: any) => {
expect(prevData).toEqual({
name: 'John',
})

return {
name: 'Jane',
}
}))

act(() => form.current.setData('name', 'John'))
act(() => form.current.reset())
expect(form.current.data.name).toBe('Jane')

act(() => form.current.validate('name'))
expect(requests).toBe(0)
})

it('can set defaults with a field and value', () => {
let requests = 0
axios.request.mockImplementation(async () => {
requests++
})

const { result: form } = renderHook(() => useForm('post', '/register', {
name: 'John',
}))

act(() => form.current.setDefaults('name', 'Jane'))

act(() => form.current.setData('name', 'John'))
act(() => form.current.reset())
expect(form.current.data.name).toBe('Jane')

act(() => form.current.validate('name'))
expect(requests).toBe(0)
})
7 changes: 7 additions & 0 deletions packages/react-inertia/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
environment: 'jsdom',
},
})
Loading