Skip to content

Commit

Permalink
Added getButtonProps() to useForm()
Browse files Browse the repository at this point in the history
  • Loading branch information
jalik committed Apr 15, 2024
1 parent 371017e commit 4d78ccc
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 48 deletions.
46 changes: 3 additions & 43 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2024 Karl STEIN
*/

import React, { ElementType, useCallback, useMemo } from 'react'
import React, { ElementType } from 'react'
import useFormContext from '../useFormContext'

export type ButtonProps<C extends ElementType> = React.ComponentProps<C> & {
Expand All @@ -21,59 +21,19 @@ function Button<C extends ElementType = 'button'> (props: ButtonProps<C>): React
const {
children,
component: Component = 'button',
disabled,
onClick,
type,
...others
} = props

const {
disabled: formDisabled,
modified,
reset,
submit,
submitting
} = useFormContext()

// Disable button when form is disabled, submitting, or unmodified.
const isDisabled = useMemo(() => (
disabled ||
formDisabled ||
submitting ||
(type === 'reset' && !modified) ||
(type === 'submit' && !modified)
), [disabled, formDisabled, modified, submitting, type])

const handleClick = useCallback((ev: React.MouseEvent) => {
// Prevent submission.
ev.preventDefault()
// Prevent parent form submission.
ev.stopPropagation()

if (onClick) {
onClick(ev)
} else if (type === 'submit') {
submit()
} else if (type === 'reset') {
reset()
}
}, [onClick, reset, submit, type])
const { getButtonProps } = useFormContext()

return (
<Component
{...others}
disabled={isDisabled}
onClick={handleClick}
type={type}
>
<Component {...getButtonProps(others)}>
{children}
</Component>
)
}

Button.defaultProps = {
children: undefined,
component: undefined,
type: 'button'
}

Expand Down
47 changes: 42 additions & 5 deletions src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export interface UseFormHook<V extends Values, E, R> extends FormState<V, E, R>
* @param fields
*/
clearTouchedFields (fields?: string[]): void;
/**
* Returns form button props.
* @param props
*/
getButtonProps (props?: React.ComponentProps<'button'>): React.ComponentProps<'button'>;
/**
* Returns field props by name.
* @param name
Expand Down Expand Up @@ -800,6 +805,21 @@ function useForm<V extends Values, E = Error, R = any> (options: UseFormOptions<
})
}, [])

const handleButtonClick = useCallback((listener: (ev: React.MouseEvent<HTMLButtonElement>) => void) => (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
event.stopPropagation()

const { currentTarget } = event

if (listener) {
listener(event)
} else if (currentTarget.type === 'submit') {
submit()
} else if (currentTarget.type === 'reset') {
reset()
}
}, [reset, submit])

/**
* Handles leaving of a field.
*/
Expand Down Expand Up @@ -865,6 +885,22 @@ function useForm<V extends Values, E = Error, R = any> (options: UseFormOptions<
validateAndSubmit()
}, [validateAndSubmit])

/**
* Returns button props.
*/
const getButtonProps = useCallback((props: React.ComponentProps<'button'>): React.ComponentProps<'button'> => {
const type = props?.type || 'button'
return {
...props,
disabled: formDisabled || props?.disabled ||
// Disable submit or reset button if form is not modified.
(!state.modified && (type === 'submit' || type === 'reset')),
onClick: props?.onClick != null
? handleButtonClick(props?.onClick)
: undefined
}
}, [formDisabled, handleButtonClick, state.modified])

/**
* Returns props of a field.
* Merging is done in the following order:
Expand Down Expand Up @@ -1002,6 +1038,7 @@ function useForm<V extends Values, E = Error, R = any> (options: UseFormOptions<
clear,
clearErrors,
clearTouchedFields,
getButtonProps,
getFieldProps,
getInitialValue,
getValue,
Expand All @@ -1024,11 +1061,11 @@ function useForm<V extends Values, E = Error, R = any> (options: UseFormOptions<
validate: debouncedValidate,
validateField: debouncedValidateField,
validateFields: debouncedValidateFields
}), [state, formDisabled, clear, clearErrors, clearTouchedFields, getFieldProps, getInitialValue,
getValue, handleBlur, handleChange, handleReset, handleSetValue, handleSubmit, setInitialValues,
load, removeFields, reset, setError, setErrors, setValue, setValues, debouncedValidateAndSubmit,
setTouchedField, setTouchedFields, debouncedValidate, debouncedValidateField,
debouncedValidateFields])
}), [state, formDisabled, clear, clearErrors, clearTouchedFields, getButtonProps,
getFieldProps, getInitialValue, getValue, handleBlur, handleChange, handleReset, handleSetValue,
handleSubmit, setInitialValues, load, removeFields, reset, setError, setErrors, setValue,
setValues, debouncedValidateAndSubmit, setTouchedField, setTouchedFields, debouncedValidate,
debouncedValidateField, debouncedValidateFields])
}

export default useForm
78 changes: 78 additions & 0 deletions test/useForm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,84 @@ describe('useForm()', () => {
})
})

describe('getButtonProps()', () => {
const initialValues = { username: '' }
const { result } = renderHook(() => {
return useForm({
disabled: false,
initialValues,
onSubmit: () => Promise.resolve(true)
})
})

describe('without custom props', () => {
it('should return default props', () => {
const props = result.current.getButtonProps()
expect(props).toBeDefined()
expect(props.disabled).toBe(false)
expect(typeof props.type).toBe('undefined')
})
})

describe('with custom props', () => {
it('should return custom props', () => {
const props = result.current.getButtonProps({
disabled: true,
type: 'submit'
})
expect(props).toBeDefined()
expect(props.disabled).toBe(true)
expect(props.type).toBe('submit')
})
})

describe('with { type: "submit" }', () => {
it('should return disabled = true when form.modified = false', () => {
const initialValues = { username: '' }
const { result } = renderHook(() => {
return useForm({
initialValues,
onSubmit: () => Promise.resolve(true)
})
})
const props = result.current.getButtonProps({ type: 'submit' })
expect(props.disabled).toBe(true)
})
})

describe('with { type: "reset" }', () => {
it('should return disabled = true when form.modified = false', () => {
const initialValues = { username: '' }
const { result } = renderHook(() => {
return useForm({
initialValues,
onSubmit: () => Promise.resolve(true)
})
})
const props = result.current.getButtonProps({ type: 'reset' })
expect(props.disabled).toBe(true)
})
})

describe('with form submitting', () => {
it('should return disabled = true', () => {
const initialValues = { username: '' }
const { result } = renderHook(() => {
return useForm({
initialValues,
onSubmit: () => Promise.resolve(true)
})
})
act(() => {
result.current.setValue('username', 'jalik')
result.current.submit()
const props = result.current.getButtonProps({ type: 'submit' })
expect(props.disabled).toBe(true)
})
})
})
})

describe('getFieldProps(name)', () => {
const initialValues = { username: 'jalik' }
const { result } = renderHook(() => {
Expand Down

0 comments on commit 4d78ccc

Please sign in to comment.