Last active
March 14, 2022 09:53
-
-
Save srph/9a763d3c6b5464233243bb756654f2dc to your computer and use it in GitHub Desktop.
React: File upload component example (attachments)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
All test