Semantic-UI-React: missing file upload input/button

Could not find a way to present an upload file button/input using the components provided here (great library btw!)

I’ve been using this and it seems to work well, perhaps a cleaner version could be included:

function UploadButton({label, onUpload, id}) {
  let fileInput = null;
  // If no id was specified, generate a random one
  const uid = id || Math.random().toString(36).substring(7);

  return (
    <span>
      <label htmlFor={uid} className="ui icon button">
        <i className="upload icon"></i>
        {label}
      </label>
      <input type="file" id={uid}
        style={{display: "none"}}
        onChange={() => {
          onUpload(fileInput.files[0]);
        }}
        ref={input => {
          fileInput = input;
        }}
      />
    </span>
  );
} 

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 17
  • Comments: 18 (4 by maintainers)

Most upvoted comments

I think a file button is a bit too specific, the idea you’ve used is a solution.

@strrel try this one The original is a typescript snippet

import * as React from 'react';
import { Component } from 'react';
import { Button, ButtonProps, Label } from 'semantic-ui-react';
import * as uuid from 'uuid';


export class FileButton extends Component {
    constructor(props) {
        super(props);

        this.id = uuid.v1();
        this.onChangeFile = this.onChangeFile.bind(this);
    }

    render() {
        return (
            <div>
                <Button
                    {...this.props}
                    as="label"
                    htmlFor={this.id} />
                <input
                    hidden
                    id={this.id}
                    multiple
                    type="file"
                    onChange={this.onChangeFile} />
            </div>
        );
    }

    onChangeFile() {
        const fileButton = document.getElementById(this.id);
        const file = fileButton ? fileButton.files[0] : null;
        if (this.props.onSelect) {
            this.props.onSelect(file);
        }
    }
}

export default FileButton;

For those wishing to accomplish this, the following works:

<Label
    as="label"
    basic
    htmlFor="upload"
>
    <Button
        icon="upload"
        label={{
            basic: true,
            content: 'Select file(s)'
        }}
        labelPosition="right"
    />
    <input
        hidden
        id="upload"
        multiple
        type="file"
    />
</Label>

Produces: suir_non-ugly-file-uploader

Note that the Label’s as="label" (why is this not the default??!) htmlFor is the magic that makes it all work: The browser natively triggers an input when its associated label is clicked. A label can be associated in 2 ways:

  1. Simply wrapping the input
  2. The for (htmlFor) attribute with a value equal to the input’s name or id

The first does not work here because there are multiple form elements within the label; unfortunately that means relying on the second, which is not as good for accessibility and is more brittle (requires manually ensuring the input’s name and the label’s for stay synced).

I used a Button for its cursor: pointer (and because it fits nicely with the icon).

If you really wanted to go the whole 9 yards, you’d add an onChange to the input[type=file] to track the selected files and list their names below the button within the Label (which is what the native one does).

based on @jshado1’s suggestion i came up with this component. (in typescript, but you can remove unnecessary types) I just declared the Button as label and removed the surrounding label. That should also solve the submit problem. Furthermore you can now treat the component as a normal semantic Button since all props are passed through. For my purpose I also added an onSelect handler:

import * as React from 'react';
import { Component } from 'react';
import { Button, ButtonProps, Label } from 'semantic-ui-react';
import * as uuid from 'uuid';

interface ActionProps {
    onSelect?: (file) => void;
}

export class FileButton extends Component<ActionProps & ButtonProps> {
    private id: string = uuid.v1();

    constructor(props) {
        super(props);
        this.onChangeFile = this.onChangeFile.bind(this);
    }

    public render() {
        return (
            <React.Fragment>
                <Button
                    {...this.props}
                    as="label"
                    htmlFor={this.id} />
                <input
                    hidden
                    id={this.id}
                    multiple
                    type="file"
                    onChange={this.onChangeFile} />
            </React.Fragment>
        );
    }

    private onChangeFile() {
        const fileButton: any = document.getElementById(this.id);
        const file = fileButton ? fileButton.files[0] : null;
        if (this.props.onSelect) {
            this.props.onSelect(file);
        }
    }
}

Instead of relying on ID for this, you can use ref and call .click().

let fileInput: HTMLInputElement;

return (
    <Button
        as="label"
        title="Add another video file"
        onClick={() => fileInput.click()}
    >
        <Icon.Group>
            <Icon name="video" />
            <Icon corner name="add" />
        </Icon.Group>
    </Button>

    <input
        ref={element => fileInput = element}
        hidden
        type="file"
    />
);

This is safer as there’s no risk of ID clashes

After looking for options, this is what I finally come up with (plain React no TS)

<Button as='div' labelPosition='right' onClick={() => this.input.click()}>
    <Button icon title="Add another file">
        <Icon name="upload"/>
    </Button>
    <Label as='a' basic pointing='left'>
        Upload CSV
    </Label>
</Button>

<input
    ref={element => this.input = element}
    hidden
    onChange={(e)=>this.onChange(e)}
    type="file"
/>

image

@iad42 Thank you!

It’s rare that I come across a solution that was recently posted to an issue I’m looking for in the moment. I’m not sure how much time you just saved me, but I’m sure at the very least I gained a day in development haha. Cheers! 🥂

@jshado1 Hmm, i have it inside form group, with explicit submit button to. Maybe browser specific reaction.

I did it like this, not as fancy, but works.

<Label width="4" as="label" htmlFor="file" size="big">
  <Icon name="file" />
  Image
</Label>
<input id="file" hidden type="file" />

capture

Just require CSS style for cursor.

@jshado1 Dose not work properly, clicking upload icon submits form. Adding type=“button” did not helped. Adding e.preventDefault() blocks file selection menu.

There should be official guide to do this properly.