0

I'm working on a form in React using Material UI (MUI) and react-hook-form, and I want to create a reusable TextField component that:

Works with react-hook-form using the Controller.

Supports different input types (like password with show/hide functionality).

Allows for optional adornments (icons before/after input).

Can be customized with props like readOnly, variant, outerLabel, etc.

Here’s the component I’ve built so far:

import {
  FormLabel,
  IconButton,
  InputAdornment,
  Stack,
  TextField,
} from "@mui/material";

// form
import { Controller, useFormContext } from "react-hook-form";
import { useState } from "react";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import { JSX } from "@emotion/react/jsx-dev-runtime";

// ----------------------------------------------------------------------

export function RHFTextField({
  name,
  type = "text",
  variant = "outlined",
  readOnly = false,
  StartIcon,
  EndIcon,
  outerLabel,
  fullWidth = true,
  ...other
}: any): JSX.Element {
  const { control } = useFormContext();
  const [showPassword, setShowPassword] = useState<boolean>(false);

  const endAdornment =
    type === "password" ? (
      <InputAdornment position="end">
        <IconButton
          aria-label="toggle password visibility"
          onClick={() => {
            setShowPassword((prev) => !prev);
          }}
        >
          {showPassword ? <Visibility /> : <VisibilityOff />}
        </IconButton>
      </InputAdornment>
    ) : (
      EndIcon
    );

  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState: { error } }) => (
        <Stack gap="0.6rem">
          {outerLabel && <FormLabel>{outerLabel}</FormLabel>}
          <TextField
            {...field}
            error={Boolean(error)}
            helperText={error?.message}
            type={showPassword ? "text" : type}
            variant={variant}
            InputProps={{
              readOnly,
              endAdornment,
              startAdornment: StartIcon,
            }}
            fullWidth={fullWidth}
            {...other}
          />
        </Stack>
      )}
    />
  );
}
1
  • Hi, you didn't mention where you need help. Is there an issue with what you're building? share Commented Apr 14 at 10:22

1 Answer 1

1

Took me some time but here is a custom reusable MUI TextField component that incorporates all your stipulated requirements:

import React from "react";
import {
  Control,
  Controller,
  FieldValues,
  RegisterOptions,
} from "react-hook-form";
import Stack from "@mui/material/Stack";
import FormHelperText from "@mui/material/FormHelperText";
import FormLabel from "@mui/material/FormLabel";
import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment";
import TextField from "@mui/material/TextField";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";

import { TextFieldProps } from "@mui/material";

interface CustomMuiTextFieldProps<T extends FieldValues>
  extends Omit<TextFieldProps, "defaultValue" | "name"> {
  name: keyof T & string;
  control: Control<T>;
  rules?:
    | Omit<
        RegisterOptions<FieldValues, string>,
        "disabled" | "valueAsNumber" | "valueAsDate" | "setValueAs"
      >
    | undefined;
  defaultValue?: string;
  outerLabel?: string;
  id?: string;
  type?: string;
  showPassword?: boolean;
  setShowPassword?: React.Dispatch<React.SetStateAction<boolean>>;
  endIcon?: React.ReactNode;
}

const CustomMuiTextField = React.forwardRef<
  HTMLInputElement,
  CustomMuiTextFieldProps<any>
>((props, ref) => {
  const {
    name,
    rules,
    control,
    type,
    defaultValue,
    id,
    outerLabel,
    showPassword,
    setShowPassword,
    endIcon,
    ...rest
  } = props;

  const endAdornment =
    type === "password" ? (
      <InputAdornment position="end">
        <IconButton
          onClick={() => {
            setShowPassword?.((prev) => !prev);
          }}
        >
          {showPassword ? <Visibility /> : <VisibilityOff />}
        </IconButton>
      </InputAdornment>
    ) : (
      endIcon
    );

  return (
    <Controller
      control={control}
      name={name}
      rules={rules}
      defaultValue={defaultValue}
      render={({ field, fieldState: { error } }) => (
        <Stack gap="0.6rem">
          {outerLabel && <FormLabel htmlFor={id}>{outerLabel}</FormLabel>}
          <TextField
            {...field}
            {...rest}
            type={type}
            InputProps={{ endAdornment: endAdornment, ...rest.InputProps }}
            variant="standard"
            inputRef={ref}
          />
          {error && <FormHelperText>{error.message}</FormHelperText>}
        </Stack>
      )}
    />
  );
});

export default CustomMuiTextField;

This is what it looks like when it's rendered:

<CustomMuiTextField
     control={control}
     name="currentPassword"
     outerLabel="Current Password"
     id="currentPassword"
      type="password"
      rules={{
        required: "Current password is required",
      }}
      InputProps={{ readOnly: true }}
      endIcon={<AccessTimeFilledIcon />}
/>
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.