Last active
February 4, 2021 06:00
-
-
Save ZachHaber/7f443450bb83c92d2c8dbc74dd936f2a to your computer and use it in GitHub Desktop.
Field Wrappers for React-hook-form and Semantic UI React
This file contains hidden or 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 { 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> | |
); | |
} |
This file contains hidden or 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 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