Open lucafaggianelli opened 3 months ago
I already integrated tremor with RHF, here you have the code in case you find it useful. The key is to use the Controller class:
SelectInput.tsx
'use client'
import { Select, SelectItem, SelectProps, SelectItemProps } from '@tremor/react'
import {
BaseFormInputProps,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from 'components/Primitives/Form'
export interface SelectOptionProps extends SelectItemProps {
label: string
}
export { Select, SelectItem }
type BaseSelectProps = BaseFormInputProps & Omit<SelectProps, 'children'>
export interface SelectInputProps extends BaseSelectProps {
options: SelectOptionProps[]
}
export default function SelectInput(props: SelectInputProps) {
const { name, control, label, description, defaultValue, options, ...rest } =
props
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value?.toString() || ''}
{...rest}
>
{options.map((option) => (
<SelectItem
key={option.value}
value={option.value}
icon={option.icon}
>
{option.label}
</SelectItem>
))}
</Select>
</FormControl>
<FormDescription>{description}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)
}
SearchSelect.tsx
'use client'
import {
SearchSelect,
SearchSelectProps as TremorSearchSelectProps,
SearchSelectItem,
SearchSelectItemProps,
} from '@tremor/react'
import {
BaseFormInputProps,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from 'components/Primitives/Form'
import {
Avatar,
AvatarImage,
AvatarFallback,
} from 'components/Primitives/Avatar'
export { SearchSelect, SearchSelectItem }
export type {
TremorSearchSelectProps as SearchSelectProps,
SearchSelectItemProps,
}
interface SearchSelectOptionProps extends SearchSelectItemProps {
label: string
picture?: string | null
}
type SearchSelectProps = Omit<TremorSearchSelectProps, 'children'> &
BaseFormInputProps & { enableIconFallback?: boolean }
export interface SearchSelectInputProps extends SearchSelectProps {
options: SearchSelectOptionProps[]
}
const OptionIcon = ({ option }: { option: SearchSelectOptionProps }) => {
return (
<Avatar className="w-7 h-7 mr-4">
<AvatarImage src={option.picture as string} />
<AvatarFallback>{option.label}</AvatarFallback>
</Avatar>
)
}
export default function SearchSelectInput(props: SearchSelectInputProps) {
const {
name,
control,
label,
description,
placeholder,
enableIconFallback,
options,
onValueChange,
onSearchValueChange,
enableClear = true,
} = props
const onChange = (
newValue: string,
onFormChange: (...event: any[]) => void
) => {
onFormChange(newValue)
onValueChange?.(newValue)
}
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<SearchSelect
placeholder={placeholder}
defaultValue={field.value}
onValueChange={(value) => onChange(value, field.onChange)}
onSearchValueChange={onSearchValueChange}
enableClear={enableClear}
>
{options.map((option) => (
<SearchSelectItem
key={option.value}
value={option.value}
icon={
option.picture || enableIconFallback
? () => <OptionIcon option={option} />
: undefined
}
>
{option.label}
</SearchSelectItem>
))}
</SearchSelect>
</FormControl>
<FormDescription>{description}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)
}
MultiSelect.tsx
'use client'
import { useState } from 'react'
import { useFormContext } from 'react-hook-form'
import {
MultiSelect,
MultiSelectItem,
MultiSelectProps as TremorMultiSelectProps,
MultiSelectItemProps,
} from '@tremor/react'
import {
BaseFormInputProps,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from 'components/Primitives/Form'
// This is used to control how the multiselect input behaves
export const multiSelectWithAllOptionChangeTransformer =
(allOptionsValue: string) => (prev: string[], newValues: string[]) => {
// If there is no selection, reset to all option
if (newValues.length === 0) return [allOptionsValue]
// If we have selected the all option while it wasnt selected before, reset to all
if (!prev.includes(allOptionsValue) && newValues.includes(allOptionsValue))
return [allOptionsValue]
// If we had all selected but now there is more selection, then unselect the all option
if (prev.includes(allOptionsValue) && newValues.length > 1) {
return newValues.filter((v: string) => v !== allOptionsValue)
}
return newValues
}
interface MultiSelectOptionProps extends MultiSelectItemProps {
label: string
}
type MultiSelectProps = Omit<TremorMultiSelectProps, 'children'> &
BaseFormInputProps
export interface MultiSelectInputProps extends MultiSelectProps {
options: MultiSelectOptionProps[]
allOptionsValue?: string
}
export default function MultiSelectInput(props: MultiSelectInputProps) {
const {
name,
control,
label,
description,
options,
allOptionsValue,
...rest
} = props
const { getValues } = useFormContext()
const [values, setValues] = useState<string[]>(getValues(name) || [])
const onSelectionChange = (
newValues: string[],
onChange: (...event: any[]) => void
) => {
const newSelection = allOptionsValue
? multiSelectWithAllOptionChangeTransformer(allOptionsValue)(
values,
newValues
)
: newValues
setValues(newSelection)
onChange(newSelection)
}
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<MultiSelect
value={values}
defaultValue={values}
placeholderSearch="Buscar..."
onValueChange={(newValues) =>
onSelectionChange(newValues, field.onChange)
}
{...rest}
>
{options.map((option) => (
<MultiSelectItem key={option.value} value={option.value}>
{option.label}
</MultiSelectItem>
))}
</MultiSelect>
</FormControl>
<FormDescription>{description}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)
}
Hey @angelhodar thanks!
What problem does this feature solve?
React Hook Form is a convenient library to handle forms and with native HTML input elements the integration is flawless, actually also with most of Tremor input components the integration is flawless, i.e. with the
TextInput
I can just do (which is what I would do for the<input>
element):Though the same method doesn't work for the
Select
component, this is the workaround I used:What does the proposed API look like?
It would be nice to make the
Select
component compatible with RHF so the integration would be: