React/Typescript ForwardRef 类型用于返回输入或文本区域的元素

2024-05-05

我正在尝试使用 React 和 TypeScript 为我们的应用程序创建一个通用文本输入组件。我希望它能够成为基于给定 prop 的输入元素或文本区域元素。所以它看起来有点像这样:

import {TextArea, Input} from 'ourComponentLibrary'

export const Component = forwardRef((props, ref) => {
  const Element = props.type === 'textArea' ? TextArea : Input

  return (
    <Element ref={ref} />
  )
})

这段代码工作正常。然而,当尝试合并类型时,它就变得有点冒险了。 ref 类型应该是HTMLInputElement or HTMLTextAreaElement基于通过的type支柱。在我的脑海里,它看起来像这样:

interface Props {
  ...
}

export const Component = forwardRef<
  HTMLInputElement | HTMLTextAreaElement,
  Props
>((props, ref) => {
  ...
});

但我知道这并不完全是我所需要的。因此,错误:Type 'HTMLInputElement' is missing the following properties from type 'HTMLTextAreaElement': cols, rows, textLength, wrap

总之,我希望类型能够对齐,以便如果type道具是textArea那么ref类型应该是HTMLTextAreaElement,如果 type 属性是input那么 ref 类型应该是HTMLInputAreaElement

有什么建议吗?

Thanks.


虽然这并不能解决问题React.forwardProps,一种替代方法是解决它并利用innerRef财产。然后你可以强制类型innerRef财产。实现与您想要的相同结果,但具有灵活的类型、更少的开销并且无需实例化。

工作演示:


组件/标签/index.tsx

import * as React from "react";
import { FC, LabelProps } from "~types";

/*
  Field label for form elements

  @param {string} name - form field name
  @param {string} label - form field label 
  @returns {JSX.Element}
*/
const Label: FC<LabelProps> = ({ name, label }) => (
  <label className="label" htmlFor={name}>
    {label}&#58;
  </label>
);

export default Label;

组件/字段/index.tsx

import * as React from "react";
import Label from "../Label";
import { FC, InputProps, TextAreaProps } from "~types";

/*
  Field elements for a form that are conditionally rendered by a fieldType
  of "input" or "textarea".

  @param {Object} props - properties for an input or textarea
  @returns {JSX.Element | null} 
*/
const Field: FC<InputProps | TextAreaProps> = (props) => {
  switch (props.fieldType) {
    case "input":
      return (
        <>
          <Label name={props.name} label={props.label} />
          <input
            ref={props.innerRef}
            name={props.name}
            className={props.className}
            placeholder={props.placeholder}
            type={props.type}
            value={props.value}
            onChange={props.onChange}
          />
        </>
      );
    case "textarea":
      return (
        <>
          <Label name={props.name} label={props.label} />
          <textarea
            ref={props.innerRef}
            name={props.name}
            className={props.className}
            placeholder={props.placeholder}
            rows={props.rows}
            cols={props.cols}
            value={props.value}
            onChange={props.onChange}
          />
        </>
      );
    default:
      return null;
  }
};

export default Field;

组件/表单/index.tsx

import * as React from "react";
import Field from "../Fields";
import { FormEvent, FC, EventTargetNameValue } from "~types";

const initialState = {
  email: "",
  name: "",
  background: ""
};

const Form: FC = () => {
  const [state, setState] = React.useState(initialState);
  const emailRef = React.useRef<HTMLInputElement>(null);
  const nameRef = React.useRef<HTMLInputElement>(null);
  const bgRef = React.useRef<HTMLTextAreaElement>(null);

  const handleChange = React.useCallback(
    ({ target: { name, value } }: EventTargetNameValue) => {
      setState((s) => ({ ...s, [name]: value }));
    },
    []
  );

  const handleReset = React.useCallback(() => {
    setState(initialState);
  }, []);

  const handleSubmit = React.useCallback(
    (e: FormEvent<HTMLFormElement>) => {
      e.preventDefault();

      const alertMessage = Object.values(state).some((v) => !v)
        ? "Must fill out all form fields before submitting!"
        : JSON.stringify(state, null, 4);

      alert(alertMessage);
    },
    [state]
  );

  return (
    <form className="uk-form" onSubmit={handleSubmit}>
      <Field
        innerRef={emailRef}
        label="Email"
        className="uk-input"
        fieldType="input"
        type="email"
        name="email"
        onChange={handleChange}
        placeholder="Enter email..."
        value={state.email}
      />
      <Field
        innerRef={nameRef}
        label="Name"
        className="uk-input"
        fieldType="input"
        type="text"
        name="name"
        onChange={handleChange}
        placeholder="Enter name..."
        value={state.name}
      />
      <Field
        innerRef={bgRef}
        label="Background"
        className="uk-textarea"
        fieldType="textarea"
        rows={5}
        name="background"
        onChange={handleChange}
        placeholder="Enter background..."
        value={state.background}
      />
      <button
        className="uk-button uk-button-danger"
        type="button"
        onClick={handleReset}
      >
        Reset
      </button>
      <button
        style={{ float: "right" }}
        className="uk-button uk-button-primary"
        type="submit"
      >
        Submit
      </button>
    </form>
  );
};

export default Form;

类型/index.ts

import type {
  FC,
  ChangeEvent,
  RefObject as Ref,
  FormEvent,
  ReactText
} from "react";

// custom utility types that can be reused
type ClassName = { className?: string };
type InnerRef<T> = { innerRef?: Ref<T> };
type OnChange<T> = { onChange: (event: ChangeEvent<T>) => void };
type Placeholder = { placeholder?: string };
type Value<T> = { value: T };

// defines a destructured event in a callback
export type EventTargetNameValue = {
  target: {
    name: string;
    value: string;
  };
};

/*
  Utility interface that constructs typings based upon passed in arguments

  @param {HTMLElement} E - type of HTML Element that is being rendered
  @param {string} F - the fieldType to be rendered ("input" or "textarea")
  @param {string} V - the type of value the field expects to be (string, number, etc)
*/
interface FieldProps<E, F, V>
  extends LabelProps,
    ClassName,
    Placeholder,
    OnChange<E>,
    InnerRef<E>,
    Value<V> {
  fieldType: F;
}

// defines props for a "Label" component
export interface LabelProps {
  name: string;
  label: string;
}

// defines props for an "input" element by extending the FieldProps interface
export interface InputProps
  extends FieldProps<HTMLInputElement, "input", ReactText> {
  type: "text" | "number" | "email" | "phone";
}

// defines props for an "textarea" element by extending the FieldProps interface
export interface TextAreaProps
  extends FieldProps<HTMLTextAreaElement, "textarea", string> {
  cols?: number;
  rows?: number;
}

// exporting React types for reusability
export type { ChangeEvent, FC, FormEvent };

索引.tsx

import * as React from "react";
import { render } from "react-dom";
import Form from "./components/Form";
import "uikit/dist/css/uikit.min.css";
import "./index.css";

render(<Form />, document.getElementById("root"));
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

React/Typescript ForwardRef 类型用于返回输入或文本区域的元素 的相关文章

随机推荐