Skip to content

Instantly share code, notes, and snippets.

@srph
Last active March 14, 2022 09:53
Show Gist options
  • Select an option

  • Save srph/9a763d3c6b5464233243bb756654f2dc to your computer and use it in GitHub Desktop.

Select an option

Save srph/9a763d3c6b5464233243bb756654f2dc to your computer and use it in GitHub Desktop.
React: File upload component example (attachments)
import React, {Component, createElement, DOM} from 'react';
import PropTypes from 'prop-types';
import c from 'classnames';
import ProgressBar from 'app/components/ProgressBar';
import Icon from 'app/components/Icon';
import mime from 'mime';
import axios from 'axios';
import put from '101/put';
import del from '101/del';
import uuid from 'uuid/v4';
import reader from 'app/utils/reader';
import formdata from 'app/utils/formdata';
import objectify from 'app/utils/objectify';
import insert from 'app/utils/insert';
import toast from 'app/utils/toast';
import messages from 'app/messages';
import getStatusMessage from 'app/utils/getStatusMessage';
// Extension regex
const IMG = /jpg|jpeg|png/;
const DOC = /docx|doc|pdf|txt/;
const SPREADSHEET = /xlsx|csv/;
const PRESENTATION = /ppt|pptx/;
/**
* @example
* Basic
* <Attachments resource={`/comments/:id/files`} files={x.files} onAttach={cb} />
*
* @example
* Triggering file selection
* <Attachments {...props} ref={c => this.attachments = c} />
* this.attachments.trigger()
*
* @example
* Read-only attachments - Do not provide resource
* <Attachments files={x.files} />
*/
class Attachments extends Component {
constructor() {
super();
this.state = {
error: '',
uploading: {},
progress: {},
removing: {}
};
this.input = null;
this.handleSelect = this.handleSelect.bind(this);
this.handleProgress = this.handleProgress.bind(this);
this.handleRemove = this.handleRemove.bind(this);
}
render() {
const {uploading, progress, removing} = this.state;
const {attachments, resource} = this.props;
const readonly = resource == null;
const Wrapper = readonly ? DOM.a : DOM.div;
const input = <input type="file"
multiple
ref={c => this.input = c}
onChange={this.handleSelect}
style={{ display: 'none' }} />
if (!attachments) {
return input;
}
return (
<div className="board-attachments">
<h4 className="heading">
<span className="count">{attachments.length}</span>
Attachments
</h4>
<div className="set">
{attachments.map((attachment, i) => {
const loading = uploading[attachment.id];
const completed = progress[attachment.id];
const deleting = removing[attachment.id];
const ext = mime.extension(attachment.mime);
return Wrapper({
title: loading ? 'Uploading...' : (deleting ? `Deleting ${attachment.name}...` : attachment.name),
className: c('item', {
'-loading': loading || deleting,
'-doc': DOC.test(ext),
'-spreadsheet': SPREADSHEET.test(ext),
'-presentation': PRESENTATION.test(ext),
}),
href: !readonly && attachment.url,
target: !readonly && '_blank',
key: attachment.id
}, [
IMG.test(attachment.url) && <img src={attachment.url} className="thumbnail" />,
!readonly && Boolean(attachment.mime) && <div className="type">
{ext.toUpperCase()}
</div>,
Boolean(loading) && <ProgressBar progress={completed} />,
!readonly && !loading && !deleting && <div className="remove">
<button type="button"
className="plain-button"
title="Delete attachment"
onClick={() => this.handleRemove(attachment.id)}>
<Icon src="remove-primary" />
</button>
</div>
]);
})}
</div>
{input}
</div>
);
}
/**
* Trigger `input` from parent
*/
trigger() {
this.input.click();
}
handleSelect(evt) {
const original = evt.target.files;
// Files = File events from FileReader
// We'll use the base64 result to display
// the local version of the file while uploading
const files = Array.from(original).map((file) => {
return reader(file);
});
Promise.all(files).then(files => {
const attachments = files.map((evt, i) => {
return { id: uuid(), url: evt.target.result, mime: original[i].type };
});
this.setState({
uploading: put(this.state.uploading, objectify(attachments.map(attachment => attachment.id), true)),
progress: put(this.state.progress, objectify(attachments.map(attachment => attachment.id), 0))
});
this.props.onChange(
[...this.props.attachments, ...attachments]
);
attachments.forEach((attachment, i) => {
// We'll use FormData instead of a POJO because
// API requires a file instead of a blob or base64.
const payload = formdata({ file: original[i] });
axios.post(this.props.resource, payload, {
onUploadProgress: this.handleProgress(attachment.id)
}).then(res => {
const index = this.props.attachments
.findIndex(a => a.id === attachment.id);
this.setState({
uploading: del(this.state.uploading, attachment.id),
progress: del(this.state.progress, attachment.id)
});
this.props.onChange(
put(this.props.attachments, index, res.data.data)
);
toast(messages['attachment:upload.success']);
}, err => {
const response = err && err.response;
toast(getStatusMessage(response && response.status));
});
});
})
}
handleProgress(id) {
return (evt) => {
const progress = Math.round((evt.loaded * 100) / evt.total);
this.setState({ progress: put(this.state.progress, id, progress) });
}
}
handleRemove(id) {
if (!confirm('Are you sure to remove this attachment?')) {
return;
}
this.setState({
removing: put(this.state.removing, id, true)
});
axios.delete(`${this.props.resource}/${id}`)
.then(res => {
this.setState({
removing: del(this.state.removing, id)
});
this.props.onChange(
this.props.attachments.filter(a => a.id !== id)
);
toast(
messages['attachment:remove.success']
);
}, err => {
this.setState({
removing: del(this.state.removing, id)
});
toast(
getStatusMessage(err.response && err.response.status)
);
});
}
}
Attachments.propTypes = {
/**
* Files
*/
attachments: PropTypes.array,
/**
* The base end point
*/
resource: PropTypes.string,
/**
* Attachment callback handler
*/
onChange: PropTypes.func
};
export default Attachments;
@princeToker

Copy link
Copy Markdown

All test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment