ui icon indicating copy to clipboard operation
ui copied to clipboard

Cannot use value other than string on Select's onValueChange

Open niebag opened this issue 1 year ago • 16 comments

Form schema and hook?

const FormSchema = z.object({
	theme: z.enum(['light', 'dark', 'system']),
});

	const form = useForm<z.infer<typeof FormSchema>>({
		resolver: zodResolver(FormSchema),
		defaultValues: {
			theme: theme as z.infer<typeof FormSchema>['theme'],
		},
	});

Form:

						<FormField
							control={form.control}
							name='theme'
							render={({ field }) => (
								<FormItem>
									<FormLabel>Thema</FormLabel>
										<Select onValueChange={field.onChange} defaultValue={field.value}>

Error:

Type '(event: "light" | "dark" | "system" | ChangeEvent<Element>) => void' is not assignable to type '(value: string) => void'.
  Types of parameters 'event' and 'value' are incompatible.
    Type 'string' is not assignable to type '"light" | "dark" | "system" | ChangeEvent<Element>'.ts(2322)

The onValueChange is typed as such:

export interface SelectProps {
     ...
    onValueChange?(value: string): void;

How can I make this work with enum?

niebag avatar Jun 29 '23 14:06 niebag

@niebag Apparently it's a problem related to react-hook-form v7.45.0, which implemented a stricter type for the onChange callback. Version 7.45.1 reverted this feature so it should work normally now.

Let me know.

dan5py avatar Jun 29 '23 19:06 dan5py

@niebag Apparently it's a problem related to react-hook-form v7.45.0, which implemented a stricter type for the onChange callback. Version 7.45.1 reverted this feature so it should work normally now.

Let me know.

Hi Daniele, thanks for responding.

I am already running ^7.45.1. My code snippets in the initial description are running ^7.45.1

niebag avatar Jun 30 '23 06:06 niebag

ssame issue here any fixes 🤔

FadiAboMsalam avatar Jul 08 '23 17:07 FadiAboMsalam

define enum

const ThemeEnum = z.enum(["light", "dark", "system"])
type ThemeEnum = z.infer<typeof ThemeEnum>

schema

const formSchema = z.object({
  theme: ThemeEnum.optional(),
})

form field

        <FormField
          control={form.control}
          name="theme"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Theme</FormLabel>
              <Select
                onValueChange={(value) => field.onChange(ThemeEnum.parse(value))}
                defaultValue={field.value}>
                <FormControl>
                  <SelectTrigger>
                    <SelectValue placeholder="Select a theme" />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  {ThemeEnum.options.map((theme) => (
                    <SelectItem key={theme} value={theme}>
                      {theme}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </FormItem>
          )}
        />

sravimohan avatar Jul 09 '23 21:07 sravimohan

Same issue here... So inconvenient...

"react-hook-form": "^7.45.4",

jiapei100 avatar Aug 23 '23 09:08 jiapei100

I think I found a solution for this, in my case I wanted to handle an object as a value, I stringified everything and then I parsed them.

example:

interface SelectFieldProps {
  items: SelectListItem[];
  field: {
    onChange: (value: string) => void;
    onBlur: () => void;
    value: object | undefined; // * define the value type here
    name: string;
    ref: Ref<any>;
  };
}

const SelectField = ({
  items,
  field,
}: SelectFieldProps) => {

  const handleFieldValue = (value: string) => {
    const parsedObject = JSON.parse(value); // *parse the object here

    console.log(parsedObject);
  };

  return (
    <Select onValueChange={handleFieldValue} disabled={disabled}>
      <SelectTrigger>
        <SelectValue placeholder={placeholder} />
      </SelectTrigger>
      <SelectContent>
        <SelectGroup>
          <SelectLabel>{label}</SelectLabel>
              {items.map((item) => (
                <SelectItem
                  key={item.id}
                  value={JSON.stringify(item)} // *stringify the object here
                  textValue={item.name}
                  defaultValue="Select"
                >
                  {disabled ? disabledName : item.name}
                </SelectItem>
              ))}
        </SelectGroup>
      </SelectContent>
    </Select>
  );
};

export default SelectField;

DomVournias avatar Dec 13 '23 14:12 DomVournias

Bump. Silly that this doesn't work.

protzman avatar Jan 23 '24 20:01 protzman

ANNYONE found a solution, I cannot use other types than string

aymanechaaba1 avatar Feb 06 '24 09:02 aymanechaaba1

Same issue when using the Select component

nandramihnea avatar Feb 22 '24 19:02 nandramihnea

Same issue. So frustrated.

ttran189 avatar Feb 24 '24 05:02 ttran189

Very annoying typing here.

davidbonachera avatar Feb 28 '24 18:02 davidbonachera

Components Select, MenuItem from @mui/material don't have such problem. I can easily use number type as value, no typescript compiler issues. It would be really great for shadcn be flexible too

SolomidHero avatar Apr 02 '24 20:04 SolomidHero

Any update about this ?

LaBisquerie avatar Apr 13 '24 15:04 LaBisquerie

Probably not an ideal solution, but I solved this by creating a component that wraps Select and combines it with a label and optional errors. This wrapper has a type guard that accepts a wider type (from conform) in my case, and then checks if it's a string. This does not solve the problem if you actually want to use a different type than string in runtime, but it solved the typing issue for me:

import { Label } from './label.tsx'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
  type SelectProps,
} from './select.tsx'

interface SelectFieldProps {
  options: Array<{ value: string; label: string }>
  labelProps: JSX.IntrinsicElements['label']
  selectProps: Omit<SelectProps, 'defaultValue'> & {
    id?: string
    defaultValue?: ConformSelectProps['defaultValue']
    disabled?: boolean
    placeholder?: string
    'aria-invalid'?: boolean
    'aria-describedby'?: string
  }
  errors?: ListOfErrors
  className?: string
}

// We need to check if the defaultValue is of string or undefined,
// because conform-to/react it's `getSelectProps` returns a wider type,
// which is not accepted by Radix SelectPrimitive
function isValidSelectValue(value: unknown): value is string | undefined {
  return typeof value === 'string' || value === undefined
}

export function SelectField({
  options,
  labelProps,
  selectProps,
  errors,
  className,
}: SelectFieldProps) {
  const { placeholder, defaultValue, ...buttonProps } = selectProps
  const fallbackId = React.useId()
  const id = buttonProps.id ?? fallbackId
  const errorId = errors?.length ? `${id}-error` : undefined

  if (!isValidSelectValue(defaultValue)) {
    throw new Error(
      'Please only use string or undefined as defaultValue for SelectField.',
    )
  }

  return (
    <div className={cn('flex flex-col gap-y-2', className)}>
      <Label {...labelProps} htmlFor={id} />
      <Select {...buttonProps} defaultValue={defaultValue}>
        <SelectTrigger
          aria-invalid={selectProps['aria-invalid']}
          aria-describedby={selectProps['aria-describedby']}
        >
          <SelectValue placeholder={placeholder} />
        </SelectTrigger>
        <SelectContent id={id}>
          {options.map(option => (
            <SelectItem key={option.value} value={option.value}>
              {option.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
      {errorId ? <ErrorList id={errorId} errors={errors} /> : null}
    </div>
  )
}

TimoWestland avatar May 14 '24 07:05 TimoWestland