market/src/components/@shared/FormInput/index.tsx

155 lines
4.0 KiB
TypeScript

import React, {
ChangeEvent,
FormEvent,
KeyboardEvent,
ReactElement,
ReactNode,
useEffect,
useState
} from 'react'
import InputElement from './InputElement'
import Label from './Label'
import styles from './index.module.css'
import { ErrorMessage, FieldInputProps } from 'formik'
import classNames from 'classnames/bind'
import Disclaimer from './Disclaimer'
import Tooltip from '@shared/atoms/Tooltip'
import Markdown from '@shared/Markdown'
import FormHelp from './Help'
const cx = classNames.bind(styles)
export interface InputProps {
name: string
label?: string | ReactNode
placeholder?: string
required?: boolean
help?: string
prominentHelp?: boolean
tag?: string
type?: string
options?: string[]
sortOptions?: boolean
additionalComponent?: ReactElement
value?: string | number
onChange?(
e:
| FormEvent<HTMLInputElement>
| ChangeEvent<HTMLInputElement>
| ChangeEvent<HTMLSelectElement>
| ChangeEvent<HTMLTextAreaElement>
): void
onKeyPress?(
e:
| KeyboardEvent<HTMLInputElement>
| KeyboardEvent<HTMLInputElement>
| KeyboardEvent<HTMLSelectElement>
| KeyboardEvent<HTMLTextAreaElement>
): void
rows?: number
multiple?: boolean
pattern?: string
min?: string
max?: string
disabled?: boolean
readOnly?: boolean
field?: FieldInputProps<any>
form?: any
prefix?: string | ReactElement
postfix?: string | ReactElement
step?: string
defaultChecked?: boolean
size?: 'mini' | 'small' | 'large' | 'default'
className?: string
checked?: boolean
disclaimer?: string
disclaimerValues?: string[]
}
function checkError(
form: any,
parsedFieldName: string[],
field: FieldInputProps<any>
) {
if (form?.errors && Object.entries(form?.errors).length === 0) {
return false
} else if (
(form?.touched?.[parsedFieldName[0]]?.[parsedFieldName[1]] &&
form?.errors?.[parsedFieldName[0]]?.[parsedFieldName[1]]) ||
(form?.touched[field.name] &&
form?.errors[field.name] &&
field.name !== 'links')
) {
return true
}
}
export default function Input(props: Partial<InputProps>): ReactElement {
const {
label,
help,
prominentHelp,
additionalComponent,
size,
form,
field,
disclaimer,
disclaimerValues
} = props
const isFormikField = typeof field !== 'undefined'
const isNestedField = field?.name?.includes('.')
// TODO: this feels hacky as it assumes nested `values` store. But we can't use the
// `useField()` hook in here to get `meta.error` so we have to match against form?.errors?
// handling flat and nested data at same time.
const parsedFieldName =
isFormikField && (isNestedField ? field?.name.split('.') : [field?.name])
const hasFormikError = checkError(form, parsedFieldName, field)
const styleClasses = cx({
field: true,
hasError: hasFormikError
})
const [disclaimerVisible, setDisclaimerVisible] = useState(true)
useEffect(() => {
if (!isFormikField) return
if (disclaimer && disclaimerValues) {
setDisclaimerVisible(
disclaimerValues.includes(
props.form?.values[parsedFieldName[0]]?.[parsedFieldName[1]]
)
)
}
}, [isFormikField, props.form?.values])
return (
<div className={styleClasses}>
<Label htmlFor={props.name}>
{label}
{props.required && (
<span title="Required" className={styles.required}>
*
</span>
)}
{help && !prominentHelp && (
<Tooltip content={<Markdown text={help} />} />
)}
</Label>
<InputElement size={size} {...field} {...props} />
{help && prominentHelp && <FormHelp>{help}</FormHelp>}
{isFormikField && hasFormikError && (
<div className={styles.error}>
<ErrorMessage name={field.name} />
</div>
)}
{disclaimer && (
<Disclaimer visible={disclaimerVisible}>{disclaimer}</Disclaimer>
)}
{additionalComponent && additionalComponent}
</div>
)
}