Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React.js TypeScript Same component for <input /> and <textarea />

I have created an <Input /> component with some base - helper components for better usability and minimizing the code inside the main component. However, I would like to create also a <textarea /> HTML element. I am using React.js with TypeScript. So, the props and most specifically the input event props overwrite each other, and the compiler complains. Is there anything that I can do, except for writing the same code inside the <Multiline /> component?

Note: I am wrapping the component with React.forwardRef for forwarding and passing a ref to the input component

UPDATE: Add code snippet

Input/Input.tsx

export default class Input extends React.Component<FProps, State> {    
  static displayName = "Input";

  state: State = {
    id: this.props.id || "",
    value: this.props.value || this.props.defaultValue || "",
  };

  componentDidMount() {
    if (!this.state.id) {
      this.setState(prevState => ({ ...prevState, id: generateInputId() }));
    }
  }

  handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newVal = e.target.value;
    const { onChange } = this.props;

    this.setState({ value: newVal });

    if (onChange) {
      onChange(e);
    }
  };

  render() {
    const { id, value } = this.state;

    return (
      <InputContextProvider value={{ ...this.props, value, id, onChange: this.handleChange }}>
        <InputContainer>
          <FieldContainer />
        </InputContainer>
      </InputContextProvider>
    );
  }
}

Input/helpers/FieldContainer.tsx

export const FieldContainer: React.FunctionComponent = () => {
  const {
    value: propsValue,
    type,
    label,
    id,
    defaultValue,
    className,
    state = "default",
    placeholder,
    floatingplaceholder,
    prefix,
    suffix,
    characterLimit,
    maxLength,
    allowClear,
    onChange,
    forwardref,
    ...rest
  } = useInputContext();

  const isDisabled = useDisabled(rest, state);
  const [value, setValue] = useState<string | number>(propsValue || defaultValue || "");
  const [prefixWidth, setPrefixWidth] = useState<number>(0);
  const [suffixWidth, setSuffixWidth] = useState<number>(0);

  const prefixRef = useRef<HTMLDivElement>(null);
  const suffixRef = useRef<HTMLDivElement>(null);

  // TODO: Replace with <Icon /> component
  if (allowClear) {
    // suffix = <>
  }

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newVal = e.target.value;

    if (characterLimit && maxLength && String(newVal).length > maxLength) {
      return;
    }

    setValue(newVal);

    if (onChange) {
      onChange(e);
    }
  };

  useEffect(() => {
    if (prefixRef && prefixRef.current) {
      const prefixWidth = prefixRef.current.offsetWidth;
      setPrefixWidth(prefixWidth);
    }
    if (suffixRef && suffixRef.current) {
      const suffixWidth = suffixRef.current.offsetWidth;
      setSuffixWidth(suffixWidth);
    }
  }, [prefix, suffix]);

  return (
    <OuterFieldContainer prefixWidth={prefixWidth}>
      <FixGroup group={prefix} ref={prefixRef} position="left" />
      <Base
        id={id}
        value={value}
        className={className}
        state={state}
        placeholder={floatingplaceholder === false && placeholder ? placeholder : undefined}
        disabled={isDisabled}
        onChange={e => handleChange(e)}
        ref={forwardref}
        style={{
          paddingLeft: prefix ? `${(prefixWidth + DEFAULT_PADDING) / EM_REM_MULTIPLIER}em` : "",
          paddingRight: suffix ? `${(suffixWidth + DEFAULT_PADDING) / EM_REM_MULTIPLIER}em` : "",
        }}
        maxLength={maxLength}
        {...rest}
      />
      <FixGroup group={suffix} ref={suffixRef} position="right" />
    </OuterFieldContainer>
  );
};

Input/helpers/Base/Base.tsx

export const Base = React.forwardRef<HTMLInputElement>((componentProps, ref) => {
  const {
    id: propsId,
    value,
    placeholder,
    type,
    floatingplaceholder,
    state = "default",
    onChange,
    style,
    children: propsChildren,
    characterLimit,
    overrideOnChange,
    allowClear,
    ...props
  } = useInputContext();

  const id = useInputId(propsId);
  const [classNames, rest] = useClassnames("input", props, { stateToRemove: { state } });
  const isDisabled = useDisabled(props, state);

  // return type === "textarea" ? (
  //   <textarea
  //     id={id}
  //     className={classNames}
  //     value={value || ""}
  //     placeholder={placeholder}
  //     disabled={isDisabled}
  //     aria-disabled={isDisabled}
  //     aria-label={placeholder}
  //     ref={ref}
  //     // * Enable / disabled the Grammarly extension
  //     // data-gramm="false"
  //     cols={28}
  //     rows={5}
  //     {...rest}
  //   />
  // ) : (
  return (
    <input
      id={id}
      type={type}
      className={classNames}
      value={value}
      placeholder={!floatingplaceholder ? placeholder : undefined}
      disabled={isDisabled}
      aria-disabled={isDisabled}
      aria-label={placeholder}
      ref={ref}
      data-hasfloatingplaceholder={floatingplaceholder}
      data-testid="input"
      onChange={e => onChange && onChange(e)}
      style={style}
      {...rest}
    />
  );
});
like image 803
georgekrax Avatar asked Oct 15 '25 02:10

georgekrax


1 Answers

What about using the same component with a boolean like multiLine and spreading some ...props to handle the different props of input and textarea (if there is any difference).

const Input:FC<{
  multiLine: Boolean
  [key:string]: any
}> = ({ multiLine, ...props }) => {
  if (multiLine) return <textarea {...props} />
  return <input {...props} />
}
like image 99
gazdagergo Avatar answered Oct 17 '25 16:10

gazdagergo



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!