skip to Main Content

The following code implements clickable Mui Button which upon click action opens the system’s "Select file" window. It’s usage is pretty straightforward and can be triggered as <SelectFileButton onFileSelected={(selectedFiles) => { ... }}

The problem that I’m facing right now is that I’m trying to make it more generic, enabling any "clickable" type to be used in place of the Mui Button (line: <Button onClick={handleClick}>{children}</Button>) and of course I’m failing miserably with this daunting task. Is there any simple solution that can change mentioned line of code to more generic equivalent?

import React from "react";
import Button from "@mui/material/Button";
import { ChangeEvent, ReactNode } from "react";

type Props = {
  onFileSelected?: (selectedFiles: File | File[]) => void;
  multiple?: boolean;
  children: ReactNode;
};

export const SelectFileButton: React.FC<Props> = ({
  children,
  onFileSelected = null,
  multiple = false,
}) => {
  const hiddenFileInput = React.useRef<HTMLInputElement>(null);

  const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
    if (e.target.files)
      onFileSelected?.(
        multiple ? Array.from(e.target.files) : e.target.files[0]
      );
  };

  const handleClick = () => {
    hiddenFileInput.current!.click();
  };

  return (
    <React.Fragment>
      <Button onClick={handleClick}>{children}</Button> 
      <input
        type="file"
        ref={hiddenFileInput}
        style={{ display: "none" }}
        onChange={handleFileChange}
        multiple={multiple}
      />
    </React.Fragment>
  );
};

3

Answers


  1. Chosen as BEST ANSWER

    I've ended up with the following solution. When I create SelectFileButton component I pass in an additional prop named component with the Mui component of my choice, for example

    <SelectFileButton component={<Button />} onFileSelected={handleClick}>
      Select file
    </SelectFileButton>
    
    import React from "react";
    import { ChangeEvent, ReactNode } from "react";
    
    type Props = {
      onFileSelected?: (selectedFiles: File | File[]) => void;
      multiple?: boolean;
      children: ReactNode;
      component: JSX.Element;
    };
    
    export const SelectFileButton: React.FC<Props> = ({
      children,
      component,
      onFileSelected = null,
      multiple = false,
    }) => {
      const hiddenFileInput = React.useRef<HTMLInputElement>(null);
    
      const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
        if (e.target.files)
          onFileSelected?.(
            multiple ? Array.from(e.target.files) : e.target.files[0]
          );
      };
    
      const handleClick = () => {
        hiddenFileInput.current!.click();
      };
    
      return (
        <React.Fragment>
          {React.cloneElement(component, {
            children,
            onClick: handleClick,
          })}
          <input
            type="file"
            ref={hiddenFileInput}
            style={{ display: "none" }}
            onChange={handleFileChange}
            multiple={multiple}
          />
        </React.Fragment>
      );
    };
    

  2. You can pass any element as prop

    export const FileUploadButton: React.FC<Props> = ({
      children,
      onFileSelected = null,
      multiple = false,
      ClickableElement,
      onClick,
    }) => {
      const hiddenFileInput = React.useRef<HTMLInputElement>(null)
    
      const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
        if (e.target.files)
          onFileSelected?.(
            multiple ? Array.from(e.target.files) : e.target.files[0],
          )
      }
    
      const handleUploadClick = () => {
        hiddenFileInput.current!.click()
      }
    
      return (
        <React.Fragment>
          <ClickableElement onClick={handleUploadClick}>
            {children}
          </ClickableElement>
          <input
            type="file"
            ref={hiddenFileInput}
            style={{ display: 'none' }}
            onChange={handleFileChange}
            multiple={multiple}
          />
        </React.Fragment>
      )
    }
    
     <FileUploadButton children="text" ClickableElement={Button} onClick={() => {....}/>
    

    or you can delete onClick prop and pass element with this prop:

    <FileUploadButton children="text" ClickableElement={<Button onClick={() => {....} />} />
    

    Of course you should change prop types depends on solution you will choose.

    But I think you want make over optimization.

    https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it

    This component have one simple function and include all to implement it. A lot of dependencies make component hard to use and understand. And component what you can pass may have very different props and behaviour in comparison with Button that will be hard to handle it.

    Login or Signup to reply.
  3. The MUI Button has a component prop that will accept an elementType (any HTML element or other component)

    This prop could be forwarded through your props

    type Props = {
      onFileSelected?: (selectedFiles: File | File[]) => void;
      multiple?: boolean;
      component?: React.ElementType
      children: ReactNode;
    };
    

    And then consumed by the MUI button

    <Button component={component} onClick={handleUploadClick}>{children}</Button>
    

    This way, the MUI implementation will ensure that proper button-like accessibility is applied to whichever elementType is passed in

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search