Skip to content

Commit

Permalink
feat: fixes async submit not applying transofmration or attributes re…
Browse files Browse the repository at this point in the history
…writes
  • Loading branch information
aviemet committed Jan 5, 2025
1 parent a86a6da commit 5113126
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 159 deletions.
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
"lint:fix": "npm run lint -- --fix",
"lint:types": "tsc --noEmit",
"lint:all": "yarn lint && yarn lint:types",
"test": "jest --silent=false",
"test:watch": "jest --watch --silent=false",
"test:coverage": "jest --coverage",
"test": "NODE_NO_WARNINGS=1 jest --silent=false",
"test:watch": "NODE_NO_WARNINGS=1 jest --watch --silent=false",
"test:coverage": "NODE_NO_WARNINGS=1 jest --coverage",
"release": "semantic-release",
"cz": "git-cz"
},
Expand Down Expand Up @@ -67,7 +67,7 @@
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.13",
"@types/lodash": "^4.17.14",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@typescript-eslint/eslint-plugin": "^8.19.0",
Expand Down Expand Up @@ -100,14 +100,14 @@
"react-dom": "^19.0.0",
"react-test-renderer": "^19.0.0",
"rimraf": "^6.0.1",
"rollup": "^4.29.1",
"rollup": "^4.29.2",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-filesize": "^10.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-ts": "^3.4.5",
"rollup-plugin-typescript2": "^0.36.0",
"semantic-release": "^24.2.0",
"semantic-release": "^24.2.1",
"ts-jest": "^29.2.5",
"typescript": "^5.7.2"
},
Expand Down
24 changes: 16 additions & 8 deletions src/Form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable react-hooks/rules-of-hooks */
import React, { useCallback, useEffect } from 'react'
import axios from 'axios'
import { type VisitOptions } from '@inertiajs/core'
import useInertiaForm, { NestedObject } from '../useInertiaForm'
import { useForm, type UseFormProps, type HTTPVerb, FormProvider } from './FormProvider'
import FormMetaWrapper, { useFormMeta, type FormMetaValue } from './FormMetaWrapper'
import { unsetCompact } from '../utils'
import { renameObjectWithAttributes, unsetCompact } from '../utils'

type PartialHTMLForm = Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onChange' | 'onSubmit' | 'onError'>

Expand Down Expand Up @@ -40,6 +42,8 @@ const Form = <TForm extends NestedObject>({
onError,
...props
}: Omit<FormProps<TForm>, 'railsAttributes'>) => {
const { railsAttributes } = useFormMeta()

/**
* Omit values by key from the data object
*/
Expand Down Expand Up @@ -69,13 +73,17 @@ const Form = <TForm extends NestedObject>({
const submit = async (options?: Partial<VisitOptions>) => {
let shouldSubmit = to && onSubmit?.(contextValueObject()) === false ? false : true

if(shouldSubmit) {
if(async) {
return axios[method](to, form.data)
} else {
return form.submit(method, to, options)
}
}
if(!shouldSubmit) return

// if(async) {
// const data = railsAttributes === true ?
// renameObjectWithAttributes(form.data)
// :
// form.data
// return axios[method](to, data)
// }

return form.submit(method, to, { ...options, async: async === true ? true : false })
}

const handleSubmit = (e: React.FormEvent) => {
Expand Down
27 changes: 24 additions & 3 deletions src/useInertiaForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ import {
} from './utils'
import { get, isEqual, isPlainObject, set } from 'lodash'
import { useFormMeta } from './Form/FormMetaWrapper'
import axios from 'axios'

type VisitOptions = Omit<InertiaVisitOptions, 'errors'> & {
errors?: Record<string, string | string[]>
async?: boolean
}

type OnChangeCallback = (key: string | undefined, value: unknown, prev: unknown) => void
Expand Down Expand Up @@ -274,10 +276,29 @@ export default function useInertiaForm<TForm>(
transformedData = renameObjectWithAttributes(transformedData)
}

if(method === 'delete') {
router.delete(url, { ..._options, data: transformedData as RequestPayload })
if(options.async === true) {
_options.onBefore(undefined)
_options.onStart(undefined)
axios[method](url, transformedData as RequestPayload, {
onUploadProgress: progessEvent => {
_options.onProgress(progessEvent)
},
})
.then(response => {
_options.onSuccess(undefined)
})
.catch(error => {
_options.onError(error)
})
.finally(() => {
_options.onFinish(undefined)
})
} else {
router[method](url, transformedData as RequestPayload, _options)
if(method === 'delete') {
router.delete(url, { ..._options, data: transformedData as RequestPayload })
} else {
router[method](url, transformedData as RequestPayload, _options)
}
}
}

Expand Down
172 changes: 124 additions & 48 deletions tests/formComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { router } from '@inertiajs/react'
import { get } from 'lodash'
import ContextTest from './components/ContextTest'
import { multiRootData, singleRootData } from './components/data'
import axios from 'axios'

describe('Form Component', () => {
describe('When not passed a data object', () => {
Expand Down Expand Up @@ -103,25 +104,56 @@ describe('Form Component', () => {
expect(input).toHaveValue('modified form data')
})

it('sends the correct data to the server upon form submit', () => {
const mockRequest = jest.spyOn(router, 'visit').mockImplementation((route, request) => {
const data = request?.data
expect(get(data, 'person.nested.key')).toBe('value')
return Promise.resolve({ data: request?.data })
describe('when async is false', () => {
it('sends the correct data to the server upon form submit', async () => {
let capturedData: any

const mockRequest = jest.spyOn(router, 'visit').mockImplementation((route, request) => {
capturedData = request?.data

return Promise.resolve({ data: request?.data })
})

render(
<Form model="person" to="/form" data={ { ...singleRootData } } remember={ false }>
<Input name="first_name" />
<Input name="nested.key" />
<Submit>Submit</Submit>
</Form>,
)

const button = screen.getByRole('button')
await fireEvent.click(button)

expect(mockRequest).toHaveBeenCalled()

expect(get(capturedData, 'person.nested.key')).toBe('value')
})
})

render(
<Form model="person" to="/form" data={ { ...singleRootData } } remember={ false }>
<Input name="first_name" />
<Input name="nested.key" />
<Submit>Submit</Submit>
</Form>,
)
describe('when async is true', () => {
it('sends the correct data to the server upon form submit', async () => {
let capturedData: any
const mockRequest = jest.spyOn(axios, 'post').mockImplementation((url, data, config) => {
capturedData = data
return Promise.resolve({ data })
})

render(
<Form async model="person" to="/form" data={ { ...singleRootData } } remember={ false }>
<Input name="first_name" />
<Input name="nested.key" />
<Submit>Submit</Submit>
</Form>,
)

const button = screen.getByRole('button')
await fireEvent.click(button)

const button = screen.getByRole('button')
fireEvent.click(button)
expect(mockRequest).toHaveBeenCalled()

expect(mockRequest).toHaveBeenCalled()
expect(get(capturedData, 'person.nested.key')).toBe('value')
})
})
})

Expand Down Expand Up @@ -165,42 +197,86 @@ describe('Form Component', () => {
expect(input).toHaveValue('rails attributes')
})

it('sends the correct data to the server upon form submit', () => {
const mockRequest = jest.spyOn(router, 'visit').mockImplementation((route, request) => {
const data = request?.data

expect(get(data, 'user.username')).toBe(multiRootData.user.username)
expect(get(data, 'person.nested_attributes.key')).toBe(multiRootData.person.nested.key)
expect(get(data, 'extra.value')).toBe('exists')

return Promise.resolve({ data: request?.data })
describe('with async false', () => {
it('sends the correct data to the server upon form submit', () => {
let capturedData: any

const mockRequest = jest.spyOn(router, 'visit').mockImplementation((route, request) => {
capturedData = request?.data

return Promise.resolve({ data: request?.data })
})

const handleSubmit = (form) => {
form.transform(data => ({ ...data, extra: { value: 'exists' } }))
}

render(
<Form
model="person"
to="/form"
data={ multiRootData }
railsAttributes
remember={ false }
onSubmit={ handleSubmit }
>
<Input name="first_name" />
<Input name="nested.key" />
<Submit>Submit</Submit>
</Form>,
)

const button = screen.getByRole('button')
fireEvent.click(button)

expect(mockRequest).toHaveBeenCalled()

expect(get(capturedData, 'user.username')).toBe(multiRootData.user.username)
expect(get(capturedData, 'person.nested_attributes.key')).toBe(multiRootData.person.nested.key)
expect(get(capturedData, 'extra.value')).toBe('exists')
})

const handleSubmit = (form) => {
form.transform(data => ({ ...data, extra: { value: 'exists' } }))
}

render(
<Form
model="person"
to="/form"
data={ multiRootData }
railsAttributes
remember={ false }
onSubmit={ handleSubmit }
>
<Input name="first_name" />
<Input name="nested.key" />
<Submit>Submit</Submit>
</Form>,
)

const button = screen.getByRole('button')
fireEvent.click(button)

expect(mockRequest).toHaveBeenCalled()
})

describe('with async true', () => {
it('sends the correct data to the server upon form submit', async () => {
let capturedData: any

const mockRequest = jest.spyOn(axios, 'post').mockImplementation((url, data, config) => {
capturedData = data

return Promise.resolve({ data })
})

const handleSubmit = (form) => {
form.transform(data => ({ ...data, extra: { value: 'exists' } }))
}

render(
<Form
async
model="person"
to="/form"
data={ singleRootData }
railsAttributes
remember={ false }
onSubmit={ handleSubmit }
>
<Input name="first_name" />
<Input name="nested.key" />
<Submit>Submit</Submit>
</Form>,
)

const button = screen.getByRole('button')
await fireEvent.click(button)

expect(mockRequest).toHaveBeenCalled()

expect(get(capturedData, 'person.first_name')).toEqual(singleRootData.person.first_name)
expect(get(capturedData, 'person.nested_attributes.key')).toEqual(singleRootData.person.nested.key)
expect(get(capturedData, 'extra.value')).toEqual('exists')
})
})
})

describe('Filter', () => {
Expand Down
52 changes: 52 additions & 0 deletions tests/useInertiaForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { act, renderHook } from '@testing-library/react'
import { router } from '@inertiajs/core'
import { useInertiaForm } from '../src'
import { get } from 'lodash'
import axios from 'axios'
import { singleRootData } from './components/data'

type InitialData = {
user: {
Expand Down Expand Up @@ -537,4 +539,54 @@ describe('submit', () => {
expect(mockRequest).toHaveBeenCalled()
})
})

describe('when async is true', () => {
it('should submit transformed data using axios', async () => {
const testData = {
user: {
username: 'some name',
},
}

let capturedData: any
const mockRequest = jest.spyOn(axios, 'post').mockImplementation((url, data) => {
capturedData = data
return Promise.resolve({ data })
})

const { result } = renderHook(() => useInertiaForm(testData))

await act(async () => {
result.current.transform(data => ({ ...data, transformed: 'value' }))
await result.current.submit('post', '/form', { async: true })

expect(mockRequest).toHaveBeenCalled()

expect(capturedData).toMatchObject({ ...testData, transformed: 'value' })
})
})

it('should trigger the onSuccess option', async () => {
let capturedData: any
const mockRequest = jest.spyOn(axios, 'post').mockImplementation((url, data) => {
capturedData = data
return Promise.resolve({ data })
})

const { result } = renderHook(() => useInertiaForm(singleRootData))

await act(async () => {
await result.current.submit('post', '/form', {
async: true,
onSuccess: (page) => {
console.log(page)
},
})

expect(mockRequest).toHaveBeenCalled()


})
})
})
})
Loading

0 comments on commit 5113126

Please sign in to comment.