Storybook 无法将 register 道具传递到故事中

Storybook cannot pass register prop into stories

我有以下组件

import React from 'react';
import PropTypes from 'prop-types';

export default function FormField({
  type,
  name,
  label,
  register,
  required,
  placeholder,
  validationSchema
}) {
  return (
    <>
      <div>
        <label htmlFor={name} className="block text-sm font-medium text-gray-900">
          {label}
          <span className="text-red-500 font-bold text-lg">{required && '*'}</span>
        </label>

        <div className="mt-1">
          <input
            {...register(name, validationSchema)}
            type={type}
            name={name}
            id={name}
            className={['field', `field--${type}`].join(' ')}
            placeholder={placeholder}
          />
        </div>
      </div>
    </>
  );
}

FormField.propTypes = {
  type: PropTypes.oneOf(['text', 'email', 'password', 'file', 'checkbox']),
  register: PropTypes.func,
  name: PropTypes.string.isRequired,
  label: PropTypes.string.isRequired,
  placeholder: PropTypes.string
};

FormField.defaultProps = {
  type: 'text',
  name: 'text',
  label: 'Label',
  placeholder: ''
};

和以下故事文件

import FormField from './FormField';

export default {
  title: 'Forms/FormField',
  component: FormField,
  argTypes: {
    type: {
      options: ['text', 'email', 'password', 'file', 'checkbox'],
      control: {
        type: 'select'
      }
    }
  }
};

const Template = (args) => <FormField {...args} />;

export const Text = Template.bind({});
Text.args = {
  type: 'text',
  label: 'Text Field',
  name: 'text-field',
  errorMsg: 'Text field is required',
  placeholder: 'Text goes here'
};

但是,我收到的消息如下:

TypeError: register is not a function

如何将注册传递到故事中,即使我没有将它用于我的故事?

我试过传入并使用 FormProvider 并包装模板,但这似乎不起作用,除非我遗漏了什么

...

编辑 3

在与 Joris 聊天后,我现在有了以下组件

import React from 'react';
import PropTypes from 'prop-types';

const FormField = React.forwardRef(
  ({ type, name, label, required, placeholder, ...props }, ref) => {
    return (
      <div>
        <label htmlFor={name} className="block text-sm font-medium text-gray-900">
          {label}
          <span className="text-red-500 font-bold text-lg">{required && '*'}</span>
        </label>

        <div className="mt-1">
          <input
            {...props}
            name={name}
            ref={ref}
            type={type}
            id={name}
            className={['field', `field--${type}`].join(' ')}
            placeholder={placeholder}
          />
        </div>
      </div>
    );
  }
);

export default FormField;

FormField.propTypes = {
  type: PropTypes.oneOf(['text', 'email', 'password', 'file', 'checkbox']),
  register: PropTypes.func,
  name: PropTypes.string.isRequired,
  label: PropTypes.string.isRequired,
  placeholder: PropTypes.string
};

FormField.defaultProps = {
  type: 'text',
  name: 'text',
  label: 'Label',
  placeholder: ''
};

现在是下一页


import Head from 'next/head';
import Link from 'next/link';
import Image from 'next/image';
import Background from '../../../public/images/option1.png';
import Router from 'next/router';
import { signIn } from 'next-auth/client';
import { useForm, FormProvider } from 'react-hook-form';

// components
import ErrorsPopup from '../../components/ErrorsPopup';
import FormField from '../../components/Forms/FormField';
import Button from '../../components/Button';

export default function Login() {
  const methods = useForm();
  const { handleSubmit, register } = methods;

  const onSubmit = async (data) => {
    await signIn('credentials', {
      redirect: false,
      data
    });

    Router.push('/dashboard');
  };

  return (
    <>
      <Head>
        <title>Ellis Development - Login</title>
      </Head>

      <div className="relative">
        <div className="md:flex">
          {/* Image */}
          <div className="flex items-center justify-center bg-blue-700 h-screen lg:w-96">
            <Image src={Background} width={350} height={350} layout="fixed" />
          </div>

          {/* Contact form */}
          <div className="flex flex-col justify-center px-6 sm:px-10 w-full">
            <h1 className="text-4xl font-extrabold text-grey-800">Login</h1>

            {/* errors */}
            <FormProvider {...methods}>
              <ErrorsPopup />
            </FormProvider>

            <form
              onSubmit={handleSubmit(onSubmit)}
              className="mt-6 flex flex-col gap-y-6 sm:gap-x-8">
              {/* email field */}
              <FormField
                {...register('email', {
                  required: 'Email is required',
                  pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
                })}
                type="email"
                label="Email"
                placeholder="john.doe@ebrookes.dev"
                required
              />

              {/* password field */}
              <FormField
                {...register('password', {
                  required: 'Password is required'
                })}
                type="password"
                label="Password"
                placeholder="*******"
                required
              />

              <div className="flex items-center justify-between sm:col-span-2">
                <div>
                  <Button type="submit" label="Login" icon="SaveIcon" />
                </div>

                <div>
                  <Link href="/dashboard/auth/register">
                    <a className="underline decoration-blue-500 decoration-4 hover:decoration-2 mr-4">
                      Register
                    </a>
                  </Link>

                  <Link href="/dashboard/auth/forgot">
                    <a className="underline decoration-blue-500 decoration-4 hover:decoration-2">
                      Forgot your password?
                    </a>
                  </Link>
                </div>
              </div>
            </form>
          </div>
        </div>
      </div>
    </>
  );
}

现在的问题是表单仍然没有提交。

这里有一种方法可以满足您的需求。

const Template = (args) => {
  const { register } = useForm();

  return (
    <form>
      <FormField {...args} register={register} />
    </form>
  );
}

在react-hook-form,我们建议转发ref而不是向下传递register方法

const FormField = React.forwardRef(({
  type,
  label,
  required,
  placeholder,
  validationSchema,
  ...props
}, ref) => {
  return (
      <div>
        <label htmlFor={name} className="block text-sm font-medium text-gray-900">
          {label}
          <span className="text-red-500 font-bold text-lg">{required && '*'}</span>
        </label>

        <div className="mt-1">
          <input
            {...props}
            ref={ref}
            type={type}
            id={name}
            name={name}
            className={['field', `field--${type}`].join(' ')}
            placeholder={placeholder}
          />
        </div>
      </div>
  );
});

export default FormField;

并使用它如下:

function MyForm() {
  const { register } = useForm();

  return (
    <form>
     <FormField {...register('field-name') />
    </form>
  )
}

已编辑:完整的工作示例

https://codesandbox.io/s/hopeful-mahavira-pfkqs

import React from "react";
import { useForm } from "react-hook-form";

interface Props extends React.PropsWithRef<JSX.IntrinsicElements["input"]> {
  label: string;
}

const FormField = React.forwardRef<HTMLInputElement, Props>(
  ({ label, ...props }, ref) => {
    return (
      <div>
        <label
          htmlFor={props.name}
          className="block text-sm font-medium text-gray-900"
        >
          {label}
          <span className="text-red-500 font-bold text-lg">
            {props.required && "*"}
          </span>
        </label>

        <div className="mt-1">
          <input {...props} ref={ref} id={props.name} />
        </div>
      </div>
    );
  }
);

FormField.displayName = "FormField";

export default function App() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm();
  console.log(errors);

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <FormField
        required
        {...register("myField", { required: "Field is required" })}
        label="Field example"
      />

      <pre>{errors.myField?.message}</pre>
      <button type="submit">Submit</button>
    </form>
  );
}

为了让它工作,我现在有以下组件

import React from 'react';
import PropTypes from 'prop-types';

const FormField = React.forwardRef(
  ({ type, name, label, required, placeholder, ...props }, ref) => {
    return (
      <div>
        <label htmlFor={name} className="block text-sm font-medium text-gray-900">
          {label}
          <span className="text-red-500 font-bold text-lg">{required && '*'}</span>
        </label>

        <div className="mt-1">
          <input
            {...props}
            name={name}
            ref={ref}
            type={type}
            id={name}
            className={['field', `field--${type}`].join(' ')}
            placeholder={placeholder}
          />
        </div>
      </div>
    );
  }
);

export default FormField;

FormField.propTypes = {
  type: PropTypes.oneOf(['text', 'email', 'password', 'file', 'checkbox']),
  register: PropTypes.func,
  name: PropTypes.string.isRequired,
  label: PropTypes.string.isRequired,
  placeholder: PropTypes.string
};

FormField.defaultProps = {
  type: 'text',
  name: 'text',
  label: 'Label',
  placeholder: ''
};

故事

import { useForm } from 'react-hook-form';
import FormField from './FormField';

export default {
  title: 'Forms/FormField',
  component: FormField,
  argTypes: {
    type: {
      options: ['text', 'email', 'password', 'file', 'checkbox'],
      control: {
        type: 'select'
      }
    }
  }
};

const Template = (args) => {
  const { register } = useForm();

  return <FormField {...args} register={register} />;
};

export const Text = Template.bind({});
Text.args = {
  type: 'text',
  label: 'Text Field',
  name: 'text-field',
  errorMsg: 'Text field is required',
  placeholder: 'Text goes here'
};

export const Email = Template.bind({});
Email.args = {
  type: 'email',
  name: 'email',
  label: 'Email',
  placeholder: 'john.doe@email.com'
};

export const Password = Template.bind({});
Password.args = {
  type: 'password',
  name: 'password',
  label: 'Password',
  placeholder: '********'
};

export const File = Template.bind({});
File.args = {
  type: 'file',
  name: 'file-upload',
  label: 'File upload'
};

export const Checkboxes = Template.bind({});
Checkboxes.args = {
  type: 'checkbox',
  name: 'check-1',
  label: 'Checkboxes'
};

和页面

import Head from 'next/head';
import Link from 'next/link';
import Image from 'next/image';
import Background from '../../../public/images/option1.png';
import Router from 'next/router';
import { signIn } from 'next-auth/client';
import { useForm, FormProvider } from 'react-hook-form';

// components
import ErrorsPopup from '../../components/ErrorsPopup';
import FormField from '../../components/Forms/FormField';
import Button from '../../components/Button';

export default function Login() {
  const methods = useForm();
  const { handleSubmit, register } = methods;

  const onSubmit = async (data) => {
    await signIn('credentials', {
      redirect: false,
      email: data.email,
      password: data.password
    });

    Router.push('/dashboard');
  };

  return (
    <>
      <Head>
        <title>Ellis Development - Login</title>
      </Head>

      <div className="relative">
        <div className="md:flex">
          {/* Image */}
          <div className="flex items-center justify-center bg-blue-700 h-screen lg:w-96">
            <Image src={Background} width={350} height={350} layout="fixed" />
          </div>

          {/* Contact form */}
          <div className="flex flex-col justify-center px-6 sm:px-10 w-full">
            <h1 className="text-4xl font-extrabold text-grey-800">Login</h1>

            {/* errors */}
            <FormProvider {...methods}>
              <ErrorsPopup />
            </FormProvider>

            <form
              onSubmit={handleSubmit(onSubmit)}
              className="mt-6 flex flex-col gap-y-6 sm:gap-x-8">
              {/* email field */}
              <FormField
                {...register('email', {
                  required: 'Email is required',
                  pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
                })}
                type="email"
                name="email"
                label="Email"
                placeholder="john.doe@ebrookes.dev"
                required
              />

              {/* password field */}
              <FormField
                {...register('password', {
                  required: 'Password is required'
                })}
                type="password"
                name="password"
                label="Password"
                placeholder="*******"
                required
              />

              <div className="flex items-center justify-between sm:col-span-2">
                <div>
                  <Button type="submit" label="Login" icon="SaveIcon" />
                </div>

                <div>
                  <Link href="/dashboard/auth/register">
                    <a className="underline decoration-blue-500 decoration-4 hover:decoration-2 mr-4">
                      Register
                    </a>
                  </Link>

                  <Link href="/dashboard/auth/forgot">
                    <a className="underline decoration-blue-500 decoration-4 hover:decoration-2">
                      Forgot your password?
                    </a>
                  </Link>
                </div>
              </div>
            </form>
          </div>
        </div>
      </div>
    </>
  );
}