Skip to content

Instantly share code, notes, and snippets.

@quanglochuynh
Created December 4, 2024 05:09
Show Gist options
  • Save quanglochuynh/ac29829595e5632cb4d3bf666b1e0356 to your computer and use it in GitHub Desktop.
Save quanglochuynh/ac29829595e5632cb4d3bf666b1e0356 to your computer and use it in GitHub Desktop.
React Hook Form components
import { ReactNode } from 'react';
import {
Controller,
ControllerRenderProps,
FieldValues,
useController,
UseFormReturn,
} from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export interface IControlledFormProps {
control: UseFormReturn<any>['control'];
label?: string;
description?: string | ReactNode;
name: string;
optional?: boolean;
className?: string;
}
export interface IBaseFormProps extends IControlledFormProps {
render: (field: ControllerRenderProps<FieldValues, string>) => JSX.Element;
}
export const BaseForm = (props: IBaseFormProps) => {
const { control, label, description, name, optional, className } = props;
const controller = useController({ name, control });
const error = controller.fieldState.error?.message;
return (
<Controller
control={control}
name={name}
render={({ field }) => (
<div className={twMerge('w-full', className)}>
{label && (
<span className='flex items-center'>
<span
className={twMerge(
'mb-1 text-sm font-medium',
error ? 'text-red-500' : 'text-zinc-700'
)}
>
{label}
</span>
{optional && (
<span className='text-xs font-light text-gray-400'>
&nbsp;( Không bắt buộc )
</span>
)}
</span>
)}
{props.render(field)}
{description && (
<p className='mt-1 text-xs text-zinc-400'>{description}</p>
)}
{error && (
<p className='mt-2 truncate text-xs text-red-500'>{error}</p>
)}
</div>
)}
/>
);
};
import { BaseForm, IControlledFormProps } from '@/components/form/base-form';
import ComboBox from '@/components/ui/combo-box';
import { useDisclosure } from '@/hooks';
import { ReactNode } from 'react';
interface Props extends IControlledFormProps {
options?: {
label: ReactNode;
value: string;
}[];
placeholder?: string;
loading?: boolean;
initValue?: string;
disabled?: boolean;
}
export default function ComboBoxForm({
options = [],
placeholder,
loading,
initValue,
disabled,
className,
description,
optional,
label,
name,
control,
}: Props) {
const disclosure = useDisclosure();
return (
<BaseForm
control={control}
name={name}
label={label}
description={description}
optional={optional}
className={className}
render={({ onChange, value, onBlur, name }) => (
<ComboBox
name={name}
options={options}
value={value}
onChange={onChange}
placeholder={placeholder}
initValue={initValue}
loading={loading}
disabled={disabled}
onBlur={onBlur}
disclosure={disclosure}
/>
)}
/>
);
}
import { BaseForm } from '@/components/common/form/base-form';
import { NumberInputProps } from '@/components/common/form/input/number.input';
import { Input, InputProps } from '@/components/ui/input';
import React from 'react';
import { UseFormReturn } from 'react-hook-form';
interface FloatInputFormProps extends InputProps, NumberInputProps {
control: UseFormReturn<any>['control'];
label?: string;
description?: string | React.ReactNode;
name: string;
optional?: boolean;
}
export default function FloatInputForm({
control,
name,
label,
description,
optional,
className,
...rest
}: FloatInputFormProps) {
const handleChange = (
event: React.ChangeEvent<HTMLInputElement>,
onChange: (value: string) => void
) => {
const newValue = event.target.value.replace(/[^0-9.]/g, '');
onChange(newValue);
};
const onBlur = (event: any, onChange: (value: number) => void) => {
onChange(parseFloat(event.target.value));
};
return (
<BaseForm
control={control}
name={name}
label={label}
description={description}
optional={optional}
className={className}
render={(field) => {
console.log('field', field.value);
return (
<Input
{...rest}
name={field.name}
value={String(field.value || 0)}
onChange={(e) => handleChange(e, field.onChange)}
onBlur={(e) => {
onBlur(e, field.onChange);
field.onBlur();
}}
ref={field.ref}
/>
);
}}
/>
);
}
import { BaseForm } from '@/components/form/base-form';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { X } from 'lucide-react';
import { useRef } from 'react';
import { UseFormReturn } from 'react-hook-form';
type Props = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
control: UseFormReturn<any>['control'];
name: string;
label: string;
className?: string;
placeholder?: string;
description?: string;
};
export default function InputArrayForm({
control,
label,
name,
className,
description,
placeholder,
}: Props) {
const addRef = useRef<HTMLInputElement>(null);
const confirmRef = useRef<HTMLButtonElement>(null);
return (
<BaseForm
control={control}
name={name}
label={label}
className={className}
description={description}
render={({ value, onChange, onBlur }) => {
return (
<div className='w-full items-center gap-2 rounded-lg border border-gray-300 p-2 focus:border-blue-400 focus:outline-none'>
<div className='flex flex-wrap items-center gap-1'>
{value?.map((item: string, index: number) => (
<span key={index}>
<Badge variant={'default'} className='mb-2 px-3 py-1'>
{item}
<span
className='ms-2 rounded-full border-2 border-primary-foreground'
onClick={() => {
onChange(
value.filter((_: any, i: number) => i !== index)
);
}}
>
<X size={12} />
</span>
</Badge>
</span>
))}
</div>
<div className='flex gap-2'>
<Input
placeholder={placeholder}
ref={addRef}
onBlur={onBlur}
className='h-9 rounded-sm'
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (!addRef.current?.value) return;
confirmRef.current?.click();
}
}}
/>
<Button
type='button'
size={'sm'}
ref={confirmRef}
onClick={() => {
if (!addRef.current?.value) return;
onChange([...value, addRef.current?.value || '']);
addRef.current!.value = '';
}}
>
Thêm
</Button>
</div>
</div>
);
}}
/>
);
}
import { BaseForm } from '@/components/form/base-form';
import { Input, InputProps } from '@/components/ui/input';
import { UseFormReturn } from 'react-hook-form';
interface InputFormProps extends InputProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
control: UseFormReturn<any>['control'];
label?: string;
description?: string | React.ReactNode;
name: string;
optional?: boolean;
}
export function InputForm(props: InputFormProps) {
const { control, label, description, name, className, optional, ...rest } =
props;
return (
<BaseForm
control={control}
name={name}
label={label}
description={description}
className={className}
optional={optional}
render={(field) => <Input {...rest} {...field} />}
/>
);
}
import { BaseForm } from '@/components/form/base-form';
import {
NumberInput,
NumberInputProps,
} from '@/components/form/input/number.input';
import { InputProps } from '@/components/ui/input';
import { numberWithCommas } from '@/lib/currency.helper';
import { UseFormReturn } from 'react-hook-form';
interface NumberInputFormProps extends InputProps, NumberInputProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
control: UseFormReturn<any>['control'];
label?: string;
description?: string | React.ReactNode;
name: string;
optional?: boolean;
separator?: boolean;
}
export default function NumberInputForm({
control,
name,
label,
description,
optional,
className,
separator = true,
...rest
}: NumberInputFormProps) {
const handleChange = (
event: React.ChangeEvent<HTMLInputElement>,
onChange: (value: number) => void
) => {
// Remove non-numeric characters
const newValue = parseFloat(event.target.value.replace(/[^0-9.]/g, ''));
if (!isNaN(newValue)) {
onChange(newValue);
} else {
onChange(0);
}
};
return (
<BaseForm
control={control}
name={name}
label={label}
description={description}
optional={optional}
className={className}
render={(field) => (
<NumberInput
{...rest}
name={field.name}
value={separator ? numberWithCommas(field.value) : field.value}
onChange={(e) => handleChange(e, field.onChange)}
onBlur={field.onBlur}
ref={field.ref}
/>
)}
/>
);
}
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface NumberInputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
rightAddOn?: string;
}
const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
({ className, rightAddOn, ...props }, ref) => {
return (
<div className={cn('relative')}>
<input
type='text'
ref={ref}
{...props}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-transparent py-2 pl-3 pr-14 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
/>
{rightAddOn && (
<span className='absolute right-0 top-0 flex h-full items-center rounded-br-md rounded-tr-md bg-input bg-zinc-100 px-2 text-zinc-500'>
{rightAddOn}
</span>
)}
</div>
);
}
);
NumberInput.displayName = 'Input';
export { NumberInput };
import { BaseForm } from '@/components/form/base-form';
import { Textarea } from '@/components/ui/textarea';
import { UseFormReturn } from 'react-hook-form';
type Props = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
control: UseFormReturn<any>['control'];
name: string;
label?: string;
description?: string;
placeholder?: string;
className?: string;
disabled?: boolean;
optional?: boolean;
};
export default function ParagraphInputForm({
control,
name,
description,
placeholder,
className,
disabled,
optional,
label,
}: Props) {
return (
<BaseForm
control={control}
name={name}
description={description}
label={label}
optional={optional}
className={className}
render={(field) => (
<Textarea {...field} placeholder={placeholder} disabled={disabled} />
)}
/>
);
}
import SpinnerScreen from '@/components/common/spinner/spinner-screen';
import { FormControl } from '@/components/ui/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { getKey } from '@/lib';
import { ReactNode } from 'react';
import { BaseForm, IControlledFormProps } from './base-form';
interface ISelectFormProps extends IControlledFormProps {
options?: {
label: ReactNode;
value: string;
}[];
placeholder?: string;
loading?: boolean;
initValue?: string;
}
export const SelectForm = (props: ISelectFormProps) => {
const {
control,
label,
description,
name,
optional,
options,
placeholder,
className,
loading,
initValue,
} = props;
return (
<BaseForm
control={control}
name={name}
label={label}
description={description}
optional={optional}
className={className}
render={(field) => {
const currentOption = options?.find(
(option) => option.value === field.value
);
return (
<Select
onValueChange={(v) => {
if (v === 'null') {
field.onChange(null);
return;
}
field.onChange(v);
}}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue>
{currentOption?.label ||
initValue ||
(!optional ? placeholder : 'Không chọn')}
</SelectValue>
</SelectTrigger>
</FormControl>
<SelectContent>
{loading && <SpinnerScreen className='h-auto' />}
{options?.map((option) => (
<SelectItem
className='cursor-pointer'
key={getKey('select-option', option.value)}
value={option.value}
>
{option.label}
</SelectItem>
))}
{optional && (
<SelectItem key='select-option-optional' value='null'>
Không chọn
</SelectItem>
)}
</SelectContent>
</Select>
);
}}
/>
);
};
import { BaseForm } from '@/components/form/base-form';
import { Slider } from '@/components/ui/slider';
import { SliderProps } from '@radix-ui/react-slider';
import { UseFormReturn } from 'react-hook-form';
interface Props extends SliderProps {
control: UseFormReturn<any>['control'];
label: string;
name: string;
description?: string;
}
export default function SliderForm({
control,
label,
name,
description,
className,
...rest
}: Props) {
return (
<BaseForm
control={control}
name={name}
label={label}
description={description}
className={className}
render={({ value, onChange }) => (
<div className='flex'>
<Slider
value={[value]}
onValueChange={(e) => {
onChange(e[0]);
}}
{...rest}
/>
<span className='ml-2'>{value}%</span>
</div>
)}
/>
);
}
import { BaseForm } from '@/components/common/form/base-form';
import { Switch } from '@/components/ui/switch';
import { SwitchProps } from '@radix-ui/react-switch';
import { UseFormReturn } from 'react-hook-form';
interface Props extends SwitchProps {
control: UseFormReturn<any>['control'];
name: string;
label?: string;
description?: string;
}
export default function SwitchForm({
control,
name,
label,
description,
...rest
}: Props) {
return (
<BaseForm
control={control}
name={name}
label={label}
description={description}
render={({ value, onChange, ...fields }) => (
<Switch
checked={value}
onCheckedChange={onChange}
{...fields}
{...rest}
/>
)}
/>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment