Created
December 4, 2024 05:09
-
-
Save quanglochuynh/ac29829595e5632cb4d3bf666b1e0356 to your computer and use it in GitHub Desktop.
React Hook Form components
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 { 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'> | |
( 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> | |
)} | |
/> | |
); | |
}; |
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 { 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} | |
/> | |
)} | |
/> | |
); | |
} |
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 { 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} | |
/> | |
); | |
}} | |
/> | |
); | |
} |
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 { 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> | |
); | |
}} | |
/> | |
); | |
} |
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 { 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} />} | |
/> | |
); | |
} |
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 { 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} | |
/> | |
)} | |
/> | |
); | |
} |
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 * 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 }; |
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 { 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} /> | |
)} | |
/> | |
); | |
} |
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 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> | |
); | |
}} | |
/> | |
); | |
}; |
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 { 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> | |
)} | |
/> | |
); | |
} |
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 { 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