Skip to content

Commit

Permalink
feat: 🎸 fixes support for async form submission
Browse files Browse the repository at this point in the history
Moves axios form submission logic into the useInertiaForm hook to allow
for triggering request lifecycle callbacks
  • Loading branch information
aviemet committed Jan 7, 2025
1 parent f64026e commit 23b162e
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 32 deletions.
45 changes: 28 additions & 17 deletions src/Form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/* 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 { renameObjectWithAttributes, unsetCompact } from '../utils'
import { unsetCompact } from '../utils'

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

Expand All @@ -20,10 +19,13 @@ export interface FormProps<TForm> extends PartialHTMLForm {
remember?: boolean
railsAttributes?: boolean
filter?: string[]
onSubmit?: (form: UseFormProps<TForm>) => boolean | void
onChange?: (form: UseFormProps<TForm>) => void
onSubmit?: (form: UseFormProps<TForm>) => boolean | void
onBefore?: (form: UseFormProps<TForm>) => void
onStart?: (form: UseFormProps<TForm>) => void
onSuccess?: (form: UseFormProps<TForm>) => void
onError?: (form: UseFormProps<TForm>) => void
onFinish?: (form: UseFormProps<TForm>) => void
}

const Form = <TForm extends NestedObject>({
Expand All @@ -36,14 +38,15 @@ const Form = <TForm extends NestedObject>({
resetAfterSubmit,
remember = true,
filter,
onSubmit,
onChange,
onSubmit,
onBefore,
onStart,
onSuccess,
onError,
onFinish,
...props
}: Omit<FormProps<TForm>, 'railsAttributes'>) => {
const { railsAttributes } = useFormMeta()

/**
* Omit values by key from the data object
*/
Expand All @@ -65,7 +68,6 @@ const Form = <TForm extends NestedObject>({
const contextValueObject = useCallback((): UseFormProps<TForm> => (
{ ...form, model, method, to, submit }
), [data, form, form.data, form.errors, model, method, to])

/**
* Submits the form. If async prop is true, submits using axios,
* otherwise submits using Inertia's `useForm.submit` method
Expand All @@ -75,29 +77,38 @@ const Form = <TForm extends NestedObject>({

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) => {
e.preventDefault()
e.stopPropagation()

submit({
const submitOptions: Partial<VisitOptions> = {
onSuccess: () => {
if(resetAfterSubmit || (resetAfterSubmit !== false && async === true)) {
form.reset()
}
onSuccess?.(contextValueObject())
},
})
}
if(onBefore) {
submitOptions.onBefore = () => {
onBefore(contextValueObject())
}
}
if(onStart) {
submitOptions.onStart = () => {
onStart(contextValueObject())
}
}
if(onFinish) {
submitOptions.onFinish = () => {
onFinish(contextValueObject())
}
}

submit(submitOptions)
}

// Set values from url search params. Allows for prefilling form data from a link
Expand Down
18 changes: 5 additions & 13 deletions src/useInertiaForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,11 @@ import { get, isEqual, isPlainObject, set } from 'lodash'
import { useFormMeta } from './Form/FormMetaWrapper'
import axios, { AxiosResponse } from 'axios'

type VisitOptions =
| (Omit<InertiaVisitOptions, 'errors' | 'onSuccess'> & {
errors?: Record<string, string | string[]>
async?: false
onSuccess?: (page: Page<PageProps>) => void
})
| (Omit<InertiaVisitOptions, 'errors' | 'onSuccess'> & {
errors?: Record<string, string | string[]>
async: true
onSuccess?: (page: AxiosResponse<any, any>) => void
})

type VisitOptions<TAsync extends boolean = boolean> = (Omit<InertiaVisitOptions, 'errors' | 'onSuccess'> & {
errors?: Record<string, string | string[]>
async: TAsync
onSuccess?: (page: TAsync extends true ? AxiosResponse<any, any> : Page<PageProps>) => void
})

type OnChangeCallback = (key: string | undefined, value: unknown, prev: unknown) => void

Expand Down Expand Up @@ -285,7 +278,6 @@ export default function useInertiaForm<TForm>(
if(railsAttributes) {
transformedData = renameObjectWithAttributes(transformedData)
}

if(options.async === true) {
_options.onBefore(undefined)
_options.onStart(undefined)
Expand Down
94 changes: 94 additions & 0 deletions tests/formComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Submit,
} from '../src'
import { router } from '@inertiajs/react'
import { Page, type PendingVisit } from '@inertiajs/core'
import { get } from 'lodash'
import ContextTest from './components/ContextTest'
import { multiRootData, singleRootData } from './components/data'
Expand Down Expand Up @@ -307,4 +308,97 @@ describe('Form Component', () => {
)
})
})

describe('when async is false', () => {
it('should trigger all callbacks in correct order with progress', async () => {
const callOrder: string[] = [];
const callbacks = {
onBefore: jest.fn(() => {
callOrder.push('onBefore')
}),
onStart: jest.fn(() => {
callOrder.push('onStart')
}),
onProgress: jest.fn(() => {
callOrder.push('onProgress')
}),
onSuccess: jest.fn(() => {
callOrder.push('onSuccess')
}),
onError: jest.fn(),
onFinish: jest.fn(() => {
callOrder.push('onFinish')
}),
}

const mockRequest = jest.spyOn(router, 'visit').mockImplementation((route, request) => {
const pendingVisit: PendingVisit = Object.assign({
url: new URL(`http://www.example.com${route}`),
method: 'post',
data: {},
replace: false,
preserveScroll: false,
preserveState: false,
only: [],
except: [],
headers: {},
errorBag: null,
forceFormData: false,
queryStringArrayFormat: 'indices',
async: false,
showProgress: false,
prefetch: false,
fresh: false,
reset: [],
preserveUrl: false,
completed: false,
cancelled: false,
interrupted: false,
}, request)

request.onBefore(pendingVisit)
request.onStart(pendingVisit)
request.onSuccess({
component: 'Page',
props: {},
url: `http://www.example.com${route}`,
version: '',
clearHistory: true,
encryptHistory: true,
} as Page<{}>)
request.onFinish(pendingVisit as any)

return Promise.resolve(request)
})

render(
<Form
model="person"
to="/form"
data={ singleRootData }
onBefore={ callbacks.onBefore }
onStart={ callbacks.onStart }
onProgress={ callbacks.onProgress }
onSuccess={ callbacks.onSuccess }
onFinish={ callbacks.onFinish }
>
<Input name="first_name" />
<Submit>Submit</Submit>
</Form>
)

const button = screen.getByRole('button')
await act(async () => {
await fireEvent.click(button)
});

expect(mockRequest).toHaveBeenCalled()

expect(callbacks.onBefore).toHaveBeenCalled();
expect(callbacks.onStart).toHaveBeenCalled();
expect(callbacks.onSuccess).toHaveBeenCalled();
expect(callbacks.onFinish).toHaveBeenCalled();

})
})
})
5 changes: 3 additions & 2 deletions tests/useInertiaForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useInertiaForm } from '../src'
import { get } from 'lodash'
import axios from 'axios'
import { singleRootData } from './components/data'
import { fillEmptyValues } from '../src/utils'

type InitialData = {
user: {
Expand Down Expand Up @@ -598,7 +599,7 @@ describe('submit', () => {
percentage: 50,
});
}
return Promise.resolve({ data });
return Promise.resolve(data);
});

const { result } = renderHook(() => useInertiaForm(singleRootData));
Expand All @@ -622,7 +623,7 @@ describe('submit', () => {
lengthComputable: true,
percentage: 50,
});
expect(callbacks.onSuccess).toHaveBeenCalledWith(undefined);
expect(callbacks.onSuccess).toHaveBeenCalledWith(fillEmptyValues(singleRootData));
expect(callbacks.onFinish).toHaveBeenCalledWith(undefined);

expect(callOrder).toEqual([
Expand Down

0 comments on commit 23b162e

Please sign in to comment.