Skip to content

Commit

Permalink
Merge pull request #25 from aviemet/defaults
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 authored May 27, 2024
2 parents 6a6b367 + 128bb0c commit c28a60e
Show file tree
Hide file tree
Showing 17 changed files with 531 additions and 209 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"axios-mock-adapter": "^1.22.0",
"circular-json": "^0.5.9",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.57",
Expand Down
9 changes: 6 additions & 3 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,14 +60,14 @@ 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,
* otherwise submits using Inertia's `useForm.submit` method
*/
const submit = async (options?: Partial<VisitOptions>) => {
let shouldSubmit = to && onSubmit && onSubmit(contextValueObject()) === false ? false : true
let shouldSubmit = to && onSubmit?.(contextValueObject()) === false ? false : true

if(shouldSubmit) {
if(async) {
Expand Down
2 changes: 1 addition & 1 deletion src/Inputs/DynamicInputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const DynamicInputs = ({

return (
<>
{ React.cloneElement(addInputButton, { onClick: addInput, type: 'button' }) }
{ React.cloneElement(addInputButton, { onClick: ( )=> addInput(), type: 'button' }) }
{ paths.map((path, i) => (
<NestedFields key={ i } model={ path }>
<div>{ children }</div>
Expand Down
47 changes: 31 additions & 16 deletions src/Inputs/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,54 @@
import React from 'react'
import useInertiaInput from '../useInertiaInput'
import { NestedObject } from '../useInertiaForm'
import { BaseFormInputProps, InputConflicts } from '.'

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
name: string
model?: string
interface InputProps<TForm extends NestedObject, T = string>
extends
Omit<React.InputHTMLAttributes<HTMLInputElement>, InputConflicts>,
BaseFormInputProps<T, TForm>
{
component?: React.ElementType
}

const Input = React.forwardRef<HTMLInputElement, InputProps>((
{ name, component = 'input', model, onChange, ...props },
ref,
const Input = <TForm extends NestedObject, T = string>(
{ name,
component = 'input',
type = 'text',
model,
onChange,
errorKey,
defaultValue,
clearErrorsOnChange,
...props
}: InputProps<TForm, T>,
) => {
const { inputName, inputId, value, setValue } = useInertiaInput({ name, model })

const { form, inputName, inputId, value, setValue } = useInertiaInput<T, TForm>({
name,
model,
errorKey,
defaultValue,
clearErrorsOnChange,
})
// console.log({ name, model, errorKey, defaultValue, inputName, inputId, value })
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if(onChange) {
onChange(e)
return
}

setValue(e.target.value)
const value = (e.target?.checked || e.target.value) as T
setValue(value)
onChange?.(value, form)
}

const Element = component

return (
<Element
type={ type }
name={ inputName }
id={ inputId }
value={ value }
onChange={ handleChange }
ref={ ref }
{ ...props }
/>
)
})
}

export default Input
19 changes: 19 additions & 0 deletions src/Inputs/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
import { UseFormProps } from '../Form'
import { NestedObject } from '../useInertiaForm'
import { UseInertiaInputProps } from '../useInertiaInput'

export { default as Input } from './Input'
export { default as Submit } from './Submit'

export type InputConflicts = 'name'|'onChange'|'onBlur'|'onFocus'|'value'|'defaultValue'
export interface BaseFormInputProps<T, TForm extends NestedObject = NestedObject>
extends UseInertiaInputProps<T>
{
model?: string
errorKey?: string
field?: boolean
required?: boolean
hidden?: boolean
onChange?: (value: T, form: UseFormProps<TForm>) => void
onBlur?: (value: T, form: UseFormProps<TForm>) => void
onFocus?: (value: T, form: UseFormProps<TForm>) => void
wrapperProps?: Record<string, any>
}
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
31 changes: 23 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|boolean> {
name: string
model?: string
defaultValue?: T
errorKey?: string
strategy?: InputStrategy
clearErrorsOnChange?: boolean
Expand All @@ -15,40 +16,54 @@ 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|boolean, 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)

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 ?? '' as T,
setValue: (value: T) => {
return form.setData(inputName, value)
},
Expand Down
32 changes: 30 additions & 2 deletions 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 Expand Up @@ -110,9 +110,36 @@ export const isUnset = (v: any) => {
return isEmpty(v)
}

// Added recursion limit to path types to prevent the error:
// "Type instantiation is excessively deep and possibly infinite"
type Increment<A extends any[]> = [0, ...A];

type PathImpl<T, K extends keyof T, A extends any[] = []> =
A['length'] extends 5 ? never :
K extends string
? T[K] extends Record<string, any>
? T[K] extends ArrayLike<any>
? K | `${K}.${PathImpl<T[K], Exclude<keyof T[K], keyof any[]>, Increment<A>>}`
: K | `${K}.${PathImpl<T[K], keyof T[K], Increment<A>>}`
: K
: never;

export type Path<T> = PathImpl<T, keyof T> | Extract<keyof T, string>;

export type PathValue<T, P extends Path<Required<T>>> =
P extends `${infer K}.${infer Rest}`
? K extends keyof Required<T>
? Rest extends Path<Required<T>[K]>
? PathValue<Required<T>[K], Rest>
: never
: never
: P extends keyof Required<T>
? Required<T>[P]
: never;

// Copied from https://gist.github.com/balthild/1f23725059aef8b9231d6c346494b918
// which was copied from https://twitter.com/diegohaz/status/1309489079378219009
type PathImpl<T, K extends keyof T> =
/*type PathImpl<T, K extends keyof T, D extends number = 5> =
K extends string
? T[K] extends Record<string, any>
? T[K] extends ArrayLike<any>
Expand All @@ -133,3 +160,4 @@ export type PathValue<T, P extends Path<Required<T>>> =
: P extends keyof Required<T>
? Required<T>[P]
: never
*/
5 changes: 5 additions & 0 deletions tests/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"no-unused-vars": "off"
}
}
29 changes: 0 additions & 29 deletions tests/TestInput.tsx

This file was deleted.

38 changes: 38 additions & 0 deletions tests/components/ContextTest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { useEffect } from 'react'
import { NestedObject, useForm, UseFormProps } from '../../src'
import CircularJSON from 'circular-json'

const safeStringify = (obj) => {
try {
return CircularJSON.stringify(obj)
} catch(err) {
console.error('Error stringifying object:', err)
return null
}
}

interface ContextTestProps<T = NestedObject> {
cb?: (form: UseFormProps<T>) => void
}

const ContextTest = <T = NestedObject>({ cb }: ContextTestProps<T>) => {
const form = useForm<T>()

useEffect(() => {
cb?.(form)
}, [cb])

return (
<>
<div data-testid="data">
{ safeStringify(form.data) }
</div>
<div data-testid="errors">
{ safeStringify(form.errors) }
</div>
</>
)

}

export default ContextTest
Loading

0 comments on commit c28a60e

Please sign in to comment.