├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── README.md ├── examples ├── .eslintrc ├── components │ ├── App.js │ ├── ImagePreview.js │ ├── ImageProgress.js │ ├── ImageResponse.js │ ├── ImageUploadDemo.js │ ├── SelectFileButton.js │ └── Vanilla.js ├── index.html └── index.js ├── package-lock.json ├── package.json ├── src ├── components │ ├── FileManager.js │ └── FileUploader.js └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": ["last 2 versions", "ie >= 9"] 6 | } 7 | }], 8 | "react" 9 | ], 10 | "plugins": [ 11 | "transform-object-rest-spread", 12 | "babel-plugin-transform-exponentiation-operator", 13 | "react-hot-loader/babel" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier", "prettier/react"], 3 | "env": { 4 | "jest": true 5 | }, 6 | "rules": { 7 | "react/jsx-filename-extension": 0, 8 | "react/sort-comp": 0, 9 | "react/no-unused-prop-types": 0, 10 | "react/forbid-prop-types": 0, 11 | "no-underscore-dangle": 0, 12 | "no-bitwise": 0, 13 | "no-unused-expressions": 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | npm-debug.log 4 | 5 | # Editor and other tmp files 6 | *.swp 7 | *.un~ 8 | *.iml 9 | *.ipr 10 | *.iws 11 | *.sublime-* 12 | .idea/* 13 | *.DS_Store 14 | 15 | # Build directories (Will be preserved by npm) 16 | dist 17 | build 18 | /style.css 19 | /style.css.map 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactJS File Uploader 2 | 3 | A flexible React component for uploading files. Supports multiple files, progress feedback and upload / abort controls. 4 | 5 | View Live Demo 6 | 7 | Features 8 | 9 | * Multiple files 10 | * Progress feedback 11 | * Upload / abort controls 12 | 13 | ## Example 14 | 15 | ```jsx 16 | class VanillaExample extends React.Component { 17 | 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | files: [], 22 | }; 23 | this.uploadFiles = this.uploadFiles.bind(this); 24 | this.uploadFile = this.uploadFile.bind(this); 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 | this.setState({files: this.state.files.concat(Array.from(event.target.files))})} 34 | multiple 35 | /> 36 | 39 | {this.uploadFiles} 40 | 41 |
42 | ) 43 | } 44 | 45 | uploadFiles(files) { 46 | return files.map(this.uploadFile); 47 | } 48 | 49 | uploadFile(file) { 50 | return ( 51 | 62 | {this.fileProgress} 63 | 64 | ) 65 | } 66 | 67 | static fileProgress({ 68 | 69 | /* 70 | References to the Event objects. 71 | Initial state is null and each propert gets assigned on Event. 72 | */ 73 | uploadReady, 74 | uploadStart, 75 | uploadProgress, 76 | uploadComplete, 77 | downloadStart, 78 | downloadProgress, 79 | downloadComplete, 80 | error, 81 | abort, 82 | timeout, 83 | 84 | /* 85 | The sequential state of the request 86 | enum { 87 | uploadReady, uploadStart, uploadProgress, uploadComplete, downloadStart 88 | downloadStart, downloadProgress, downloadComplete 89 | } 90 | */ 91 | requestState, 92 | 93 | /* 94 | Function references to start / abort request 95 | */ 96 | startUpload, 97 | abortRequest, 98 | 99 | /* 100 | Request Object reference (XMLHttpReqeust) 101 | */ 102 | request, 103 | 104 | /* 105 | Response text Object (JSON) 106 | */ 107 | response, 108 | 109 | /* 110 | Data of the file being uploaded (if readData props is true) 111 | */ 112 | fileData, 113 | 114 | }) { 115 | return ( 116 |
117 | {fileData && Preview} 118 | {startUpload && } 119 | {requestState && requestState} 120 |
121 | ) 122 | } 123 | 124 | } 125 | ``` 126 | 127 | ## Options FileUploader 128 | 129 | | Property | Type | Default | Description | 130 | | :------------------------------ | :----: | :----: | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | 131 | | children
_(required)_ | func: (state) => React.Node | | Returns state of FileUploader instance. See above example for state structure. | 132 | | file
_(required)_ | object: File | | File objects are generally retrieved from a FileList object returned as a result of a user selecting files using the element, from a drag and drop operation's DataTransfer object, or from the mozGetAsFile() API on an HTMLCanvasElement. | 133 | | url
_(required)_ | string | | Upload url endpoint. | 134 | | formData | object: { key: value } | `{}` | `key: value` formData to be sent with the request. If sending the file this needs to be explicit e.g. `formData={{file_field: file}}` | 135 | | autoUpload | bool | `false` | If `true` immediately start upload when file is passed to Component. | 136 | | readFile | bool | `false` | If `true` provides a reference to the file data in the component state returned by `children` | 137 | | onUploadReady | func: (XMLHttpRequest) => void |() => {} | Hook to `uploadReady` event. 138 | | onUploadStart | func: (ProgressEvent) => void |() => {} | Hook to `uploadStart` event. 139 | | onUploadProgress | func: (ProgressEvent) => void |() => {} | Hook to `uploadProgress` event. 140 | | onUploadComplete | func: (ProgressEvent) => void |() => {} | Hook to `uploadComplete` event. 141 | | onDownloadStart | func: (Event) => void |() => {} | Hook to `downloadStart` event. 142 | | onDownloadComplete | func: (Event) => void |() => {} | Hook to `downloadComplete` event. 143 | | onError | func: (Event) => void |() => {} | Hook to `error` event. 144 | | onAbort | func: (Event) => void |() => {} | Hook to `abort` event. 145 | | onTimeout | func: (Event) => void |() => {} | Hook to `timeout` event. 146 | 147 | 148 | ## Options FileManager 149 | 150 | The FileManager is an optional HOC that manages the files that are sent to the FileUploader. 151 | 152 | It ensures no duplicate are sent to be uploaded and provides each file a unique key attribute. 153 | 154 | | Property | Type | Default | Description | 155 | | :------------------------------ | :----: | :----: | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | 156 | | children
_(required)_ | func: (file) => React.Node | | Returns File from Array of File objects. | 157 | | files
_(required)_ | array: File | | Array or FileList of File objects. | 158 | 159 | 160 | ## License 161 | 162 | MIT 163 | -------------------------------------------------------------------------------- /examples/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": 0, 4 | "jsx-a11y/no-static-element-interactions": 0, 5 | "jsx-a11y/click-events-have-key-events": 0, 6 | "react/no-array-index-key": 0, 7 | "no-unused-vars": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AppBar from '@material-ui/core/AppBar'; 4 | import blue from '@material-ui/core/colors/blue'; 5 | import Icon from "@material-ui/core/es/Icon/Icon"; 6 | import Toolbar from "@material-ui/core/es/Toolbar/Toolbar"; 7 | import IconButton from "@material-ui/core/es/IconButton/IconButton"; 8 | import Typography from "@material-ui/core/es/Typography/Typography"; 9 | import MuiThemeProvider from "@material-ui/core/es/styles/MuiThemeProvider"; 10 | import {createMuiTheme} from '@material-ui/core/styles'; 11 | 12 | import ImageUploadDemo from './ImageUploadDemo'; 13 | 14 | const theme = createMuiTheme({ 15 | palette: { 16 | primary: { 17 | ...blue 18 | }, 19 | }, 20 | 21 | typography: { 22 | fontFamily: 'Open Sans', 23 | // fontSize: '5rem', 24 | }, 25 | }); 26 | 27 | const styles = { 28 | pageStyle: { 29 | margin: 25, 30 | padding: 25, 31 | }, 32 | typography: { 33 | marginBottom: 20, 34 | } 35 | 36 | }; 37 | 38 | const App = () => ( 39 | 40 | 41 | 42 | 43 | 48 | ReactJS File Uploader 49 | 50 | 51 |
52 | 55 | 61 | 62 | 63 | 64 | 65 | 66 | 69 | 75 | 76 | 77 | 78 |
79 | 80 |
81 |
82 |
83 | 84 | 89 | ReactJS File Uploader Demo 90 | 91 | 92 | 96 | Select files to upload to see it in action. 97 | 98 | 102 | Toggle the switches. 103 | 104 | 105 | 109 | View the events. 110 | 111 | 112 | 113 | 114 |
115 |
116 | ); 117 | 118 | export default App; 119 | -------------------------------------------------------------------------------- /examples/components/ImagePreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Paper from "@material-ui/core/es/Paper/Paper"; 4 | 5 | const ImagePreview = props => { 6 | 7 | const imgPreviewStyle = { 8 | position: 'absolute', 9 | backgroundImage: `url(${props.src})`, 10 | backgroundSize: 'cover', 11 | backgroundPosition: 'center', 12 | width: 150, 13 | height: 150, 14 | }; 15 | 16 | return 17 | }; 18 | 19 | ImagePreview.propTypes = { 20 | src: PropTypes.string.isRequired 21 | }; 22 | 23 | export default ImagePreview; -------------------------------------------------------------------------------- /examples/components/ImageProgress.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import CircularProgress from "@material-ui/core/es/CircularProgress/CircularProgress"; 5 | 6 | const ImageProgress = props => { 7 | 8 | const imgPreviewStyle = { 9 | position: 'absolute', 10 | backgroundImage: `url(${props.src})`, 11 | backgroundSize: 'cover', 12 | backgroundPosition: 'center', 13 | width: 150, 14 | height: 150, 15 | }; 16 | 17 | const imgProgressStyle = { 18 | position: 'absolute', 19 | width: '100%', 20 | height: `${100 - props.progress}%`, 21 | transition: 'height 0.5s', 22 | backgroundColor: 'white', 23 | opacity: 0.5 24 | }; 25 | 26 | const frontStyle = { 27 | backfaceVisibility: 'hidden', 28 | height: '100%', 29 | width: '100%', 30 | position: 'absolute', 31 | zIndex: 20, 32 | }; 33 | 34 | const backStyle = { 35 | transform: 'rotateY(180deg)', 36 | backgroundColor: 'whitesmoke', 37 | height: 'inherit', 38 | width: 'inherit', 39 | position: 'absolute', 40 | display: 'flex', 41 | justifyContent: 'center', 42 | alignItems: 'center', 43 | zIndex: 10, 44 | }; 45 | 46 | const progressCtrStyle = { 47 | transition: 'all 1.0s linear', 48 | transformStyle: 'preserve-3d', 49 | transform: `rotateY(${props.completed ? '180' : 0}deg)`, 50 | height: '100%', 51 | width: '100%', 52 | }; 53 | 54 | return ( 55 |
56 |
57 |
58 |
59 |
60 |
61 | 62 |
63 |
64 | ); 65 | 66 | }; 67 | 68 | ImageProgress.propTypes = { 69 | src: PropTypes.string.isRequired, 70 | progress: PropTypes.number.isRequired, 71 | completed: PropTypes.bool.isRequired 72 | }; 73 | 74 | export default ImageProgress; -------------------------------------------------------------------------------- /examples/components/ImageResponse.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Image} from 'cloudinary-react'; 3 | 4 | import CircularProgress from "@material-ui/core/es/CircularProgress/CircularProgress"; 5 | 6 | class ImageResponse extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | loaded: false, 12 | } 13 | }; 14 | 15 | render() { 16 | 17 | const imageStyle = { 18 | transition: 'all 0.5s', 19 | width: 'inherit', 20 | height: 'inherit', 21 | }; 22 | 23 | const frontStyle = { 24 | backfaceVisibility: 'hidden', 25 | height: 'inherit', 26 | width: 'inherit', 27 | position: 'absolute', 28 | zIndex: 20, 29 | display: 'flex', 30 | justifyContent: 'center', 31 | alignItems: 'center', 32 | backgroundColor: 'whitesmoke', 33 | }; 34 | 35 | const backStyle = { 36 | transform: 'rotateY(180deg)', 37 | height: 'inherit', 38 | width: 'inherit', 39 | position: 'absolute', 40 | zIndex: 10, 41 | }; 42 | 43 | const progressCtrStyle = { 44 | transform: `rotateY(${this.state.loaded ? '180' : 0}deg)`, 45 | transition: 'all 0.5s linear', 46 | transformStyle: 'preserve-3d', 47 | height: 'inherit', 48 | width: 'inherit', 49 | }; 50 | 51 | return ( 52 |
53 |
54 | 55 |
56 |
57 | { 61 | this.setState({loaded: true}) 62 | }} 63 | /> 64 |
65 |
66 | ); 67 | } 68 | } 69 | 70 | export default ImageResponse; -------------------------------------------------------------------------------- /examples/components/ImageUploadDemo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from '@material-ui/core/Button'; 4 | import Icon from "@material-ui/core/es/Icon/Icon"; 5 | import Badge from "@material-ui/core/es/Badge/Badge"; 6 | import Switch from "@material-ui/core/es/Switch/Switch"; 7 | import Paper from "@material-ui/core/es/Paper/Paper"; 8 | import Table from "@material-ui/core/es/Table/Table"; 9 | import TableHead from "@material-ui/core/es/TableHead/TableHead"; 10 | import TableRow from "@material-ui/core/es/TableRow/TableRow"; 11 | import TableCell from "@material-ui/core/es/TableCell/TableCell"; 12 | import TableBody from "@material-ui/core/es/TableBody/TableBody"; 13 | import LinearProgress from "@material-ui/core/es/LinearProgress/LinearProgress"; 14 | import CircularProgress from "@material-ui/core/es/CircularProgress/CircularProgress"; 15 | import FormControlLabel from "@material-ui/core/es/FormControlLabel/FormControlLabel"; 16 | 17 | import ImagePreview from "./ImagePreview"; 18 | import ImageProgress from "./ImageProgress"; 19 | import ImageResponse from "./ImageResponse"; 20 | import SelectFileButton from "./SelectFileButton"; 21 | import FileManager from '../../src/components/FileManager'; 22 | import FileUploader from '../../src/components/FileUploader'; 23 | 24 | const CLOUD_NAME = 'dpdenton'; 25 | const CLOUD_URL = `https://api.cloudinary.com/v1_1/${CLOUD_NAME}/upload`; 26 | 27 | const styles = { 28 | 29 | containerStyle: { 30 | border: 'thin solid rgb(221, 221, 221)', 31 | display: 'flex', 32 | flexWrap: 'wrap', 33 | justifyContent: 'center', 34 | }, 35 | controlsStyle: { 36 | padding: 25, 37 | backgroundColor: 'whitesmoke', 38 | textAlign: 'center', 39 | }, 40 | controlStyle: { 41 | margin: 10, 42 | 43 | }, 44 | fileStyle: { 45 | width: '100%', 46 | height: '100%', 47 | border: 'thin solid #eee', 48 | borderRadius: 10, 49 | overflow: 'hidden', 50 | cursor: 'pointer', 51 | }, 52 | wrapperStyle: { 53 | position: 'relative', 54 | }, 55 | 56 | buttonStyle: { 57 | position: 'absolute', 58 | width: 44, 59 | height: 44, 60 | right: 16, 61 | bottom: 16, 62 | }, 63 | 64 | progressStyle: { 65 | position: 'absolute', 66 | width: 52, 67 | height: 52, 68 | bottom: 12, 69 | right: 12, 70 | zIndex: 1, 71 | color: 'white', 72 | }, 73 | 74 | eventContainer: { 75 | padding: 25, 76 | backgroundColor: 'whitesmoke' 77 | } 78 | }; 79 | 80 | class ImageUploadDemo extends React.Component { 81 | 82 | constructor(props) { 83 | super(props); 84 | this.state = { 85 | files: [], 86 | events: {}, 87 | progress: {}, 88 | selectedIndex: null, 89 | multiple: true, 90 | autoUpload: false, 91 | showEvents: false, 92 | }; 93 | 94 | this.uploadFile = this.uploadFile.bind(this); 95 | } 96 | 97 | render() { 98 | 99 | const totalProgress = Object.values(this.state.progress).reduce((a, b) => a + b, 0); 100 | const progress = totalProgress / Object.keys(this.state.progress).length * 100 || 0; 101 | 102 | return ( 103 |
104 | 108 | 109 |
110 |
111 | { 114 | this.setState({files: this.state.files.concat(Array.from(event.target.files))}) 115 | }} 116 | button={( 117 | 126 | )} 127 | /> 128 |
129 | 130 | this.setState({multiple: !this.state.multiple})} 137 | /> 138 | } 139 | label="Allow Multiple" 140 | /> 141 | this.setState({autoUpload: !this.state.autoUpload})} 148 | /> 149 | } 150 | label="Auto Upload" 151 | /> 152 | this.setState({showEvents: !this.state.showEvents})} 159 | /> 160 | } 161 | label="Show Events" 162 | /> 163 |
164 | 165 | 168 | {files =>
{files.map(this.uploadFile)}
} 169 |
170 | 171 | {this.state.showEvents 172 | && this.selectedIndex !== null 173 | && Object.keys(this.state.events).length > 0 174 | && this.renderEvents()} 175 |
176 | ); 177 | } 178 | 179 | uploadFile(file) { 180 | 181 | return ( 182 | { 194 | const {progress} = this.state; 195 | progress[file.key] = 0; 196 | this.setState({progress, selectedIndex: file.key}); 197 | this.addTransitionState(event, FileUploader.UPLOAD_READY, file.key); 198 | }} 199 | onUploadStart={event => { 200 | this.addTransitionState(event, FileUploader.UPLOAD_START, file.key); 201 | }} 202 | onUploadProgress={event => { 203 | const {progress} = this.state; 204 | progress[file.key] = event.total ? event.loaded / event.total : 0; 205 | this.setState({progress}); 206 | this.addTransitionState(event, FileUploader.UPLOAD_PROGRESS, file.key); 207 | }} 208 | onUploadComplete={event => { 209 | this.addTransitionState(event, FileUploader.UPLOAD_COMPLETE, file.key); 210 | }} 211 | onDownloadStart={event => { 212 | this.addTransitionState(event, FileUploader.DOWNLOAD_START, file.key); 213 | }} 214 | onDownloadProgress={event => { 215 | this.addTransitionState(event, FileUploader.DOWNLOAD_PROGRESS, file.key); 216 | }} 217 | onDownloadComplete={event => { 218 | const {progress} = this.state; 219 | delete progress[file.key]; 220 | this.setState({progress}); 221 | this.addTransitionState(event, FileUploader.DOWNLOAD_COMPLETE, file.key); 222 | }} 223 | >{data => { 224 | 225 | const fileContainerStyle = { 226 | width: 150, 227 | height: 150, 228 | margin: 25, 229 | }; 230 | 231 | return ( 232 |
{ 235 | this.setState({selectedIndex: file.key}); 236 | }} 237 | > 238 |
239 | {ImageUploadDemo.renderImage(data)} 240 |
241 |
242 | {ImageUploadDemo.renderButton(data)} 243 |
244 | 245 |
246 | ); 247 | }} 248 |
249 | ) 250 | }; 251 | 252 | static renderButton(data) { 253 | 254 | switch (data.requestState) { 255 | 256 | case FileUploader.UPLOAD_READY: 257 | return ( 258 |
259 | 267 |
268 | ); 269 | 270 | case FileUploader.ABORT: 271 | case FileUploader.UPLOAD_START: 272 | case FileUploader.UPLOAD_PROGRESS: { 273 | 274 | const progress = data.uploadProgress 275 | ? Math.floor(data.uploadProgress.loaded / data.uploadProgress.total * 100) 276 | : 0; 277 | 278 | return ( 279 |
280 | 288 | 292 |
293 | ); 294 | } 295 | 296 | case FileUploader.UPLOAD_COMPLETE: 297 | case FileUploader.DOWNLOAD_PROGRESS: 298 | case FileUploader.DOWNLOAD_COMPLETE: 299 | return ( 300 |
301 | 308 |
309 | ); 310 | 311 | case FileUploader.ERROR: 312 | return

Error

; 313 | 314 | default: 315 | return

Something has gone wrong!

; 316 | } 317 | 318 | } 319 | 320 | static renderImage(data) { 321 | 322 | switch (data.requestState) { 323 | 324 | case FileUploader.UPLOAD_READY: 325 | return ( 326 | 329 | ); 330 | 331 | case FileUploader.UPLOAD_START: 332 | case FileUploader.UPLOAD_PROGRESS: 333 | case FileUploader.UPLOAD_COMPLETE: 334 | case FileUploader.ABORT: { 335 | 336 | const progress = data.uploadProgress 337 | ? Math.floor(data.uploadProgress.loaded / data.uploadProgress.total * 100) 338 | : 0; 339 | 340 | return ( 341 | 346 | ); 347 | } 348 | 349 | case FileUploader.DOWNLOAD_PROGRESS: 350 | case FileUploader.DOWNLOAD_COMPLETE: 351 | return ( 352 | 361 | ); 362 | default: 363 | return

Something has gone wrong!

; 364 | } 365 | } 366 | 367 | renderEvents() { 368 | 369 | return ( 370 |
371 | 372 | 373 | 374 | 375 | Event Name 376 | Event Object 377 | 378 | 379 | 380 | {this.state.events[this.state.selectedIndex] 381 | .filter(event => event.eventName !== null) 382 | .reduce((events, event) => { 383 | const existingEvent = events.find(t => t.eventName === event.eventName); 384 | if (existingEvent) { 385 | existingEvent.count += 1; 386 | } else { 387 | events.push({ 388 | count: 1, 389 | ...event, 390 | }); 391 | } 392 | return events; 393 | }, []) 394 | .map(event => ( 395 | 396 | 397 | {} 402 | {event.eventName} 403 | 404 | 405 | {event.eventObject.constructor.name} 406 | 407 | 408 | ) 409 | )} 410 | 411 |
412 |
413 |
414 | ) 415 | } 416 | 417 | addTransitionState(event, eventName, index) { 418 | 419 | const {events} = this.state; 420 | 421 | const eventState = { 422 | eventName, 423 | eventObject: event, 424 | eventTimestamp: +new Date(), 425 | }; 426 | 427 | if (!events[index]) { 428 | events[index] = [eventState]; 429 | } else { 430 | events[index].push(eventState); 431 | } 432 | 433 | this.setState({events}) 434 | } 435 | } 436 | 437 | export default ImageUploadDemo; 438 | -------------------------------------------------------------------------------- /examples/components/SelectFileButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const SelectFileButton = props => ( 5 | 6 | 7 | 15 | { 16 | global.document.getElementById('___SelectFileButton___').click(); 17 | }}> 18 | {props.button} 19 | 20 | 21 | ); 22 | 23 | SelectFileButton.propTypes = { 24 | onChange: PropTypes.func.isRequired, 25 | button: PropTypes.node, 26 | multiple: PropTypes.bool, 27 | }; 28 | 29 | SelectFileButton.defaultProps = { 30 | button: , 31 | multiple: true, 32 | }; 33 | 34 | export default SelectFileButton; -------------------------------------------------------------------------------- /examples/components/Vanilla.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FileManager from "../../src/components/FileManager"; 3 | import FileUploader from "../../src/components/FileUploader"; 4 | 5 | class Vanilla extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | files: [], 11 | }; 12 | this.uploadFiles = this.uploadFiles.bind(this); 13 | this.uploadFile = this.uploadFile.bind(this); 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 | this.setState({files: this.state.files.concat(Array.from(event.target.files))})} 23 | multiple 24 | /> 25 | {this.uploadFiles} 28 | 29 |
30 | ) 31 | } 32 | 33 | uploadFiles(files) { 34 | return files.map(this.uploadFile); 35 | } 36 | 37 | uploadFile(file) { 38 | return ( 39 | 50 | {this.fileProgress} 51 | 52 | ) 53 | } 54 | 55 | static fileProgress({ 56 | 57 | /* 58 | References to the Event objects. 59 | Initial state is null and gets assign on each Event. 60 | */ 61 | uploadReady, 62 | uploadStart, 63 | uploadProgress, 64 | uploadComplete, 65 | downloadStart, 66 | downloadProgress, 67 | downloadComplete, 68 | error, 69 | abort, 70 | timeout, 71 | 72 | /* 73 | The sequential state of the request 74 | enum { 75 | uploadReady, uploadStart, uploadProgress, uploadComplete, downloadStart 76 | downloadStart, downloadProgress, downloadComplete 77 | } 78 | */ 79 | requestState, // 80 | 81 | /* 82 | Function references to start / abort request 83 | */ 84 | startUpload, 85 | abortRequest, 86 | 87 | /* 88 | Request Object reference (XMLHttpReqeust) 89 | */ 90 | request, 91 | 92 | /* 93 | Response text Object (JSON) 94 | */ 95 | response, 96 | 97 | /* 98 | Data of the file being uploaded (if readData props is true) 99 | */ 100 | fileData, 101 | 102 | }) { 103 | return ( 104 |
105 | {fileData && Preview} 106 | {startUpload && } 107 | {requestState && requestState} 108 |
109 | ) 110 | } 111 | 112 | } 113 | 114 | export default Vanilla; -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReactJS File Uploader 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {AppContainer} from 'react-hot-loader'; // eslint-disable-line import/no-extraneous-dependencies 4 | 5 | import App from './components/App'; 6 | 7 | const rootEl = global.document.getElementById('app'); 8 | const render = AppComponent => { 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | rootEl 14 | ); 15 | }; 16 | 17 | /* eslint-disable global-require, import/newline-after-import */ 18 | render(App); 19 | if (module.hot) 20 | module.hot.accept(App, () => render(require('./components/App').default)); 21 | /* eslint-enable global-require, import/newline-after-import */ 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-file-uploader", 3 | "version": "0.1.7", 4 | "description": "Flexible React file uploader supporting progress feedback, multiple images and upload/abort controls.", 5 | "scripts": { 6 | "build": "npm run clean && cross-env NODE_ENV=production TARGET=umd webpack --bail", 7 | "build:demo": "npm run clean:demo && cross-env NODE_ENV=production TARGET=demo webpack --bail", 8 | "demo": "npm run clean:demo && cross-env NODE_ENV=development TARGET=demo webpack --bail", 9 | "clean": "rimraf dist style.css style.css.map", 10 | "clean:demo": "rimraf build", 11 | "start": "cross-env NODE_ENV=development TARGET=development webpack-dev-server --inline --hot", 12 | "lint": "eslint .", 13 | "prettier": "prettier --single-quote --trailing-comma es5 --write \"./**/*.{md,js,css}\" \"!./{build,dist}/**\" \"!./style.css*\"", 14 | "prepublishOnly": "npm run lint && npm run build", 15 | "test": "jest", 16 | "test:watch": "jest --watchAll", 17 | "deploy": "npm run build:demo && gh-pages -d build" 18 | }, 19 | "main": "dist/main.js", 20 | "files": [ 21 | "dist" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/dpdenton/reactjs-file-uploader.git" 26 | }, 27 | "authors": [ 28 | "David Denton" 29 | ], 30 | "license": "MIT", 31 | "dependencies": { 32 | "prop-types": "^15.6.2" 33 | }, 34 | "peerDependencies": { 35 | "react": "^15.5.0 || ^16.0.0", 36 | "react-dom": "^15.5.0 || ^16.0.0" 37 | }, 38 | "devDependencies": { 39 | "@material-ui/core": "^1.3.0", 40 | "autoprefixer": "^8.6.4", 41 | "babel-cli": "^6.10.1", 42 | "babel-core": "^6.26.3", 43 | "babel-loader": "^7.1.4", 44 | "babel-plugin-transform-exponentiation-operator": "^6.8.0", 45 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 46 | "babel-preset-env": "^1.7.0", 47 | "babel-preset-react": "^6.11.1", 48 | "cloudinary-react": "^1.0.6", 49 | "coveralls": "^3.0.2", 50 | "cross-env": "^5.2.0", 51 | "css-loader": "^0.28.11", 52 | "enzyme": "^3.3.0", 53 | "enzyme-adapter-react-16": "^1.1.1", 54 | "enzyme-to-json": "^3.3.4", 55 | "eslint": "^4.19.1", 56 | "eslint-config-airbnb": "^16.1.0", 57 | "eslint-config-prettier": "^2.9.0", 58 | "eslint-plugin-import": "^2.13.0", 59 | "eslint-plugin-jsx-a11y": "^6.0.3", 60 | "eslint-plugin-react": "^7.10.0", 61 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 62 | "file-loader": "^1.1.11", 63 | "gh-pages": "^1.2.0", 64 | "html-webpack-plugin": "^3.2.0", 65 | "jest": "^22.4.4", 66 | "jest-enzyme": "^6.0.2", 67 | "postcss-loader": "^2.1.5", 68 | "prettier": "^1.13.7", 69 | "react": "^16.4.1", 70 | "react-dom": "^16.4.1", 71 | "react-hot-loader": "^4.3.3", 72 | "rimraf": "^2.5.3", 73 | "style-loader": "^0.21.0", 74 | "uglifyjs-webpack-plugin": "^1.2.7", 75 | "webpack": "^4.14.0", 76 | "webpack-cli": "^2.1.5", 77 | "webpack-dev-server": "^3.1.4", 78 | "webpack-node-externals": "^1.7.2" 79 | }, 80 | "keywords": [ 81 | "react", 82 | "react-component", 83 | "file", 84 | "image", 85 | "upload", 86 | "uploader" 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /src/components/FileManager.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class FileManager extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this._fileMap = {}; 9 | this._addFileToMap = this._addFileToMap.bind(this); 10 | } 11 | 12 | componentWillMount() { 13 | this.props.files.forEach(this._addFileToMap); 14 | } 15 | 16 | componentWillReceiveProps(props) { 17 | props.files.forEach(this._addFileToMap); 18 | } 19 | 20 | render() { 21 | const keys = []; 22 | const files = this.props.files.filter(file => { 23 | const key = FileManager.generateKey(file.name + file.size + file.lastModified); 24 | Object.assign(file, {key}); 25 | return keys.indexOf(key) === -1 ? keys.push(key) : false; 26 | }); 27 | return this.props.children(files); 28 | } 29 | 30 | _addFileToMap(file) { 31 | const key = FileManager.generateKey(file.name + file.size + file.lastModified); 32 | this._fileMap[key] = file; 33 | } 34 | 35 | static generateKey(s) { 36 | let hash = 0; 37 | if (s.length === 0) return hash; 38 | for (let i = 0; i < s.length; i += 1) { 39 | const chr = s.charCodeAt(i); 40 | hash = ((hash << 5) - hash) + chr; 41 | hash |= 0; // Convert to 32bit integer 42 | } 43 | return hash; 44 | }; 45 | } 46 | 47 | FileManager.propTypes = { 48 | files: PropTypes.array.isRequired, 49 | children: PropTypes.func.isRequired 50 | }; 51 | 52 | export default FileManager; -------------------------------------------------------------------------------- /src/components/FileUploader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class FileUploader extends React.Component { 5 | 6 | static get UPLOAD_READY() { 7 | return 'uploadReady'; 8 | } 9 | 10 | static get UPLOAD_START() { 11 | return 'uploadStart'; 12 | } 13 | 14 | static get UPLOAD_PROGRESS() { 15 | return 'uploadProgress'; 16 | } 17 | 18 | static get UPLOAD_COMPLETE() { 19 | return 'uploadComplete'; 20 | } 21 | 22 | static get DOWNLOAD_START() { 23 | return 'downloadStart'; 24 | } 25 | 26 | static get DOWNLOAD_PROGRESS() { 27 | return 'downloadProgress'; 28 | } 29 | 30 | static get DOWNLOAD_COMPLETE() { 31 | return 'downloadComplete'; 32 | } 33 | 34 | static get ERROR() { 35 | return 'error'; 36 | } 37 | 38 | static get ABORT() { 39 | return 'abort'; 40 | } 41 | 42 | static get TIMEOUT() { 43 | return 'timeout'; 44 | } 45 | 46 | constructor(props) { 47 | super(props); 48 | this.state = { 49 | 50 | // sequential request state 51 | uploadReady: null, 52 | uploadStart: null, 53 | uploadProgress: null, 54 | uploadComplete: null, 55 | downloadStart: null, 56 | downloadProgress: null, 57 | downloadComplete: null, 58 | 59 | // error state 60 | error: null, 61 | abort: null, 62 | timeout: null, 63 | 64 | // helper references 65 | requestState: null, 66 | request: null, 67 | response: null, 68 | readyState: null, 69 | fileData: null, 70 | file: props.file, 71 | 72 | // func references to start / abort request 73 | startUpload: null, 74 | abortRequest: null, 75 | }; 76 | this._onEvent = this._onEvent.bind(this); 77 | this.onUploadStart = this.onUploadStart.bind(this); 78 | this.onUploadProgress = this.onUploadProgress.bind(this); 79 | this.onUploadComplete = this.onUploadComplete.bind(this); 80 | this.onDownloadStart = this.onDownloadStart.bind(this); 81 | this.onDownloadProgress = this.onDownloadProgress.bind(this); 82 | this.onDownloadComplete = this.onDownloadComplete.bind(this); 83 | this.onError = this.onError.bind(this); 84 | this.onAbort = this.onAbort.bind(this); 85 | this.onTimeout = this.onTimeout.bind(this); 86 | this.onReadyStateChange = this.onReadyStateChange.bind(this); 87 | this.startUpload = this.startUpload.bind(this); 88 | this.abortRequest = this.abortRequest.bind(this); 89 | } 90 | 91 | componentWillMount() { 92 | this._prepareRequest(); 93 | } 94 | 95 | componentDidMount() { 96 | if (this.props.readFile) { 97 | const reader = new global.FileReader(); 98 | reader.onload = e => this.onFileDataReady(e); 99 | reader.readAsDataURL(this.props.file); 100 | } 101 | this.onUploadReady(this.xhr); 102 | this.props.autoUpload && this.startUpload(); 103 | } 104 | 105 | render() { 106 | return this.props.children(this.state) 107 | } 108 | 109 | componentWillReceiveProps(nextProps) { 110 | // make sure uploaded hasn't already started and that the upload prop has changed to true; 111 | if (this.state.uploadStart === null && nextProps.autoUpload && nextProps.autoUpload !== this.props.autoUpload) { 112 | this.startUpload(); 113 | } 114 | } 115 | 116 | componentWillUnmount() { 117 | this.xhr.upload.removeEventListener("loadstart", this.onUploadStart); 118 | this.xhr.upload.removeEventListener("progress", this.onUploadProgress); 119 | this.xhr.upload.removeEventListener("loadend", this.onUploadComplete); 120 | this.xhr.removeEventListener("error", this.onError); 121 | this.xhr.removeEventListener("abort", this.onAbort); 122 | this.xhr.removeEventListener("timeout", this.onTimeout); 123 | this.xhr.onreadystatechange = null; 124 | } 125 | 126 | onFileDataReady(event) { 127 | // don't call _event because it isn't part of the sequential request state. 128 | this.setState({ 129 | fileData: event.target.result, 130 | }); 131 | this.props.onFileDataReady(event, this.state); 132 | } 133 | 134 | onUploadReady(event) { 135 | // provide ref to upload file if not immediately invoked. 136 | const newState = !this.props.autoUpload 137 | ? {file: this.props.file, startUpload: this.startUpload} 138 | : {file: this.props.file}; 139 | this._onEvent(FileUploader.UPLOAD_READY, event, newState); 140 | } 141 | 142 | onUploadStart(event) { 143 | const newState = { 144 | request: this.xhr, 145 | startUpload: null, 146 | abortRequest: this.abortRequest, 147 | }; 148 | this._onEvent(FileUploader.UPLOAD_START, event, newState); 149 | } 150 | 151 | onUploadProgress(event) { 152 | this._onEvent(FileUploader.UPLOAD_PROGRESS, event); 153 | }; 154 | 155 | onUploadComplete(event) { 156 | this._onEvent(FileUploader.UPLOAD_COMPLETE, event); 157 | } 158 | 159 | onDownloadStart(event) { 160 | this._onEvent(FileUploader.DOWNLOAD_START, event); 161 | } 162 | 163 | onDownloadProgress(event) { 164 | const newState = { 165 | response: JSON.parse(event.currentTarget.responseText), 166 | }; 167 | this._onEvent(FileUploader.DOWNLOAD_PROGRESS, event, newState); 168 | } 169 | 170 | onDownloadComplete(event) { 171 | const newState = { 172 | response: JSON.parse(event.currentTarget.responseText), 173 | abortRequest: null, 174 | }; 175 | this._onEvent(FileUploader.DOWNLOAD_COMPLETE, event, newState); 176 | } 177 | 178 | onError(event) { 179 | this._onEvent(FileUploader.ERROR, event); 180 | } 181 | 182 | onAbort(event) { 183 | const newState = { 184 | abortRequest: null, 185 | }; 186 | this._onEvent(FileUploader.ABORT, event, newState); 187 | } 188 | 189 | onTimeout(event) { 190 | this._onEvent(FileUploader.TIMEOUT, event); 191 | } 192 | 193 | onReadyStateChange(event) { 194 | this.props.onReadyStateChange(event); 195 | switch (event.currentTarget.readyState) { 196 | case 2: 197 | this.onDownloadStart(event); 198 | break; 199 | case 3: 200 | this.onDownloadProgress(event); 201 | break; 202 | case 4: 203 | this.onDownloadComplete(event); 204 | break; 205 | default: 206 | break; 207 | } 208 | } 209 | 210 | abortRequest() { 211 | this.xhr.abort(); 212 | }; 213 | 214 | startUpload() { 215 | const formData = new global.FormData(); 216 | Object.keys(this.props.formData).forEach(key => formData.append(key, this.props.formData[key])); 217 | this.xhr.send(formData); 218 | }; 219 | 220 | _prepareRequest() { 221 | // safe to do this before component has mounted as listeners aren't attached 222 | // when onreadystatechanges for 'UNSENT' and 'OPEN'. 223 | this.xhr = new global.XMLHttpRequest(); 224 | this.xhr.open(this.props.method, this.props.url, true); 225 | Object.keys(this.props.headers).forEach(key => this.xhr.setRequestHeader(key, this.props.headers[key])); 226 | 227 | this.xhr.upload.addEventListener("loadstart", this.onUploadStart); 228 | this.xhr.upload.addEventListener("progress", this.onUploadProgress); 229 | this.xhr.upload.addEventListener("loadend", this.onUploadComplete); 230 | this.xhr.addEventListener("error", this.onError); 231 | this.xhr.addEventListener("abort", this.onAbort); 232 | this.xhr.addEventListener("timeout", this.onTimeout); 233 | this.xhr.onreadystatechange = this.onReadyStateChange; 234 | } 235 | 236 | _onEvent(eventName, event, newState = {}) { 237 | const eventState = { 238 | [eventName]: event, 239 | requestState: eventName, 240 | }; 241 | this.props[FileUploader._onEventName(eventName)](event, this.state); 242 | this.setState(Object.assign(newState, eventState)); 243 | } 244 | 245 | static _onEventName(eventName) { 246 | return `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`; 247 | } 248 | } 249 | 250 | FileUploader.propTypes = { 251 | 252 | // File to be uploaded 253 | file: PropTypes.instanceOf(global.File).isRequired, 254 | 255 | // url to POST to 256 | url: PropTypes.string.isRequired, 257 | 258 | // upload image immediately 259 | autoUpload: PropTypes.bool, 260 | 261 | // get file contents (to display preview image etc) 262 | readFile: PropTypes.bool, 263 | 264 | // request method 265 | method: PropTypes.string, 266 | 267 | // headers 268 | headers: PropTypes.object, 269 | 270 | // form data 271 | formData: PropTypes.object, 272 | 273 | // request upload events 274 | onUploadReady: PropTypes.func, 275 | onUploadStart: PropTypes.func, 276 | onUploadProgress: PropTypes.func, 277 | onUploadComplete: PropTypes.func, 278 | onDownloadStart: PropTypes.func, 279 | onDownloadProgress: PropTypes.func, 280 | onDownloadComplete: PropTypes.func, 281 | onReadyStateChange: PropTypes.func, 282 | onFileDataReady: PropTypes.func, 283 | 284 | // request events 285 | onError: PropTypes.func, 286 | onAbort: PropTypes.func, 287 | onTimeout: PropTypes.func, 288 | 289 | children: PropTypes.func.isRequired, 290 | }; 291 | 292 | FileUploader.defaultProps = { 293 | 294 | autoUpload: false, 295 | readFile: false, 296 | method: 'POST', 297 | headers: {'X-Requested-With': 'XMLHttpRequest'}, 298 | formData: {}, 299 | onUploadReady: () => { 300 | }, 301 | onUploadStart: () => { 302 | }, 303 | onUploadProgress: () => { 304 | }, 305 | onUploadComplete: () => { 306 | }, 307 | onDownloadStart: () => { 308 | }, 309 | onDownloadProgress: () => { 310 | }, 311 | onDownloadComplete: () => { 312 | }, 313 | onError: event => { 314 | global.alert(event.currentTarget.statusText || 'Something has gone wrong.'); 315 | }, 316 | onAbort: () => { 317 | }, 318 | onTimeout: () => { 319 | }, 320 | onReadyStateChange: () => { 321 | }, 322 | onFileDataReady: () => { 323 | }, 324 | }; 325 | 326 | 327 | export default FileUploader; 328 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export {default as FileManager} from './components/FileManager'; 2 | export {default as FileUploader} from './components/FileUploader'; 3 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const autoprefixer = require('autoprefixer'); 4 | const nodeExternals = require('webpack-node-externals'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 7 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | 9 | const target = process.env.TARGET || 'umd'; 10 | 11 | const styleLoader = { 12 | loader: 'style-loader', 13 | options: {insertAt: 'top'}, 14 | }; 15 | 16 | const fileLoader = { 17 | loader: 'file-loader', 18 | options: {name: 'static/[name].[ext]'}, 19 | }; 20 | 21 | const postcssLoader = { 22 | loader: 'postcss-loader', 23 | options: { 24 | plugins: () => [autoprefixer()], 25 | }, 26 | }; 27 | 28 | const cssLoader = { 29 | loader: 'css-loader', 30 | options: { 31 | importLoaders: true, 32 | }, 33 | }; 34 | 35 | const defaultCssLoaders = [cssLoader, postcssLoader]; 36 | 37 | const cssLoaders = 38 | target !== 'development' && target !== 'demo' 39 | ? ExtractTextPlugin.extract({ 40 | fallback: styleLoader, 41 | use: defaultCssLoaders, 42 | }) 43 | : [styleLoader, ...defaultCssLoaders]; 44 | 45 | const config = { 46 | mode: 'production', 47 | entry: {'dist/main': './src/index'}, 48 | output: { 49 | path: __dirname, 50 | filename: '[name].js', 51 | libraryTarget: 'umd', 52 | library: 'ReactImageLightbox', 53 | }, 54 | devtool: 'source-map', 55 | plugins: [ 56 | new webpack.EnvironmentPlugin({NODE_ENV: 'production'}), 57 | new webpack.optimize.OccurrenceOrderPlugin(), 58 | new ExtractTextPlugin('style.css'), 59 | ], 60 | module: { 61 | rules: [ 62 | { 63 | test: /\.jsx?$/, 64 | use: ['babel-loader'], 65 | exclude: path.join(__dirname, 'node_modules'), 66 | }, 67 | { 68 | test: /\.css$/, 69 | use: cssLoaders, 70 | exclude: [path.join(__dirname, 'examples')], 71 | }, 72 | { 73 | test: /\.css$/, 74 | use: [styleLoader, ...defaultCssLoaders], 75 | include: path.join(__dirname, 'examples'), 76 | }, 77 | ], 78 | }, 79 | }; 80 | 81 | switch (target) { 82 | case 'umd': 83 | // Exclude library dependencies from the bundle 84 | config.externals = [ 85 | nodeExternals({ 86 | // load non-javascript files with extensions, presumably via loaders 87 | whitelist: [/\.(?!(?:jsx?|json)$).{1,5}$/i], 88 | }), 89 | ]; 90 | 91 | // Keep the minimizer from mangling variable names 92 | // (we keep minimization enabled to remove dead code) 93 | config.optimization = { 94 | minimizer: [ 95 | new UglifyJSPlugin({ 96 | uglifyOptions: { 97 | mangle: false, 98 | compress: { 99 | warnings: false, 100 | }, 101 | output: { 102 | beautify: true, 103 | comments: true, 104 | }, 105 | }, 106 | }), 107 | ], 108 | }; 109 | break; 110 | case 'development': 111 | config.mode = 'development'; 112 | config.devtool = 'eval-source-map'; 113 | config.module.rules.push({ 114 | test: /\.(jpe?g|png|gif|ico|svg)$/, 115 | use: [fileLoader], 116 | exclude: path.join(__dirname, 'node_modules'), 117 | }); 118 | config.entry = [ 119 | 'react-hot-loader/patch', 120 | './examples/index' 121 | ]; 122 | config.output = { 123 | path: path.join(__dirname, 'build'), 124 | filename: 'static/[name].js', 125 | }; 126 | config.plugins = [ 127 | new HtmlWebpackPlugin({ 128 | inject: true, 129 | template: './examples/index.html', 130 | }), 131 | new webpack.EnvironmentPlugin({NODE_ENV: 'development'}), 132 | new webpack.NoEmitOnErrorsPlugin(), 133 | ]; 134 | config.devServer = { 135 | contentBase: path.join(__dirname, 'build'), 136 | port: process.env.PORT || 3001, 137 | host: '0.0.0.0', 138 | disableHostCheck: true, 139 | stats: 'minimal', 140 | }; 141 | 142 | break; 143 | case 'demo': 144 | config.module.rules.push({ 145 | test: /\.(jpe?g|png|gif|ico|svg)$/, 146 | use: [fileLoader], 147 | exclude: path.join(__dirname, 'node_modules'), 148 | }); 149 | config.entry = './examples/index'; 150 | config.output = { 151 | path: path.join(__dirname, 'build'), 152 | filename: 'static/[name].js', 153 | }; 154 | config.plugins = [ 155 | new HtmlWebpackPlugin({ 156 | inject: true, 157 | template: './examples/index.html', 158 | }), 159 | new webpack.EnvironmentPlugin({NODE_ENV: 'production'}), 160 | ]; 161 | 162 | break; 163 | default: 164 | } 165 | 166 | module.exports = config; 167 | --------------------------------------------------------------------------------