Tutorial

How to Handle File Uploads in Vue 2

Updated on September 15, 2020
    author

    Jecelyn Yeen

    How to Handle File Uploads in Vue 2

    Introduction

    In this article, we will talk about how to handle file uploads with VueJs. We will create an images uploader that allow user to upload single or multiple images file by drag and drop or select file dialog.

    We will then upload the selected images and display them accordingly. We will also learn to filter the upload file type, for example, we only allow images, do not allow file type like PDF.

    Image uploader

    File Upload UI & API

    File upload consists of two parts: the UI (front-end) and the API (back-end). We will be using VueJs to handle the UI part. We need a backend application to accept the uploaded files. You may follow the backend tutorials or download and run either one of these server side application to handle file upload for your backend:-

    We will be using File upload with Hapi.js as our backend throughout this articles. We will also learn the tricks to enable fake upload on the front-end.

    Setup Project with Vue-Cli

    We will be using vue-cli to scaffold Vue.js projects. We will be using the webpack-simple project template.

    # install cli
    npm install vue-cli -g
    
    # then create project, with sass
    # follow the instructions to install all necessary dependencies
    vue init webpack-simple file-upload-vue
    

    Alright, all set. Let’s proceed to create our component.

    File Upload Component

    We will write our code in App.vue. Remove all the auto-generated code in the file.

    <!-- App.vue -->
    
    <!-- HTML Template -->
    <template>
      <div id="app">
        <div class="container">
          <!--UPLOAD-->
          <form enctype="multipart/form-data" novalidate v-if="isInitial || isSaving">
            <h1>Upload images</h1>
            <div class="dropbox">
              <input type="file" multiple :name="uploadFieldName" :disabled="isSaving" @change="filesChange($event.target.name, $event.target.files); fileCount = $event.target.files.length"
                accept="image/*" class="input-file">
                <p v-if="isInitial">
                  Drag your file(s) here to begin<br> or click to browse
                </p>
                <p v-if="isSaving">
                  Uploading {{ fileCount }} files...
                </p>
            </div>
          </form>
      </div>
    </template>
    
    <!-- Javascript -->
    <script>
    </script>
    
    <!-- SASS styling -->
    <style lang="scss">
    </style>
    

    Notes:-

    1. Our App.vue component consists of 3 part: template (HTML), script (Javascript) and styles (SASS).
    2. Our template has an upload form.
    3. The form attribute enctype="multipart/form-data" is important. To enable file upload, this attribute must be set. Learn more about enctype here.
    4. We have a file input <input type="file" /> to accept file upload. The property multiple indicate it’s allow multiple file upload. Remove it for single file upload.
    5. We will handle the file input change event. Whenever the file input change (someone drop or select files), we will trigger the filesChange function and pass in the control name and selected files $event.target.files, and then upload to server.
    6. We limit the file input to accept images only with the attribute accept="image/*".
    7. The file input will be disabled during upload, so user can only drop / select files again after upload complete.
    8. We capture the fileCount of the when file changes. We use the fileCount variable in displaying number of files uploading Uploading {{ fileCount }} files....

    Style our File Upload Component

    Now, that’s the interesting part. Currently, our component look like this:

    File upload component without styling

    We need to transform it to look like this:

    File upload component with styling

    Let’s style it!

    <!-- App.vue -->
    ...
    
    <!-- SASS styling -->
    <style lang="scss">
      .dropbox {
        outline: 2px dashed grey; /* the dash box */
        outline-offset: -10px;
        background: lightcyan;
        color: dimgray;
        padding: 10px 10px;
        min-height: 200px; /* minimum height */
        position: relative;
        cursor: pointer;
      }
      
      .input-file {
        opacity: 0; /* invisible but it's there! */
        width: 100%;
        height: 200px;
        position: absolute;
        cursor: pointer;
      }
      
      .dropbox:hover {
        background: lightblue; /* when mouse over to the drop zone, change color */
      }
      
      .dropbox p {
        font-size: 1.2em;
        text-align: center;
        padding: 50px 0;
      }
    </style>
    

    With only few lines of scss, our component looks prettier now.

    Notes:-

    1. We make the file input invisible by applying opacity: 0 style. This doesn’t hide the file input, it just make it invisible.
    2. Then, we style the file input parent element, the dropbox css class. We make it look like a drop file zone surround with dash.
    3. Then, we align the text inside dropbox to center.

    File Upload Component Code

    Let’s proceed to code our component.

    <!-- App.vue -->
    ...
    
    <!-- Javascript -->
    <script>
      import { upload } from './file-upload.service';
    
      const STATUS_INITIAL = 0, STATUS_SAVING = 1, STATUS_SUCCESS = 2, STATUS_FAILED = 3;
    
      export default {
        name: 'app',
        data() {
          return {
            uploadedFiles: [],
            uploadError: null,
            currentStatus: null,
            uploadFieldName: 'photos'
          }
        },
        computed: {
          isInitial() {
            return this.currentStatus === STATUS_INITIAL;
          },
          isSaving() {
            return this.currentStatus === STATUS_SAVING;
          },
          isSuccess() {
            return this.currentStatus === STATUS_SUCCESS;
          },
          isFailed() {
            return this.currentStatus === STATUS_FAILED;
          }
        },
        methods: {
          reset() {
            // reset form to initial state
            this.currentStatus = STATUS_INITIAL;
            this.uploadedFiles = [];
            this.uploadError = null;
          },
          save(formData) {
            // upload data to the server
            this.currentStatus = STATUS_SAVING;
    
            upload(formData)
              .then(x => {
                this.uploadedFiles = [].concat(x);
                this.currentStatus = STATUS_SUCCESS;
              })
              .catch(err => {
                this.uploadError = err.response;
                this.currentStatus = STATUS_FAILED;
              });
          },
          filesChange(fieldName, fileList) {
            // handle file changes
            const formData = new FormData();
    
            if (!fileList.length) return;
    
            // append the files to FormData
            Array
              .from(Array(fileList.length).keys())
              .map(x => {
                formData.append(fieldName, fileList[x], fileList[x].name);
              });
    
            // save it
            this.save(formData);
          }
        },
        mounted() {
          this.reset();
        },
      }
    
    </script>
    

    Notes:-

    1. Our component will have a few statuses: STATUS_INITIAL, STATUS_SAVING, STATUS_SUCCESS, STATUS_FAILED, the variable name is pretty expressive themselves.
    2. Later on, we will call the Hapi.js file upload API to upload images, the API accept a field call photos. That’s our file input field name.
    3. We handle the file changes with the filesChange function. FileList is an object returned by the files property of the HTML <input> element. It allow us to access the list of files selected with the <input type=“file”> element. Learn more [here]((https://developer.mozilla.org/en/docs/Web/API/FileList).
    4. We then create a new FormData, and append all our photos files to it. FormData interface provides a way to easily construct a set of key/value pairs representing form fields and their values. Learn more here.
    5. The save function will call our file upload service (hang on, we will create the service next!). We also set the status according to the result.
    6. mount() is the vue component life cycle hook. During that point, we will set our component status to initial state.

    File Upload Service

    Let’s proceed to create our service. We will be using axios to make HTTP calls.

    Install axios

    # install axios
    npm install axios --save
    

    Service

    // file-upload.service.js
    
    import * as axios from 'axios';
    
    const BASE_URL = 'http://localhost:3001';
    
    function upload(formData) {
        const url = `${BASE_URL}/photos/upload`;
        return axios.post(url, formData)
            // get data
            .then(x => x.data)
            // add url field
            .then(x => x.map(img => Object.assign({},
                img, { url: `${BASE_URL}/images/${img.id}` })));
    }
    
    export { upload }
    

    Nothing much, the code is pretty expressive itself. We upload the files, wait for the result, map it accordingly.

    You may run the application now with npm run dev command. Try uploading a couple of images, and it’s working! (Remember to start your backend server)

    Display Success and Failed Result

    We can upload the files successfully now. However, there’s no indication in UI. Let’s update our HTML template.

    <!-- App.vue -->
    
    <!-- HTML Template -->
    <template>
      <div id="app">
        <div class="container">
          ...form...
    
          <!--SUCCESS-->
          <div v-if="isSuccess">
            <h2>Uploaded {{ uploadedFiles.length }} file(s) successfully.</h2>
            <p>
              <a href="javascript:void(0)" @click="reset()">Upload again</a>
            </p>
            <ul class="list-unstyled">
              <li v-for="item in uploadedFiles">
                <img :src="item.url" class="img-responsive img-thumbnail" :alt="item.originalName">
              </li>
            </ul>
          </div>
          <!--FAILED-->
          <div v-if="isFailed">
            <h2>Uploaded failed.</h2>
            <p>
              <a href="javascript:void(0)" @click="reset()">Try again</a>
            </p>
            <pre>{{ uploadError }}</pre>
          </div>
        </div>
      </div>
    </template>
    

    Notes:-

    1. Display the uploaded image when upload successfully.
    2. Display the error message when upload failed.

    Fake the Upload in Front-end

    If you are lazy to start the back-end application (Hapi, Express, etc) to handle file upload. Here is a fake service to replace the file upload service.

    // file-upload.fake.service.js
    
    function upload(formData) {
        const photos = formData.getAll('photos');
        const promises = photos.map((x) => getImage(x)
            .then(img => ({
                id: img,
                originalName: x.name,
                fileName: x.name,
                url: img
            })));
        return Promise.all(promises);
    }
    
    function getImage(file) {
        return new Promise((resolve, reject) => {
            const fReader = new FileReader();
            const img = document.createElement('img');
    
            fReader.onload = () => {
                img.src = fReader.result;
                resolve(getBase64Image(img));
            }
    
            fReader.readAsDataURL(file);
        })
    }
    
    function getBase64Image(img) {
        const canvas = document.createElement('canvas');
        canvas.width = img.width;
        canvas.height = img.height;
    
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);
    
        const dataURL = canvas.toDataURL('image/png');
    
        return dataURL;
    }
    
    export { upload }
    

    Came across this solution in this Stackoverflow post. Pretty useful. My online demo is using this service.

    Basically, what the code do is read the source, draw it in canvas, and save it as data url with the canvas toDataURL function. Learn more about canvas here.

    Now you can swap the real service with the fake one.

    <!-- App.vue -->
    ...
    
    <!-- Javascript -->
    <script>
      // swap as you need
      import { upload } from './file-upload.fake.service'; // fake service
      // import { upload } from './file-upload.service';   // real service
    </script>
    
    ...
    
    

    Done! Stop your backend API, refresh your browser, you should see our app is still working, calling fake service instead.

    Bonus: Delay Your Promises

    Sometimes, you may want to delay the promises to see the state changes. In our case, the file upload may complete too fast. Let’s write a helper function for that.

    // utils.js
    
    // utils to delay promise
    function wait(ms) {
        return (x) => {
            return new Promise(resolve => setTimeout(() => resolve(x), ms));
        };
    }
    
    export { wait }
    
    

    Then, you can use it in your component

    <!-- App.vue -->
    ...
    
    <!-- Javascript -->
    <script>
      import { wait } from './utils';
      ...
      
      save(formData) {
    	 ....
    	 
            upload(formData)
              .then(wait(1500)) // DEV ONLY: wait for 1.5s 
              .then(x => {
                this.uploadedFiles = [].concat(x);
                this.currentStatus = STATUS_SUCCESS;
              })
             ...
    		 
          },
    </script>
    

    Conclusion

    That’s it. This is how you can handle file upload without using any 3rd party libraries and plugins in Vue. It isn’t that hard right?

    Happy coding!

    The UI (Front-end)

    The API (Back-end) Tutorials and Sourcode

    Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

    Learn more about our products

    About the authors
    Default avatar
    Jecelyn Yeen

    author

    While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

    Still looking for an answer?

    Ask a questionSearch for more help

    Was this helpful?
     
    2 Comments
    

    This textbox defaults to using Markdown to format your answer.

    You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

    All images are dead. The CDN blocks access to images

    Nice article, there is however a bug in file-upload.fake.service.js.

    Image loading is async so you need to handle the resolve in img.onload

    FIX: … function getImage( …

    fReader.onload = () => { img.onload = () => { resolve(getBase64Image(img)) } img.src = fReader.result; }

    Try DigitalOcean for free

    Click below to sign up and get $200 of credit to try our products over 60 days!

    Sign up

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Featured on Community

    Get our biweekly newsletter

    Sign up for Infrastructure as a Newsletter.

    Hollie's Hub for Good

    Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

    Become a contributor

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    Welcome to the developer cloud

    DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

    Learn more