Skip to content

Instantly share code, notes, and snippets.

@ashfahan
Created July 20, 2023 17:58
Show Gist options
  • Save ashfahan/2953d86202eb8073999126233c479f39 to your computer and use it in GitHub Desktop.
Save ashfahan/2953d86202eb8073999126233c479f39 to your computer and use it in GitHub Desktop.
FormField
import type { HTMLAttributes, ReactNode } from "react"
import type { ControllerRenderProps, FieldError, FieldValues, Path, UseControllerProps } from "react-hook-form"
type ExtraElement<ControllerProps extends FieldValues> = (
values: ControllerRenderProps<ControllerProps, FormFieldProps<ControllerProps>["name"]>
) => ReactNode | ReactNode[]
export type FormFieldChildren<ControllerProps extends FieldValues> = {
error?: FieldError
checked?: ControllerRenderProps["value"]
disabled?: boolean
} & ControllerRenderProps<ControllerProps, Path<ControllerProps>>
export type FormFieldChildrenFn<ControllerProps extends FieldValues> = (
props: FormFieldChildren<ControllerProps>
) => JSX.Element
export interface FormFieldProps<ControllerProps extends FieldValues = FieldValues>
extends UseControllerProps<ControllerProps>,
Omit<HTMLAttributes<HTMLDivElement | HTMLLabelElement>, "defaultValue" | "children" | "onChange"> {
ExtraElement?: ExtraElement<ControllerProps>
label?: string | JSX.Element
infoText?: string | JSX.Element
hideError?: boolean
disabled?: boolean
showDisabledIcon?: boolean
noLabelTag?: boolean
keepErrorSpace?: TransientFormFieldProps["$keepErrorSpace"]
transformChange?: (value: ControllerRenderProps["value"], ...rest: any) => ControllerRenderProps["value"]
transformValue?: (value: ControllerRenderProps["value"]) => ControllerRenderProps["value"]
onChange?: (value: ControllerRenderProps["value"], ...rest: any) => void
children: JSX.Element | FormFieldChildrenFn<ControllerProps>
className?: string
}
export interface TransientFormFieldProps {
$keepErrorSpace?: boolean
}
import { COLORS } from "@ui/colors"
import styled from "styled-components"
import type { TransientFormFieldProps } from "./FormField.interfaces"
export const FieldWrapper = styled.div`
display: block;
width: 100%;
`
export const FieldLabel = styled.div`
display: block;
font-size: 0.875rem;
margin-bottom: 0.4rem;
`
export const Field = styled.div`
position: relative;
display: flex;
flex-direction: column;
`
export const FieldSubtitle = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
`
export const FieldError = styled.div<TransientFormFieldProps>`
display: flex;
color: ${COLORS.ERROR};
transition: height 0.3s ease-in-out;
min-height: ${({ $keepErrorSpace }) => ($keepErrorSpace ? "1.5rem" : "")};
margin-top: ${({ children }) => (children ? ".25rem" : "")};
height: ${({ children }) => (children ? "" : 0)};
> img {
margin-right: 0.25rem;
}
`
export const FieldInfo = styled.div`
margin-top: 0.25rem;
font-style: italic;
color: ${COLORS.MUTED};
`
import info from "@assets/images/info-icon.svg";
import lock from "@assets/images/lock-150.svg";
import type { ControllerRenderProps, FieldValues } from "react-hook-form";
import { Controller } from "react-hook-form";
import type { FormFieldProps } from "./FormField.interfaces";
import { Field, FieldError, FieldInfo, FieldLabel, FieldSubtitle, FieldWrapper } from "./FormField.styled";
export const FormField = <T extends FieldValues = FieldValues>(props: FormFieldProps<T>): JSX.Element => {
const {
ExtraElement,
className,
name,
rules,
shouldUnregister,
defaultValue,
control,
label,
children,
infoText,
transformChange,
transformValue,
onChange,
hideError,
noLabelTag,
keepErrorSpace,
disabled,
showDisabledIcon = true,
...rest
} = props;
return (
<Controller
name={name}
control={control}
defaultValue={defaultValue}
rules={rules}
shouldUnregister={shouldUnregister}
render={({ field, fieldState: { error } }) => {
const change: ControllerRenderProps["onChange"] = (event, ...rest: any[]) => {
const prev = field.value;
let val = event;
if (event.target) {
const { type, checked, value, files } = event.target;
val = type === "checkbox" ? checked : type === "file" ? files : value;
}
onChange?.(val, prev, ...rest);
// @ts-ignore ts(2556)
return field.onChange(transformChange ? transformChange(val, prev, ...rest) : val, ...rest);
};
return (
<FieldWrapper as={noLabelTag ? "div" : "label"} {...rest}>
{label && <FieldLabel>{label}</FieldLabel>}
<Field>
{typeof children === "function"
? children({
...field,
error,
disabled,
checked: transformValue ? transformValue(field.value) : field.value,
value: transformValue ? transformValue(field.value) : field.value,
onChange: change,
})
: {
...children,
props: {
...field,
error,
disabled,
checked: transformValue ? transformValue(field.value) : field.value,
value: transformValue ? transformValue(field.value) : field.value,
onChange: change,
...children.props,
},
}}
{disabled && showDisabledIcon && (
<div className="absolute inset-y-0 right-0 flex items-center justify-center p-2">
<img src={lock} alt="lock" className="mr-1 h-3 w-3" />
</div>
)}
</Field>
<FieldSubtitle>
{!hideError && (
<FieldError $keepErrorSpace={keepErrorSpace}>
{error?.message && (
<>
<img src={info} alt="" />
{error.message}
</>
)}
</FieldError>
)}
{infoText && <FieldInfo>{infoText}</FieldInfo>}
</FieldSubtitle>
{ExtraElement?.(field)}
</FieldWrapper>
);
}}
/>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment