Skip to content

Instantly share code, notes, and snippets.

@arleighdickerson
Last active July 9, 2023 17:54
Show Gist options
  • Save arleighdickerson/7ada7cc4854316f43288da97f456821c to your computer and use it in GitHub Desktop.
Save arleighdickerson/7ada7cc4854316f43288da97f456821c to your computer and use it in GitHub Desktop.
a reusable "active field" to display validation errors in react-final-form
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