├── .editorconfig ├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── example.html ├── gulpfile.js ├── package.json ├── upload.php ├── vue.file-upload.js └── vue.pretty-bytes.js /.editorconfig: -------------------------------------------------------------------------------- 1 | ; top-most EditorConfig file 2 | root = true 3 | 4 | ; Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # go away osx file 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # Commenting this out is preferred by some people, see 27 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | 33 | # workspace files are user-specific 34 | *.sublime-workspace 35 | 36 | # project files should be checked into the repository, unless a significant 37 | # proportion of contributors will probably not be using SublimeText 38 | # *.sublime-project 39 | 40 | #sftp configuration file 41 | sftp-config.json 42 | 43 | uploads/* 44 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Settings 3 | "passfail": false, // Stop on first error. 4 | "maxerr": 100, // Maximum error before stopping. 5 | // Predefined globals whom JSHint will ignore. 6 | "browser": true, // Standard browser globals e.g. `window`, `document`. 7 | "node": false, 8 | "rhino": false, 9 | "couch": false, 10 | "wsh": true, // Windows Scripting Host. 11 | "jquery": true, 12 | "ender": true, 13 | "prototypejs": false, 14 | "mootools": false, 15 | "dojo": false, 16 | "predef": [ // Custom globals. 17 | "Modernizr" 18 | //"anotherCoolGlobal", 19 | //"iLoveDouglas" 20 | ], 21 | // Development. 22 | "debug": false, // Allow debugger statements e.g. browser breakpoints. 23 | "devel": true, // Allow developments statements e.g. `console.log();`. 24 | // ECMAScript 5. 25 | "es5": true, // Allow ECMAScript 5 syntax. 26 | "strict": false, // Require `use strict` pragma in every file. 27 | "globalstrict": false, // Allow global "use strict" (also enables 'strict'). 28 | // The Good Parts. 29 | "asi": true, // Tolerate Automatic Semicolon Insertion (no semicolons). 30 | "laxbreak": true, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 31 | "bitwise": true, // Prohibit bitwise operators (&, |, ^, etc.). 32 | "boss": false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 33 | "curly": true, // Require {} for every new block or scope. 34 | "eqeqeq": false, // Require triple equals i.e. `===`. 35 | "eqnull": false, // Tolerate use of `== null`. 36 | "evil": false, // Tolerate use of `eval`. 37 | "expr": false, // Tolerate `ExpressionStatement` as Programs. 38 | "forin": false, // Tolerate `for in` loops without `hasOwnPrototype`. 39 | "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 40 | "latedef": true, // Prohipit variable use before definition. 41 | "loopfunc": false, // Allow functions to be defined within loops. 42 | "noarg": false, // Prohibit use of `arguments.caller` and `arguments.callee`. 43 | "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. 44 | "regexdash": false, // Tolerate unescaped last dash i.e. `[-...]`. 45 | "scripturl": true, // Tolerate script-targeted URLs. 46 | "shadow": false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 47 | "supernew": false, // Tolerate `new function () { ... };` and `new Object;`. 48 | "undef": true, // Require all non-global variables be declared before they are used. 49 | 50 | // Personal styling preferences. 51 | "newcap": true, // Require capitalization of all constructor functions e.g. `new F()`. 52 | "noempty": true, // Prohibit use of empty blocks. 53 | "nonew": true, // Prohibit use of constructors for side-effects. 54 | "nomen": true, // Prohibit use of initial or trailing underbars in names. 55 | "onevar": false, // Allow only one `var` statement per function. 56 | "plusplus": false, // Prohibit use of `++` & `--`. 57 | "sub": false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 58 | "trailing": true, // Prohibit trailing whitespaces. 59 | "white": false, // Check against strict whitespace and indentation rules. 60 | "indent": 2 // Specify indentation spacing 61 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 James Doyle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-file-upload-component 2 | 3 | A simple file upload component for Vue.js. Emits events for XHR Upload Progress for nice progress bars. 4 | 5 | I came up with the original idea when looking at [this repo](https://github.com/tj/s3.js). I knew I wanted a nice component with upload progress and so I copied some code from that library. 6 | 7 | ### Install 8 | 9 | Available through npm as `vue-file-upload-component`. Or include as an inline script, like in `example.html`. 10 | 11 | ### Demo 12 | 13 | ![](http://cl.ly/image/3k2M2I0f4417/Screen%20Recording%202015-12-04%20at%2008.58%20AM.gif) 14 | 15 | In order to use the demo, you need to have PHP setup and this project running under a server. There is a script in the root called `upload.php`, which is a simple script to handle single file uploads. Most likely you will have your own way of handling files. 16 | 17 | ### Setting Headers 18 | 19 | You can set headers for the submission by using the attribute `v-bind:headers="xhrHeaders"`. `xhrHeaders` may look something like this: 20 | 21 | ```json 22 | // ... Vue stuff above 23 | data: { 24 | xhrHeaders: { 25 | "X-CSRF-TOKEN": "32charactersOfRandomStringNoise!" 26 | } 27 | }, 28 | // ... more stuff below 29 | ``` 30 | 31 | You can set many headers in the object. 32 | 33 | ### Caveats 34 | 35 | This upload script only uploads 1 file at a time. The upload handler uses `Promises` internally to know when all the files are uploaded. 36 | 37 | If you are using Internet Explorer, you will probably need a polyfill. I have [used this one before](https://github.com/getify/native-promise-only) and it is small and well tested. 38 | 39 | You also need [support for FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) but it has higher support than `Promises` so you are probably fine. 40 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue.js File Upload Component 6 | 7 | 16 | 17 | 18 |
19 | 20 | 24 | 25 |
26 | 27 |

All Files Uploaded

28 | 29 | Inside Slot Text 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* global require */ 2 | var gulp = require('gulp'); 3 | var livereload = require('gulp-livereload'); 4 | 5 | gulp.task('reload', function() { 6 | gulp.src('**/*.{php,html}') 7 | gulp.src('js/main.js') 8 | .pipe(livereload()); 9 | }); 10 | 11 | gulp.task('watch', function() { 12 | livereload.listen(); 13 | gulp.watch('**/*.{php,html}', ['reload']); 14 | gulp.watch('js/main.js', ['reload']); 15 | }); 16 | 17 | gulp.task('default', [], function() { 18 | // fired before 'finished' event 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-file-upload-component", 3 | "version": "1.4.0", 4 | "description": "A simple file upload component for Vue.js. Emits events for XHR Upload Progress for nice progress bars.", 5 | "homepage": "http://ohdoylerules.com", 6 | "repository": "https://github.com/james2doyle/vue-file-upload-component", 7 | "main": "vue.file-upload.js", 8 | "author": { 9 | "name": "James Doyle", 10 | "email": "james2doyle@gmail.com", 11 | "url": "http://ohdoylerules.com/" 12 | }, 13 | "licenses": [{ 14 | "type": "MIT", 15 | "url": "http://opensource.org/licenses/MIT" 16 | }], 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "gulp": "^3.9.0", 20 | "gulp-livereload": "^3.8.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /upload.php: -------------------------------------------------------------------------------- 1 | '200 OK', 19 | 400 => '400 Bad Request', 20 | 500 => '500 Internal Server Error' 21 | ); 22 | // ok, validation error, or failure 23 | header('Status: '.$status[$code]); 24 | // return the encoded json 25 | return json_encode(array( 26 | 'status' => $code < 300, // success or not? 27 | 'message' => $message 28 | )); 29 | } 30 | 31 | // this function is very simple 32 | // it just uploads a single file with a timestamp prepended 33 | $target_dir = "uploads/"; 34 | if(!file_exists($target_dir)){ 35 | mkdir($target_dir); 36 | } 37 | $target_file = $target_dir . time() . basename($_FILES["file"]["name"]); 38 | if (move_uploaded_file($_FILES["file"]["tmp_name"], $target_file)) { 39 | echo json_response(200, "The file ". basename( $_FILES["file"]["name"]). " has been uploaded."); 40 | } else { 41 | echo json_response(500, "Sorry, there was an error uploading your file."); 42 | } -------------------------------------------------------------------------------- /vue.file-upload.js: -------------------------------------------------------------------------------- 1 | /* globals FormData, Promise, Vue */ 2 | // define 3 | var FileUploadComponent = Vue.extend({ 4 | template: '
', 5 | props: { 6 | class: String, 7 | name: { 8 | type: String, 9 | required: true 10 | }, 11 | id: String, 12 | action: { 13 | type: String, 14 | required: true 15 | }, 16 | accept: String, 17 | multiple: String, 18 | headers: Object, 19 | method: String, 20 | buttonText: { 21 | type: String, 22 | default: 'Upload' 23 | } 24 | }, 25 | data: function() { 26 | return { 27 | myFiles: [] // a container for the files in our field 28 | }; 29 | }, 30 | methods: { 31 | fileInputClick: function() { 32 | // click actually triggers after the file dialog opens 33 | this.$dispatch('onFileClick', this.myFiles); 34 | }, 35 | fileInputChange: function() { 36 | // get the group of files assigned to this field 37 | var ident = this.id || this.name 38 | this.myFiles = document.getElementById(ident).files; 39 | this.$dispatch('onFileChange', this.myFiles); 40 | }, 41 | _onProgress: function(e) { 42 | // this is an internal call in XHR to update the progress 43 | e.percent = (e.loaded / e.total) * 100; 44 | this.$dispatch('onFileProgress', e); 45 | }, 46 | _handleUpload: function(file) { 47 | this.$dispatch('beforeFileUpload', file); 48 | var form = new FormData(); 49 | var xhr = new XMLHttpRequest(); 50 | try { 51 | form.append('Content-Type', file.type || 'application/octet-stream'); 52 | // our request will have the file in the ['file'] key 53 | form.append('file', file); 54 | } catch (err) { 55 | this.$dispatch('onFileError', file, err); 56 | return; 57 | } 58 | 59 | return new Promise(function(resolve, reject) { 60 | 61 | xhr.upload.addEventListener('progress', this._onProgress, false); 62 | 63 | xhr.onreadystatechange = function() { 64 | if (xhr.readyState < 4) { 65 | return; 66 | } 67 | if (xhr.status < 400) { 68 | var res = JSON.parse(xhr.responseText); 69 | this.$dispatch('onFileUpload', file, res); 70 | resolve(file); 71 | } else { 72 | var err = JSON.parse(xhr.responseText); 73 | err.status = xhr.status; 74 | err.statusText = xhr.statusText; 75 | this.$dispatch('onFileError', file, err); 76 | reject(err); 77 | } 78 | }.bind(this); 79 | 80 | xhr.onerror = function() { 81 | var err = JSON.parse(xhr.responseText); 82 | err.status = xhr.status; 83 | err.statusText = xhr.statusText; 84 | this.$dispatch('onFileError', file, err); 85 | reject(err); 86 | }.bind(this); 87 | 88 | xhr.open(this.method || "POST", this.action, true); 89 | if (this.headers) { 90 | for(var header in this.headers) { 91 | xhr.setRequestHeader(header, this.headers[header]); 92 | } 93 | } 94 | xhr.send(form); 95 | this.$dispatch('afterFileUpload', file); 96 | }.bind(this)); 97 | }, 98 | fileUpload: function() { 99 | if(this.myFiles.length > 0) { 100 | // a hack to push all the Promises into a new array 101 | var arrayOfPromises = Array.prototype.slice.call(this.myFiles, 0).map(function(file) { 102 | return this._handleUpload(file); 103 | }.bind(this)); 104 | // wait for everything to finish 105 | Promise.all(arrayOfPromises).then(function(allFiles) { 106 | this.$dispatch('onAllFilesUploaded', allFiles); 107 | }.bind(this)).catch(function(err) { 108 | this.$dispatch('onFileError', this.myFiles, err); 109 | }.bind(this)); 110 | } else { 111 | // someone tried to upload without adding files 112 | var err = new Error("No files to upload for this field"); 113 | this.$dispatch('onFileError', this.myFiles, err); 114 | } 115 | } 116 | } 117 | }); 118 | 119 | // register 120 | Vue.component('file-upload', FileUploadComponent); 121 | -------------------------------------------------------------------------------- /vue.pretty-bytes.js: -------------------------------------------------------------------------------- 1 | /* globals Vue */ 2 | // optional filter for formatting the bytes in the view 3 | Vue.filter('prettyBytes', function (num) { 4 | // jacked from: https://github.com/sindresorhus/pretty-bytes 5 | if (typeof num !== 'number' || isNaN(num)) { 6 | throw new TypeError('Expected a number'); 7 | } 8 | 9 | var exponent; 10 | var unit; 11 | var neg = num < 0; 12 | var units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 13 | 14 | if (neg) { 15 | num = -num; 16 | } 17 | 18 | if (num < 1) { 19 | return (neg ? '-' : '') + num + ' B'; 20 | } 21 | 22 | exponent = Math.min(Math.floor(Math.log(num) / Math.log(1000)), units.length - 1); 23 | num = (num / Math.pow(1000, exponent)).toFixed(2) * 1; 24 | unit = units[exponent]; 25 | 26 | return (neg ? '-' : '') + num + ' ' + unit; 27 | }); --------------------------------------------------------------------------------