Semantic-ui-react: missing file upload input/button

Created on 22 Sep 2017  路  18Comments  路  Source: Semantic-Org/Semantic-UI-React

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>
  );
} 
question

Most helpful comment

@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;

All 18 comments

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

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).

@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.

@mjasnikovs I didn't experience that behaviour, although I am using it in a form with another button that is explicitly type="submit", so maybe that's why. I did notice after I posted this (I swear it wasn't happening before) that clicking the upload icon (which is the actual button) caused the file browser to not open; since I didn't want to actually kill the click event entirely (just make the button get out of the way), I added a className to the Label and then the following less:

.file-uploader {
    .ui.input { display: none; }
    // I ended up switching the `input` to an SUIR `Input`
    // which doesn't accept the `hidden` attribute 馃槱
    button.ui.button { pointer-events: none; }
    // tells the browser to ignore button for all MouseEvents (doesn't affect :hover, etc)
    // so the click hits the containing label instead
}

Yes, at the very least, there should be an example of this in the docs. However, I think it deserves its own component. I ended up extracting this into its own within my project (and added support for passing most of the values as props, plus fluid and onChange鈥攆luid was a bit tricky because it had to cascade to SUIR components that don't strictly support it but behave properly if passed the fluid css class).

I would be happy to contribute it back to SUIR if they're interested.

@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.

@mjasnikovs interesting. I just checked in Chrome (which I used before), and $0.type indeed returns "submit" (although it's not present in the html). This is apparently the default鈥擨 thought the default was "button" (source: MDN); however, SUIR doesn't seem to support specifying the type for <Button>, or at least not with all of the other props I've set (SUIR applies the type="button" to the container div instead of the button itself).

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);
        }
    }
}

jira-screenshot

@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;

@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! 馃

I wonder why those solutions are working for me even without the use of htmlFor and id. Tried with Safari and Chrome, maybe I need it for IE?

Also, I just wanted to mention that instead of using document.getElementById to get the files, the onChangeFile receives an event which contains the files in event.target.files

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

@Jameskmonger's solution is great. i did a refactor that i think blends the best of all of the above solutions. it's extensible, provides good defaults, and mandates essential props (e.g. id for the input so the label/accessibility works).

image

export const InputFile: React.FC<{
  button?: ButtonProps
  input: React.InputHTMLAttributes<any> & { id: string }
}> = ({ button = {}, input: inputProps }) => {
  let hiddenInput: HTMLInputElement | null = null
  return (
    <React.Fragment>
      <Button
        icon='upload'
        htmlFor={inputProps.id}
        label={<Label as='label' style={{ cursor: 'pointer' }} basic children='Select file' />}
        onClick={() => hiddenInput!.click()}
        labelPosition='right'
        {...button}
      />
      <input
        hidden
        ref={el => {
          hiddenInput = el!
        }}
        type='file'
        {...inputProps}
      />
    </React.Fragment>
  )
}

usage:

<InputFile
  input={{
    id: 'upload-file',
    onChange: handleUploadRequest
  }}
/>

and i released https://www.npmjs.com/package/semantic-ui-react-input-file for it. perhaps we can collab :)

@cdaringe What does the ! do here in hiddenInput!.click() or hiddenInput = el!?

It's a TS-ism that asserts that the operand is non-null/non-undefined.

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

@laurensiusadi more like "plain React no BS"

Was this page helpful?
0 / 5 - 0 ratings