diff --git a/src/Form/index.tsx b/src/Form/index.tsx index c2b418d..0c60180 100644 --- a/src/Form/index.tsx +++ b/src/Form/index.tsx @@ -9,7 +9,7 @@ import { unset } from 'lodash' type PartialHTMLForm = Omit, 'onChange'|'onSubmit'|'onError'> export interface FormProps extends PartialHTMLForm { - data: TForm + data?: TForm model?: string method?: HTTPVerb to: string @@ -40,6 +40,9 @@ const Form = ({ onError, ...props }: Omit, 'railsAttributes'>) => { + /** + * Omit values by key from the data object + */ const filteredData = useCallback((data: TForm) => { if(!filter) return data @@ -57,7 +60,7 @@ const Form = ({ const contextValueObject = useCallback((): UseFormProps => ( { ...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, diff --git a/src/useInertiaForm.ts b/src/useInertiaForm.ts index 9109530..bb6109d 100644 --- a/src/useInertiaForm.ts +++ b/src/useInertiaForm.ts @@ -108,7 +108,7 @@ export default function useInertiaForm( return keys[0] } return undefined - }, []) + }, [data]) // Errors const [errors, setErrors] = rememberKey @@ -293,9 +293,9 @@ export default function useInertiaForm( wasSuccessful, recentlySuccessful, - transform: useCallback((callback) => { + transform: (callback) => { transformRef.current = callback - }, []), + }, onChange: (callback) => { onChangeRef.current = callback @@ -310,6 +310,7 @@ export default function useInertiaForm( } set(clone as NestedObject, keyOrData, maybeValue) + return clone }) } diff --git a/src/useInertiaInput/index.ts b/src/useInertiaInput/index.ts index 5a277f0..1a6033f 100644 --- a/src/useInertiaInput/index.ts +++ b/src/useInertiaInput/index.ts @@ -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 { name: string model?: string + defaultValue?: T errorKey?: string strategy?: InputStrategy clearErrorsOnChange?: boolean @@ -15,32 +16,47 @@ export interface UseInertiaInputProps { /** * Returns form data and input specific methods to use with an input. */ -const useInertiaInput = ({ +const useInertiaInput = ({ name, model, + defaultValue, errorKey, strategy = inputStrategy, clearErrorsOnChange = true, -}: UseInertiaInputProps) => { +}: UseInertiaInputProps) => { const form = useForm() 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]) @@ -48,7 +64,7 @@ const useInertiaInput = ({ form, inputName: inputName, inputId, - value, + value: value || '', setValue: (value: T) => { return form.setData(inputName, value) }, diff --git a/src/utils.ts b/src/utils.ts index dec0716..a52f9a7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -34,7 +34,7 @@ export const unsetCompact = (data: NestedObject, path: string) => { } export const fillEmptyValues = (data: TForm) => { - const clone = structuredClone(data) + const clone = structuredClone(data ?? {} as TForm) for(const key in clone) { if(isPlainObject(clone[key])) { diff --git a/tests/.eslintrc b/tests/.eslintrc new file mode 100644 index 0000000..f3fa314 --- /dev/null +++ b/tests/.eslintrc @@ -0,0 +1,7 @@ +{ + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/indent": "off" + } +} \ No newline at end of file diff --git a/tests/useInertiaInput.test.tsx b/tests/useInertiaInput.test.tsx index cd81d93..d4b0339 100644 --- a/tests/useInertiaInput.test.tsx +++ b/tests/useInertiaInput.test.tsx @@ -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 ( @@ -21,12 +22,38 @@ const FormContextTest = () => { } describe ('useInertiaInput', () => { + describe('With defaultValue', () => { + + it('builds the data object from inputs', async () => { + + const ValuesContextTest = () => { + const { data } = useForm() + + return
{ JSON.stringify(data) }
+ } + + await render( +
+ + + , + ) + + 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( -
- + + , ) @@ -34,7 +61,7 @@ describe ('useInertiaInput', () => { 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('{}') @@ -42,12 +69,12 @@ describe ('useInertiaInput', () => { }) - describe('With clearErrorsOnChange = true', () => { + describe('With clearErrorsOnChange = false', () => { it('doesn\'t clear errors on an input when the value changes', () => { render( -
- + + , ) @@ -55,10 +82,10 @@ describe ('useInertiaInput', () => { 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"}') }) })