Last active
July 9, 2023 17:54
-
-
Save arleighdickerson/7ada7cc4854316f43288da97f456821c to your computer and use it in GitHub Desktop.
a reusable "active field" to display validation errors in react-final-form
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createRef, PureComponent, RefObject } from 'react'; | |
import { Field, RenderableProps, UseFieldConfig } from 'react-final-form'; | |
import { ExclamationCircleIcon } from '@heroicons/react/24/outline'; | |
import _ from 'lodash'; | |
export interface ActiveFieldProps { | |
attribute: string; | |
label?: string; | |
type?: string; | |
component?: RenderableProps<any>['component']; | |
placeholder?: string; | |
description?: string; | |
autoComplete?: string; | |
} | |
const safeInvoke: (object: any, path: string[], ...args: any[]) => unknown = new Proxy(_.invoke, { | |
apply(target, thisArg, argArray: any[]): any { | |
try { | |
return Reflect.apply(target, thisArg, argArray); | |
} catch (e) { | |
if (process.env.NODE_ENV === 'development') { | |
console.debug('[safe-invoke] discarding exception', e, ...argArray); | |
} | |
} | |
}, | |
}); | |
const humanize = (camelCase: string) => | |
camelCase | |
.replace(/([A-Z])/g, match => ` ${match}`) | |
.replace(/^./, match => match.toUpperCase()) | |
.trim(); | |
const valid = [ | |
'shadow-sm', | |
'focus:ring-indigo-500', | |
'focus:border-indigo-500', | |
'block', | |
'w-full', | |
'sm:text-sm', | |
'border-gray-300', | |
'rounded-md', | |
]; | |
const invalid = [ | |
'block', | |
'w-full', | |
'pr-10', | |
'border-red-300', | |
'text-red-900', | |
'placeholder-red-300', | |
'focus:outline-none', | |
'focus:ring-red-500', | |
'focus:border-red-500', | |
'sm:text-sm', | |
'rounded-md', | |
]; | |
export default class ActiveField extends PureComponent<ActiveFieldProps & UseFieldConfig<any>> { | |
private readonly inputRef: RefObject<any> = createRef(); | |
componentWillUnmount() { | |
// preact doesn't use synthetic DOM events like react. Unmounting input elements can be problematic. | |
// handlers are sunk directly into the dom (as opposed to react's synthetic event system serving as a middleman) | |
// -- blur all inputRefs before unmount, trap errors resulting from the invocation -- | |
safeInvoke(this, ['inputRef', 'current', 'blur']); | |
} | |
render() { | |
const { | |
attribute, | |
description, | |
placeholder, | |
autoComplete, | |
type = 'text', | |
label = humanize(attribute), | |
...rest | |
} = this.props; | |
return ( | |
<Field name={attribute} type={type} {...rest}> | |
{(props: any) => { | |
// @formatter:off | |
// prettier-ignore | |
// eslint-ignore | |
const showError = props.meta.touched && ( | |
( | |
props.meta.submitError && !props.meta.modifiedSinceLastSubmit | |
? `${label} ${props.meta.submitError}` | |
: props.meta.error | |
) | |
|| undefined | |
); | |
// eslint-ignore | |
// prettier-ignore | |
// @formatter:off | |
return ( | |
<div className={'mt-5'}> | |
<label | |
htmlFor={attribute} | |
className={`block text-sm font-medium text-${!!showError ? 'red' : 'gray'}-700`} | |
> | |
{label} | |
</label> | |
<div className="mt-1 relative rounded-md shadow-sm"> | |
<input | |
{...(props.input as any)} | |
className={[...(!!showError ? invalid : valid)].filter(Boolean).join(' ')} | |
autoComplete={_.isString(autoComplete) ? autoComplete : _.get(props.input, 'autoComplete')} | |
placeholder={placeholder} | |
aria-invalid={!!showError ? 'true' : undefined} | |
aria-describedby={ | |
!!showError ? `${attribute}-error` : !!description ? `${attribute}-description` : undefined | |
} | |
ref={this.inputRef} | |
/> | |
{!showError && description && ( | |
<p className="mt-2 text-sm text-gray-500" id={`${attribute}-description`}> | |
{description} | |
</p> | |
)} | |
{!!showError && ( | |
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> | |
<ExclamationCircleIcon className="h-5 w-5 text-red-500" aria-hidden="true" /> | |
</div> | |
)} | |
</div> | |
{!!showError && ( | |
<p className="mt-2 text-sm text-red-600" id={`${attribute}-error`}> | |
{showError} | |
</p> | |
)} | |
</div> | |
); | |
}} | |
</Field> | |
); | |
// @formatter:on | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment