Skip to content

Instantly share code, notes, and snippets.

@ZachHaber
Last active February 4, 2021 06:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ZachHaber/7f443450bb83c92d2c8dbc74dd936f2a to your computer and use it in GitHub Desktop.
Save ZachHaber/7f443450bb83c92d2c8dbc74dd936f2a to your computer and use it in GitHub Desktop.
Field Wrappers for React-hook-form and Semantic UI React
import { Controller, useForm } from 'react-hook-form';
import { Form, Button, Checkbox } from 'semantic-ui-react';
import WField from './WField';
export function Usage() {
const { handleSubmit, control, errors } = useForm<{
input: string;
checkbox: boolean;
textarea: string;
select: string;
}>({});
const onSubmit = handleSubmit(async (values) => {
console.log(values);
});
const required = 'Please fill out this field';
return (
<Form onSubmit={onSubmit} noValidate>
<h3>Inquiry Form</h3>
<WField.Input
name="input"
errors={errors}
label="This is an example"
control={control}
defaultValue=""
rules={{ required }}
/>
<Form.Field style={{ alignSelf: 'center' }}>
<Controller
render={({ onChange, value, ...data }) => (
<Checkbox
{...data}
onChange={(e, data) => onChange(data.checked)}
label="Checkbox"
toggle
/>
)}
name="checkbox"
control={control}
defaultValue={false}
/>
</Form.Field>
<WField.Textarea
name="textarea"
errors={errors}
control={control}
defaultValue=""
label="Text Area"
/>
<WField.Select
name="select"
errors={errors}
label="Select"
control={control}
rules={{ required }}
options={[{ key: 'Test', value: 'test', text: 'Test' }]}
/>
<div>
<Button type="submit" primary>
Submit
</Button>
</div>
</Form>
);
}
import clsx from 'clsx';
import {
CSSProperties,
FC,
forwardRef,
isValidElement,
ReactElement,
ReactNode,
RefObject,
useImperativeHandle,
useRef,
} from 'react';
import {
FieldError,
FieldErrors,
get,
useController,
UseControllerOptions,
} from 'react-hook-form';
import {
Input,
Label,
StrictFormFieldProps,
StrictInputProps,
StrictTextAreaProps,
TextArea,
} from 'semantic-ui-react';
import Select, {
StrictSelectProps,
} from 'semantic-ui-react/dist/commonjs/addons/Select';
type SemanticWidths = Required<StrictFormFieldProps>['width'];
interface Focusable {
focus: () => void;
}
const FieldWidths: Record<SemanticWidths, string> = Object.fromEntries(
[
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'ten',
'eleven',
'twelve',
'thirteen',
'fourteen',
'fifteen',
'sixteen',
].flatMap((val, idx) => [
[idx + 1, val],
[val, val],
])
) as any;
export type FieldProps = Omit<
StrictFormFieldProps,
'as' | 'content' | 'control' | 'error' | 'type' | 'id'
> & {
style?: CSSProperties;
errors?: FieldErrors;
name: string;
errorPointing?: 'above' | 'below' | 'left' | 'right';
id?: string;
label?: StrictFormFieldProps['label'] | ReactElement;
};
type WFieldProps = {
style?: CSSProperties;
className?: string;
children?: ReactNode;
fieldStyle?: CSSProperties;
fieldClassName?: string;
[key: string]: any;
} & Omit<FieldProps, 'required'>;
function errorsToString(errors: FieldErrors | undefined, name: string) {
const error: FieldError = get(errors, name);
return error && (error.message || error.type);
}
const Field = forwardRef<HTMLDivElement, FieldProps & { children: ReactNode }>(
(
{
children,
width,
inline,
disabled,
errors,
label,
required,
name,
className,
style,
errorPointing = 'above',
id,
},
ref
) => {
const error = errorsToString(errors, name);
const widthClass = width && FieldWidths[width];
const errorLabel = error && (
<Label
prompt
pointing={errorPointing}
id={id && `${id}-error-message`}
role="alert"
aria-atomic
>
{error}
</Label>
);
const errorLabelBefore =
(errorPointing === 'below' || errorPointing === 'right') && errorLabel;
const errorLabelAfter =
(errorPointing === 'above' || errorPointing === 'left') && errorLabel;
const labelProps = typeof label === 'string' ? { children: label } : label;
return (
<div
ref={ref}
className={clsx('field', className, {
disabled,
error,
inline,
required,
[`${widthClass} wide`]: widthClass,
})}
style={style}
>
{isValidElement(label) ? label : <label htmlFor={id} {...labelProps} />}
{errorLabelBefore}
{children}
{errorLabelAfter}
</div>
);
}
);
export type WSelectProps = UseControllerOptions &
Omit<StrictSelectProps, 'error' | 'value' | 'onChange'> &
WFieldProps;
export const WSelect: FC<WSelectProps> = ({
width,
inline,
disabled,
fieldStyle,
fieldClassName,
label,
errors,
errorPointing,
name,
rules,
defaultValue,
control,
onFocus,
children,
...props
}) => {
const {
field: { ref, onChange, ...inputProps },
// meta: { invalid },
} = useController({ name, rules, onFocus, defaultValue, control });
const innerRef = useRef<HTMLDivElement>(null);
useImperativeHandle(
ref as RefObject<Focusable>,
() => ({
focus() {
innerRef.current?.querySelector('input')?.focus();
},
}),
[]
);
return (
<Field
ref={innerRef}
width={width}
required={rules?.required}
inline={inline}
style={fieldStyle}
className={fieldClassName}
label={label}
errors={errors}
disabled={disabled}
errorPointing={errorPointing}
name={name}
id={props.id}
>
<Select
{...props}
{...inputProps}
onChange={(evt, data) => onChange(data.value)}
/>
{children}
</Field>
);
};
export const WInput: FC<
UseControllerOptions &
Omit<StrictInputProps, 'error' | 'value' | 'onChange'> &
WFieldProps
> = ({
width,
required,
inline,
fieldStyle,
fieldClassName,
label,
errors,
disabled,
errorPointing,
name,
rules,
defaultValue,
control,
onFocus,
children,
...props
}) => {
const {
field: { ref, onChange, ...inputProps },
} = useController({ name, rules, onFocus, defaultValue, control });
return (
<Field
width={width}
required={rules?.required}
inline={inline}
style={fieldStyle}
className={fieldClassName}
label={label}
errors={errors}
errorPointing={errorPointing}
name={name}
id={props.id}
>
<Input {...props} {...inputProps} onChange={onChange} ref={ref} />
{children}
</Field>
);
};
export const WTextArea: FC<
UseControllerOptions &
Omit<StrictTextAreaProps, 'error' | 'value' | 'onChange'> &
WFieldProps
> = ({
width,
required,
inline,
fieldStyle,
fieldClassName,
label,
errors,
disabled,
errorPointing,
name,
rules,
defaultValue,
control,
onFocus,
children,
...props
}) => {
const {
field: { ref, ...inputProps },
} = useController({ name, rules, onFocus, defaultValue, control });
return (
<Field
width={width}
required={rules?.required}
inline={inline}
style={fieldStyle}
className={fieldClassName}
label={label}
errors={errors}
errorPointing={errorPointing}
name={name}
id={props.id}
>
<TextArea ref={ref} {...props} {...inputProps} />
{children}
</Field>
);
};
const WField = {
Select: WSelect,
Input: WInput,
Textarea: WTextArea,
Field,
};
export default WField;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment