Skip to content

Commit

Permalink
feat: 🎸 Builds data object from useInertiaInput
Browse files Browse the repository at this point in the history
`data` prop is no longer strictly necessary to pass in to the Form
component. Calling `useInertaInput` will build the data object if one
isn't provided.
  • Loading branch information
aviemet committed May 23, 2024
1 parent 6a6b367 commit fa74815
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 24 deletions.
7 changes: 5 additions & 2 deletions src/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { unset } from 'lodash'
type PartialHTMLForm = Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onChange'|'onSubmit'|'onError'>

export interface FormProps<TForm> extends PartialHTMLForm {
data: TForm
data?: TForm
model?: string
method?: HTTPVerb
to: string
Expand Down Expand Up @@ -40,6 +40,9 @@ const Form = <TForm extends NestedObject>({
onError,
...props
}: Omit<FormProps<TForm>, 'railsAttributes'>) => {
/**
* Omit values by key from the data object
*/
const filteredData = useCallback((data: TForm) => {
if(!filter) return data

Expand All @@ -57,7 +60,7 @@ const Form = <TForm extends NestedObject>({

const contextValueObject = useCallback((): UseFormProps<TForm> => (
{ ...form, model, method, to, submit }
), [data, form.data, form.errors])
), [data, form.data, form.errors, model, method, to])

/**
* Submits the form. If async prop is true, submits using axios,
Expand Down
7 changes: 4 additions & 3 deletions src/useInertiaForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export default function useInertiaForm<TForm>(
return keys[0]
}
return undefined
}, [])
}, [data])

// Errors
const [errors, setErrors] = rememberKey
Expand Down Expand Up @@ -293,9 +293,9 @@ export default function useInertiaForm<TForm>(
wasSuccessful,
recentlySuccessful,

transform: useCallback((callback) => {
transform: (callback) => {
transformRef.current = callback
}, []),
},

onChange: (callback) => {
onChangeRef.current = callback
Expand All @@ -310,6 +310,7 @@ export default function useInertiaForm<TForm>(
}

set(clone as NestedObject, keyOrData, maybeValue)

return clone
})
}
Expand Down
32 changes: 24 additions & 8 deletions src/useInertiaInput/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useEffect, useRef } from 'react'
import { useForm } from '../Form'
import { useNestedAttribute } from '../NestedFields'
import inputStrategy, { type InputStrategy } from './inputStrategy'
import { type NestedObject } from '../useInertiaForm'
import { useEffect } from 'react'

export interface UseInertiaInputProps {
export interface UseInertiaInputProps<T = string|number> {
name: string
model?: string
defaultValue?: T
errorKey?: string
strategy?: InputStrategy
clearErrorsOnChange?: boolean
Expand All @@ -15,40 +16,55 @@ export interface UseInertiaInputProps {
/**
* Returns form data and input specific methods to use with an input.
*/
const useInertiaInput = <T = number|string, TForm = NestedObject>({
const useInertiaInput = <T = string|number, TForm = NestedObject>({
name,
model,
defaultValue,
errorKey,
strategy = inputStrategy,
clearErrorsOnChange = true,
}: UseInertiaInputProps) => {
}: UseInertiaInputProps<T>) => {
const form = useForm<TForm>()

let usedModel = model ?? form.model

try {
const nested = useNestedAttribute()
usedModel += `.${nested}`
} catch(e) {}


const { inputName, inputId } = strategy(name, usedModel)

// Add a valid default value to the data object
const initializingRef = useRef(true)
if(usedModel === 'values') {
}
useEffect(() => {
if(!initializingRef.current) return

const inputValue = form.getData(inputName)
if(inputValue === null || inputValue === undefined) {
form.setData(inputName, defaultValue || '')
}

initializingRef.current = false
}, [])

const value = form.getData(inputName) as T
const usedErrorKey = errorKey ?? inputName
const error = form.getError(usedErrorKey)

// Clear errors when input value changes
useEffect(() => {
if(!clearErrorsOnChange || !error) return
if(initializingRef.current || !clearErrorsOnChange || !error) return

form.clearErrors(usedErrorKey)
}, [value])

return {
form,
inputName: inputName,
inputId,
value,
value: value || '',
setValue: (value: T) => {
return form.setData(inputName, value)
},
Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const unsetCompact = (data: NestedObject, path: string) => {
}

export const fillEmptyValues = <TForm>(data: TForm) => {
const clone = structuredClone(data)
const clone = structuredClone(data ?? {} as TForm)

for(const key in clone) {
if(isPlainObject(clone[key])) {
Expand Down
7 changes: 7 additions & 0 deletions tests/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/indent": "off"
}
}
47 changes: 37 additions & 10 deletions tests/useInertiaInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import '@testing-library/jest-dom'
import { Form, useForm } from '../src/Form'
import TestInput from './TestInput'

const FormContextTest = () => {

const ErrorContextTest = () => {
const { errors, setError } = useForm()

const handleClick = e => {
e.preventDefault()
setError('name', 'Error')
setError('errors.name', 'Error')
}

return (
Expand All @@ -21,44 +22,70 @@ const FormContextTest = () => {
}

describe ('useInertiaInput', () => {
describe('With defaultValue', () => {

it('builds the data object from inputs', async () => {

const ValuesContextTest = () => {
const { data } = useForm()

return <div data-testid="data">{ JSON.stringify(data) }</div>
}

await render(
<Form role="form" to="/" model="values" remember={ false }>
<TestInput name="name" />
<ValuesContextTest />
</Form>,
)

const input = screen.getByRole('input')

expect(screen.getByTestId('data')).toHaveTextContent('{"values":{"name":""}}')

fireEvent.change(input, { target: { value: 'value' } })
expect(screen.getByTestId('data')).toHaveTextContent('{"values":{"name":"value"}}')
})
})

describe('With clearErrorsOnChange = true', () => {
it('clears errors on an input when the value changes ', () => {
render(
<Form role="form" to="/" data={ { name: '' } } remember={ false }>
<FormContextTest />
<Form role="form" to="/" data={ { errors: { name: '' } } } model="errors" remember={ false }>
<TestInput name="name" />
<ErrorContextTest />
</Form>,
)

const errorButton = screen.getByRole('button', { name: 'error' })
const input = screen.getByRole('input')

fireEvent.click(errorButton)
expect(screen.getByTestId('errors')).toHaveTextContent('{"name":"Error"}')
expect(screen.getByTestId('errors')).toHaveTextContent('{"errors.name":"Error"}')

fireEvent.change(input, { target: { value: 'something' } })
expect(screen.getByTestId('errors')).toHaveTextContent('{}')
})

})

describe('With clearErrorsOnChange = true', () => {
describe('With clearErrorsOnChange = false', () => {
it('doesn\'t clear errors on an input when the value changes', () => {
render(
<Form role="form" to="/" data={ { name: '' } } remember={ false }>
<FormContextTest />
<Form role="form" to="/" data={ { errors: { name: '' } } } model="errors" remember={ false }>
<TestInput name="name" clearErrorsOnChange={ false } />
<ErrorContextTest />
</Form>,
)

const errorButton = screen.getByRole('button', { name: 'error' })
const input = screen.getByRole('input')

fireEvent.click(errorButton)
expect(screen.getByTestId('errors')).toHaveTextContent('{"name":"Error"}')
expect(screen.getByTestId('errors')).toHaveTextContent('{"errors.name":"Error"}')

fireEvent.change(input, { target: { value: 'something' } })
expect(screen.getByTestId('errors')).toHaveTextContent('{"name":"Error"}')
expect(screen.getByTestId('errors')).toHaveTextContent('{"errors.name":"Error"}')
})

})
Expand Down

0 comments on commit fa74815

Please sign in to comment.