shadcn-ui / ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
https://ui.shadcn.com
MIT License
69.39k stars 4.14k forks source link

Select with react-hook-form Control #1253

Open rogueturnip opened 1 year ago

rogueturnip commented 1 year ago

Hi! I'm having a bit of an issue with the Select setup using react-hook-form.

What I'm trying to do is display a form with default values, there is a button to populate the content from an api. When this api is called all the fields in the form should update to the data from the api.

This works fine for all the fields except for Select. I set it up with a plain html select and it's working but I can't seem to get it to work with the Select component from shadcn.

Here is a snip of the parts I'm confused by.

Here is the form setup

  const npcForm = useForm<z.infer<typeof npcFullRandomZodSchema>>({
    resolver: zodResolver(npcFullRandomZodSchema),
    values: npc,
    defaultValues: {
      class: 'none',

    },
    resetOptions: {},
  });

Here is the Select setup:
      <Controller
        name="class"
        control={npcForm.control}
        render={({ field }) => (
          <FormItem>
            <FormLabel>Class</FormLabel>
            <Select
              onValueChange={field.onChange}
              defaultValue={field.value}
            >
              <FormControl>
                <SelectTrigger>
                  <SelectValue />
                </SelectTrigger>
              </FormControl>
              <SelectContent ref={field.ref}>
                {dnd5eClasses
                  .sort((a, b) => a.label.localeCompare(b.label))
                  .map((option) => (
                    <SelectItem key={option.value} value={option.value}>
                      {option.label}
                    </SelectItem>
                  ))}
              </SelectContent>
            </Select>
          </FormItem>
        )}
      />

And here is the html select that works.


          <Controller
            name="class"
            control={npcForm.control}
            render={({ field }) => (
              <select {...field}>
                {dnd5eClasses
                  .sort((a, b) => a.label.localeCompare(b.label))
                  .map((option) => (
                    <option key={option.value} value={option.value}>
                      {option.label}
                    </option>
                  ))}
              </select>
            )}
          />

I've tried using FormField in addition to just the controller but can't figure out what I'm missing.  I'm sure it's simple but there isn't anything I can find related in docs to help.

Thanks!
rogueturnip commented 1 year ago

I tested with react-select and it works also. So it's either a bug in the Select or I'm not sure where the "ref" goes :)

yousihy commented 1 year ago

Thanks for this tip. I am also facing a similar problem. Everything works for me, but it seems each time I select a value, it is not showing in the SelectValue control. It is just showing empty. It will only work if I manually provided the SelectItem manually without using dynamically generated values. Your solution seems to work for now. I hope this issue will be fixed soon because now I have an odd control that does not match the overall theme of the app.

joaom00 commented 1 year ago

@rogueturnip try use value={field.value} instead of defaultValue={field.value}

yousihy commented 1 year ago

@rogueturnip try use value={field.value} instead of defaultValue={field.value}

I did. It's not working. Can you check my code sample? sample

joaom00 commented 1 year ago

Hey @rogueturnip, if your issue is the same as @yousihy, make sure the value prop of SelectItem is a string Codesandbox

adnanalbeda commented 7 months ago

I'm having the same issue with Input. It doesn't catch the register field result.

Simply, I can't do this:

<Input {...register("email")} />

UPDATE

I figured the problem. I must use RHF Controller component to make custom inputs work below it.

leandiazz commented 6 months ago

can u share how did you used RHF Controller component please

nelwincatalogo commented 5 months ago

Here's how i solved this issue:

  1. create a state that will notify that you already updated the form

    const [isDone, setIsDone] = useState(false);
  2. After you reset/update the form values, update the state

    reset((v) => ({
        ...v,
        productType: `${product.product_type}`,
        productName: `${item.id}`,
        price: `${product.price}`,
        code: `${product.code}`,
        stocks: `${product.stocks}`,
        stocksWarning: `${product.stocks_warning}`,
        weight: `${product.weight}`,
        unit: `${product.unit}`,
        vatable: `${product.vatable}`,
        description: `${product.description}`,
        status: `${product.status}`,
      }));
      setIsDone(true);
  3. on your form element add key:

    <form key={isDone ? 0 : 1} onSubmit={handleSubmit(onSubmit)}>

This will force re-render the form element together with the select elements that are not updating

adnanalbeda commented 5 months ago

can u share how did you used RHF Controller component please

Same way described in the docs.

See the example section.

motanveer commented 2 months ago

I'm having the same issue. I'm pulling in data and using that to populate various form values. Input and Switch work fine, select does not.

Essentially, I'm able to dynamically update the values of the other fields based on my data. However, Select doesn't update.. it just goes back to it's original state.

I’m encountering an issue with the Select component for the type field. The form correctly retrieves initial values from session storage and uses form.reset to set these values. While form.reset works for setting initial values of other components like Input and Switch, the Select component’s value does not persist and resets to an empty string. Manually setting the value with form.setValue() works for other components but not for the Select component. Despite seeing the correct value being loaded and set in the console logs, the type field value unexpectedly clears, causing inconsistencies in the form state.

I've tried defaultValue={field.value} and value={field.value}

// Create form schema using Zod
const formSchema = z.object({
    name: z.string().min(1, { message: "Name must be at least 1 character long" }),
    type: z.string().min(1),
    notifications: z.boolean()
});

export type FormSchema = z.infer<typeof formSchema>;

// Set Session Storage:
const setSessionStorage = (key: string, formData: FormSchema) => {
    console.log("Storing data in session storage:", formData);
    sessionStorage.setItem(key, JSON.stringify(formData));
};

// Get Session Storage
const getSessionStorage = (key: string) => {
    const data = sessionStorage.getItem(key);
    console.log("Retrieved data from session storage:", data);
    return data ? JSON.parse(data) : null;
};

// OnboardingForm Component
const OnboardingForm = ({ onSubmit }: { onSubmit: (data: FormSchema) => void }) => {
    const [sessionFormData, setSessionFormData] = useState<FormSchema>({
        name: '',
        type: '',
        notifications: false,
    });

    // Create form object and initialize default values using session storage or blank object.
    const form = useForm<FormSchema>({
        resolver: zodResolver(formSchema),
        defaultValues: sessionFormData,
    });

    // Fetch session storage data on mount
    useEffect(() => {
        const storedData = getSessionStorage('onboardingFormData');
        console.log("Retrieved stored data on mount:", storedData);
        if (storedData) {
            setSessionFormData(storedData);
            console.log("Calling form.reset with:", storedData);
            form.reset(storedData); // Reset form with retrieved data
        }
    }, [form]);

    console.log(sessionFormData)

    // Watch the form for any input changes and update SessionStorage
    const formValues = useWatch({
        control: form.control,
    });

    useEffect(() => {
        const formData = form.getValues();
        console.log("Form Values Changed:", formData);
        setSessionStorage('onboardingFormData', formData);
    }, [formValues]);

    return (
        <Card className="mx-auto w-full">
            <CardHeader>
                <h1 className='text-3xl font-semibold'>Welcome!</h1>
                <CardDescription>
                    Let's get some details and then we'll create your account
                </CardDescription>
            </CardHeader>
            <CardContent>
                <Form {...form}>
                    <form className="w-full space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
                        <FormField
                            control={form.control}
                            name="name"
                            render={({ field }) => (
                                <FormItem>
                                    <FormLabel className='pl-2'>Name</FormLabel>
                                    <FormControl>
                                        <Input className='w-full' placeholder="Mo Tanveer" {...field} />
                                    </FormControl>
                                    <FormMessage />
                                </FormItem>
                            )}
                        />

                        <FormField
                            control={form.control}
                            name="type"
                            render={({ field }) => (
                                <FormItem>
                                    <FormLabel className='pl-2'>License Type</FormLabel>
                                    <Select value={field.value} onValueChange={field.onChange}>
                                        <FormControl>
                                            <SelectTrigger >
                                                <SelectValue  placeholder="Select your license type..." />
                                            </SelectTrigger>
                                        </FormControl>
                                        <SelectContent>
                                            <SelectItem value="Road">Road License</SelectItem>
                                            <SelectItem value="Circuit">International Circuit License</SelectItem>
                                            <SelectItem value="Rally">International Rally License</SelectItem>
                                        </SelectContent>
                                    </Select>
                                    <FormMessage />
                                </FormItem>
                            )}
                        />

                        <FormField
                            control={form.control}
                            name="notifications"
                            render={({ field }) => (
                                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
                                    <div className="space-y-0.5">
                                        <FormLabel>
                                            Notifications
                                        </FormLabel>
                                        <FormDescription>
                                            Receive Race Event Notifications?
                                        </FormDescription>
                                    </div>
                                    <FormControl>
                                        <Switch
                                            checked={field.value}
                                            onCheckedChange={field.onChange}
                                        />
                                    </FormControl>
                                </FormItem>
                            )}
                        />
                        <Button className='w-full' type="submit">Next</Button>
                    </form>
                </Form>
            </CardContent>
        </Card>
    );
};
ENGAhmedAbdelfattah commented 2 months ago

use:
<Select onValueChange={field.onChange} {...field}> ....

You can see this in StackOverFlow 

https://stackoverflow.com/questions/75815473/how-can-i-implement-react-hook-form-with-radix-ui-select
k4l3b4 commented 2 months ago

Here's how i solved this issue:

  1. create a state that will notify that you already updated the form
const [isDone, setIsDone] = useState(false);
<form key={isDone ? 0 : 1} onSubmit={handleSubmit(onSubmit)}>

This will force re-render the form element together with the select elements that are not updating

in this case use key={form.watch(<FIELDNAME>)} instead of declaring a separate state

stall84 commented 1 month ago

I understand there are likely work-arounds to this, and it may not even be a bug at all, but user implementation. But for anyone finding this thread I wanted to say I'm experiencing a very similar situation to motanveer above .. In my case however I'm getting the empty string value for the Select value when I navigate back and retrieve the values out of a form.reset() . He's getting his previously entered form values from local / sesh storage and I'm pulling mine out of a context-store and then form.reset(contextFormObj) and it's doing the same confounding thing where all the other shad-cn components like Input or Radio or any other ones (oh by the way .. THANK YOU @shadcn for this exceptionally awesome library) repopulate just fine .. but just the single lone Select value is an empty string .. even though I can see it's value is submitted correctly and is stored in context correctly (Meaning this is really purely a UI issue where we want to show the user that indeed that value is there to their own eyes).

Tweedle2Dum commented 1 month ago

Yeah , i am also encountering a similar issue and unable to make it work. I am using react query to fetch data and i can see all the API fields are populated but when i call form.reset() with the result from the API, the form fields which have select as input contain empty strings, but the rest of the form gets populated.

Oladejiraji commented 1 month ago

I encountered this problem too. It seems that something is firing the onValueChange event with an empty string when the route changes. I just checked if the value being passed onValueChange is an empty string before updating the value. Hopefully we can get a better solution in the future. ` <ShadSelect disabled={!!disabled} onValueChange={(val: string) => { if (val) onChange(val); }} value={value}

`

abhishek-butola commented 1 month ago

Specifying key={field.value} solved the issue for me.

 <FormField
      control={control}
      name={name}
      render={({ field }) => {
        return (
          <FormItem key={field.value} className="w-full">
            <FormLabel>{label}</FormLabel>
            <Select
              onValueChange={(value) => field.onChange(Number(value))}
              value={String(field.value)}
            >
              <FormControl>
                <SelectTrigger>
                  <SelectValue placeholder={placeholder} />
                </SelectTrigger>
              </FormControl>
              <SelectContent>
                {options.map((option) => (
                  <SelectItem key={option.id} value={option.id.toString()}>
                    {option.name}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
            <FormMessage />
          </FormItem>
        );
      }}
    />
tsulatsitamim commented 1 month ago

if i use "" as default value, the first options is always selected when submit with form element before I made a selection Screenshot 2024-08-10 at 01 41 30

stevensiht commented 1 month ago

Specifying key={field.value} solved the issue for me.

 <FormField
      control={control}
      name={name}
      render={({ field }) => {
        return (
          <FormItem key={field.value} className="w-full">
            <FormLabel>{label}</FormLabel>
            <Select
              onValueChange={(value) => field.onChange(Number(value))}
              value={String(field.value)}
            >
              <FormControl>
                <SelectTrigger>
                  <SelectValue placeholder={placeholder} />
                </SelectTrigger>
              </FormControl>
              <SelectContent>
                {options.map((option) => (
                  <SelectItem key={option.id} value={option.id.toString()}>
                    {option.name}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
            <FormMessage />
          </FormItem>
        );
      }}
    />

This fixed the issue for me as well.

HoshangDEV commented 4 weeks ago

For me, I fixed by adding a useState of type any, just put the value of the form field to the useState variable, you don't need the useState variable just put the value to it inside the onValueChange, and it works fine. I don't know why it works but it does some how. If you have multiple select field you can use the same useState to all of them, thats why i said it can be type of any.

Check it: https://codesandbox.io/p/devbox/hoshang-select-jgzsk4

export function SelectForm() {
  // add useState (We don't need the variable value, 
  // We just use setExample inside the onValueChange and that's it.)
  const [example, setExample] = useState<any>();

  const form = useForm<z.infer<typeof FormSchema>>({
    resolver: zodResolver(FormSchema),
  });

  function onSubmit(data: z.infer<typeof FormSchema>) {
    toast({
      title: "You submitted the following values:",
      description: (
        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
        </pre>
      ),
    });
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              {/* modify onValueChange */}
              <Select
                onValueChange={(e) => {
                  // add setExample
                  setExample(e);
                  field.onChange(e);
                }}
                defaultValue={field.value}
              >
                <FormControl>
                  <SelectTrigger>
                    <SelectValue placeholder="Select a verified email to display" />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  <SelectItem value="m@example.com">m@example.com</SelectItem>
                  <SelectItem value="m@google.com">m@google.com</SelectItem>
                  <SelectItem value="m@support.com">m@support.com</SelectItem>
                </SelectContent>
              </Select>
              <FormDescription>
                You can manage email addresses in your{" "}
                <Link href="/examples/forms">email settings</Link>.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
}
cupid20103 commented 3 weeks ago

Please refer to this website. React Hook Form - shadcn-ui

image

DanielSpindler commented 2 weeks ago
<FormField
      control={form.control}
      name={name}
      render={({ field }) => (
        <FormFieldItem label={label} required={required} error={error} className="lg:col-span-2">
          <Select
            onValueChange={(v) => {
              field.onChange(v);
              onChange && onChange(v);
            }}
            defaultValue={field.value}
            name={field.name}
          >
            <SelectTrigger>
              <SelectValue placeholder={placeholder} />
            </SelectTrigger>
            <SelectContent>
              {subjects.map(({ label, key }) => (
                <SelectItem key={key} value={key}>
                  {label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </FormFieldItem>
      )}
    />

FormFieldItem is jus the base FormItem we get from the installation. The difference is it Wraps everything in a since i was looking for an reuseable approach.

This is my solution. The key part is here and what we focus on:

<Select
            onValueChange={(v) => {
              field.onChange(v);
              onChange && onChange(v);
            }}
            defaultValue={field.value}
            name={field.name}
          >

since we cant use everything from the field that the render gives us we gotta use it partly.

the important parts are what the examples already states :

onValueChange -> using field.onChange with the value we get back (since the Select has no onChange) defaultValue -> For the default Value. name -> fixed it for me, the send data had no name before i put it inside.

Important DONT spread with {...field} else we get values that Select doesnt support.

Might be interesting that my solution is build using serverActions, shouldnt make a difference but just fyi.

helmuth99 commented 1 week ago

For me, I fixed by adding a useState of type any, just put the value of the form field to the useState variable, you don't need the useState variable just put the value to it inside the onValueChange, and it works fine. I don't know why it works but it does some how. If you have multiple select field you can use the same useState to all of them, thats why i said it can be type of any.

Check it: https://codesandbox.io/p/devbox/hoshang-select-jgzsk4

export function SelectForm() {
  // add useState (We don't need the variable value, 
  // We just use setExample inside the onValueChange and that's it.)
  const [example, setExample] = useState<any>();

  const form = useForm<z.infer<typeof FormSchema>>({
    resolver: zodResolver(FormSchema),
  });

  function onSubmit(data: z.infer<typeof FormSchema>) {
    toast({
      title: "You submitted the following values:",
      description: (
        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
        </pre>
      ),
    });
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              {/* modify onValueChange */}
              <Select
                onValueChange={(e) => {
                  // add setExample
                  setExample(e);
                  field.onChange(e);
                }}
                defaultValue={field.value}
              >
                <FormControl>
                  <SelectTrigger>
                    <SelectValue placeholder="Select a verified email to display" />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  <SelectItem value="m@example.com">m@example.com</SelectItem>
                  <SelectItem value="m@google.com">m@google.com</SelectItem>
                  <SelectItem value="m@support.com">m@support.com</SelectItem>
                </SelectContent>
              </Select>
              <FormDescription>
                You can manage email addresses in your{" "}
                <Link href="/examples/forms">email settings</Link>.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
}

hey i check your sandbox and it doesnt work on my end.