4

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}
    />
  );
});
2
  • Similar question here: stackoverflow.com/questions/63963850/… Commented Dec 17, 2020 at 7:50
  • It is a really helpful answer. But I would like to pass the e (event) type at the onChange() function, as the parent component can have access to all the attributes. Check my comment on @gazdagero's answer Commented Dec 17, 2020 at 8:14

1 Answer 1

2

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} />
}
Sign up to request clarification or add additional context in comments.

11 Comments

I am using various components before and after the base <input /> element, that are included in the <Input /> component. What I mean, is that I change the input value while the user types in the <Input /> component, and then if another component wants it to be available at it parent layer, they can access the onChange prop from there. So, the props are fine, but the onChange() function complains, when React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
Can you add a minimal code snippet pls, which includes the data handling in parent and this onChange stuff.
Yes, of course. I just hope the structure is not so complicated.
Yes, a little bit complicated. Can you remove the unrelated part (outer wrapper etc) to show better the problematic part?
Also would be great to know what is the expected behaviour, what functionality would you like achieve with this structure. It is also not clear for me what the <Base /> component is? Looks like a HOC base from its name, but it is not.
|

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.