(react) react-hook-form Input example

Our form elements needs to be usable with react-hook-form
This commit is contained in:
Romain Le Cellier
2023-07-26 16:52:22 +02:00
parent bdc08cf043
commit b72d0d5c90
5 changed files with 99 additions and 19 deletions

View File

@@ -100,12 +100,20 @@ using the component in a controlled way.
## Ref
You can use the `ref` props to get a reference to the input element. The ref to the native input is nested inside the `input` attribute.
You can use the `ref` props to get a reference to the input element.
<Canvas sourceState="shown">
<Story id="components-forms-input--with-ref"/>
</Canvas>
## Usage with react-hook-form
You can use this input with [react-hook-form](https://react-hook-form.com/docs)
<Canvas sourceState="shown">
<Story id="components-forms-input--react-hook-form"/>
</Canvas>
## Props
You can use all the props of the native html `<input>` element props plus the following.

View File

@@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import React, { useRef } from "react";
import userEvent from "@testing-library/user-event";
import { expect } from "vitest";
@@ -160,7 +160,7 @@ describe("<Input/>", () => {
});
expect(input).not.toHaveFocus();
await user.click(screen.getByRole("button", { name: "Focus" }));
expect(input).toHaveFocus();
waitFor(() => expect(input).toHaveFocus());
});
it("works controlled", async () => {
const Wrapper = () => {

View File

@@ -1,7 +1,15 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { Meta } from "@storybook/react";
import { useForm } from "react-hook-form";
import * as Yup from "yup";
import React, { useRef } from "react";
import { Input, InputRefType } from ":/components/Forms/Input/index";
import { Button } from ":/components/Button";
import {
getFieldState,
getFieldErrorMessage,
onSubmit,
} from ":/tests/reactHookFormUtils";
export default {
title: "Components/Forms/Input",
@@ -162,11 +170,52 @@ export const WithRef = () => {
return (
<div>
<Input label="Your identity" ref={ref} />
<Button onClick={() => ref.current?.input?.focus()}>Focus</Button>
<Button onClick={() => ref.current?.focus()}>Focus</Button>
</div>
);
};
export const ReactHookForm = () => {
interface InputExampleFormValues {
email: string;
}
const inputExampleSchema = Yup.object().shape({
email: Yup.string().email().required(),
});
const { register, handleSubmit, formState } = useForm<InputExampleFormValues>(
{
defaultValues: {
email: "",
},
mode: "onChange",
reValidateMode: "onChange",
resolver: yupResolver(inputExampleSchema),
},
);
return (
<form
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
width: "400px",
}}
onSubmit={handleSubmit(onSubmit)}
>
<Input
label="Email address"
fullWidth={true}
state={getFieldState("email", formState)}
text={getFieldErrorMessage("email", formState)}
{...register("email")}
/>
<Button fullWidth={true}>Log-in</Button>
</form>
);
};
export const FormExample = () => {
return (
<div>

View File

@@ -3,7 +3,6 @@ import React, {
InputHTMLAttributes,
ReactNode,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
@@ -12,10 +11,6 @@ import { randomString } from ":/utils";
import { Field, FieldProps } from ":/components/Forms/Field";
import { LabelledBox } from ":/components/Forms/LabelledBox";
export interface InputRefType {
input: HTMLInputElement | null;
}
type Props = InputHTMLAttributes<HTMLInputElement> &
FieldProps & {
label?: string;
@@ -25,7 +20,7 @@ type Props = InputHTMLAttributes<HTMLInputElement> &
charCounterMax?: number;
};
export const Input = forwardRef<InputRefType, Props>(
export const Input = forwardRef<HTMLInputElement, Props>(
(
{
className,
@@ -48,8 +43,7 @@ export const Input = forwardRef<InputRefType, Props>(
if (className) {
classes.push(className);
}
const inputRef = useRef<HTMLInputElement>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const [inputFocus, setInputFocus] = useState(false);
const [value, setValue] = useState(defaultValue || props.value || "");
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(!value);
@@ -78,12 +72,6 @@ export const Input = forwardRef<InputRefType, Props>(
setValue(props.value || "");
}, [props.value]);
useImperativeHandle(ref, () => ({
get input() {
return inputRef.current;
},
}));
return (
<Field
state={state}
@@ -130,7 +118,16 @@ export const Input = forwardRef<InputRefType, Props>(
setValue(e.target.value);
props.onChange?.(e);
}}
ref={inputRef}
ref={(inputTextRef) => {
if (ref) {
if (typeof ref === "function") {
ref(inputTextRef);
} else {
ref.current = inputTextRef;
}
}
inputRef.current = inputTextRef;
}}
/>
</LabelledBox>
{!!rightIcon && (

View File

@@ -0,0 +1,26 @@
import { FieldValues, FormState } from "react-hook-form";
export function getFieldState<FormValues extends FieldValues>(
field: keyof FormValues,
formState: FormState<FormValues>,
) {
if (field in formState.errors) {
return "error";
}
return "default";
}
export function getFieldErrorMessage<FormValues extends FieldValues>(
field: keyof FormValues,
formState: FormState<FormValues>,
): string {
const errorMessage = formState.errors[field]?.message;
if (!errorMessage) {
return "";
}
return errorMessage as string;
}
export function onSubmit<FormValues>(data: FormValues) {
alert(`Submited form values: ${JSON.stringify(data)}`);
}