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
58.62k stars 3.2k forks source link

[bug]: Input field not forwarded when it's in another wrapper/hoc file #3609

Open erwin-gapai opened 2 weeks ago

erwin-gapai commented 2 weeks ago

Describe the bug

Hi,

In my project, I created a wrapper component that consists of FormField along with Input component. The issue is, props that I set in the custom component is not being forwarded to the wrapper component, such as the onChange method.

LoginForm.tsx

const LoginForm = () => {
   const form = useForm<z.infer<typeof LoginSchema>>({
    resolver: zodResolver(LoginSchema),
    defaultValues: {
      email: "",
      password: ""
    }
  });

return (
    <Form {...form}>
        <form
          onSubmit={form.handleSubmit(onSubmit)}
        >
          <FormInput
            control={form.control}
            name="email"
            label="Email"
            type="text"
            placeholder="Email"
            onChange={() => console.log("asdasd")}
            id="email"
            required
          />
      </form>
   </Form>
);

};

FormInput.tsx

const FormInput = (props) => {
   return (
      <FormField
        control={control}
        name={name || ""}
        render={({ field }) => (
             <FormItem>
                 <FormControl>
                   <Input
                     {...field}
                      variant={hasError ? "danger" : "default"}
                      type={type}
                      placeholder={placeholder}
                    />
             </FormItem>   
        )}
      />
   );
}

Anyone knows the issue? Thanks

Affected component/components

Input

How to reproduce

  1. Create a wrapper component that consists of
  2. Display that component in another component

Codesandbox/StackBlitz link

No response

Logs

No response

System Info

MacOS, Arc Browser.

Before submitting

OmarAljoundi commented 2 weeks ago

You are passing the props to FormInput but you are not using it. Since you are using typescript lets re-write the code in strong type kinda way: Try this instead:

LoginForm

'use client'
import { Form } from '@/components/ui/form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { FormInput } from './FormInput'
import { Button } from '@/components/ui/button'

export const LoginSchema = z.object({
  email: z.string(),
  password: z.string(),
})

const LoginForm = () => {
  const form = useForm<z.infer<typeof LoginSchema>>({
    resolver: zodResolver(LoginSchema),
    defaultValues: {
      email: '',
      password: '',
    },
  })

  const onSubmit = (data: z.infer<typeof LoginSchema>) => {
    //Handle submition result
    console.log(data)
  }

  return (
    <div className="p-16 ">
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
          <FormInput
            control={form.control}
            name="email"
            label="Email"
            type="text"
            placeholder="Enter your email"
            id="email"
            required
          />
          <FormInput
            control={form.control}
            name="password"
            label="Password"
            type="password"
            placeholder="Enter your Password"
            id="password"
            required
          />
          <Button type="submit">Login</Button>
        </form>
      </Form>
    </div>
  )
}

export default LoginForm
import { FormControl, FormField, FormItem } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import { Control } from 'react-hook-form'
import { LoginSchema } from './LoginForm'
import { z } from 'zod'

interface FormInputProps {
  control: Control<z.infer<typeof LoginSchema>>
  name: keyof z.infer<typeof LoginSchema>
  type: string
  required?: boolean
  id?: string
  label?: string
  placeholder?: string
  hasError?: boolean
}

export const FormInput = (props: FormInputProps) => {
  const { control, name, type, hasError, placeholder, id, required } = props
  return (
    <FormField
      control={control}
      name={name}
      render={({ field }) => (
        <FormItem>
          <FormControl>
            <Input
              {...field}
              id={id}
              required={required}
              //variant={hasError ? "danger" : "default"} assuming you have configured variants
              className={cn(hasError && 'border-red-500 ring-red-500')} // if you havent configured the variant you can use classes name
              type={type}
              placeholder={placeholder}
            />
          </FormControl>
        </FormItem>
      )}
    />
  )
}

This will insure your props is passed and is being used correctly, here is a screenshot for when submitting the form: image

I hope thats help!

erwin-gapai commented 2 weeks ago

@OmarAljoundi Hey Omar, my current code resembles yours closely. The onSubmit function worked perfectly, but there were some issues with certain props like onChange and onBlur not being forwarded.

I ended up made updates to the FormInput component as shown below:

"use client";
import { Control } from "react-hook-form";
import { Input, InputProps } from "@/components/ui/input";
import {
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage
} from "@/components/ui/form";
import { ChangeEvent, FocusEvent, ReactNode } from "react";

export interface FormInputProps extends InputProps {
  label: string;
  helper?: string | ReactNode;
  control?: Control<any>;
  onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
  onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
  onFocus?: (event: FocusEvent<HTMLInputElement>) => void;
}

const FormInput = ({
  label,
  name,
  type,
  placeholder,
  control,
  required,
  helper,
  onChange,
  onBlur,
  onFocus
}: FormInputProps) => {
  const fieldState = control?.getFieldState(`${name}`);
  const hasError = !!fieldState?.error;
  const hasErrorMessage = !!fieldState?.error?.message;

  return (
    <FormField
      control={control}
      name={name || ""}
      render={({ field }) => (
        <FormItem>
          <FormLabel>
            {label}
            {required && <span className="text-danger-500">*</span>}
          </FormLabel>
          <FormControl>
            <Input
              variant={hasError ? "danger" : "default"}
              type={type}
              placeholder={placeholder}
              {...field}
              onChange={(event) => {
                if (onChange) onChange(event);
                field.onChange(event);
              }}
              onBlur={(event) => {
                if (onBlur) onBlur(event);
                field.onBlur();
              }}
              onFocus={(event) => {
                if (onFocus) onFocus(event);
              }}
            />
          </FormControl>
          {!hasErrorMessage && <FormDescription>{helper}</FormDescription>}
          <FormMessage className="text-xs font-normal" />
        </FormItem>
      )}
    />
  );
};

export default FormInput;

FYI, I utilized control?: Control<any>; because of the component's reusability concern. Thanks