In my previous post I discussed how to make a prettified file input that returns the file object. In this post we are going to look at how to upload the file we receive from our file input component and implement a progress bar.

Let’s start by using the file input component we designed in the previous post:

<div id="app">
   <upload-input @file-change="setFile"></upload-input>
</div>

We now just want implement the setFile method in our Vue instance, which simply takes the file returned from our upload-input component and sets it on the parent:

methods:{
   setFile(file){
      this.file = file;
   }
},
data() {
  return {
    file: '';
  }
}

Uploading the File

Now we have the upload-input component setup, we want to create an upload button and add an uploadFile method to upload the file to our server. So, let’s add the button and add some extra bootstrap classes for the layout. Our HTML now looks like this:

<div id="app">
  <div class="form-group">
    <upload-input @file-chosen="setFile"></upload-input>
  </div>
  <div class="form-group">
    <button class="btn btn-primary" @click="uploadFile">
      Upload File
    </button>
  </div>
</div>

Now in our view model, we need to create the uploadFile method we have referenced on the click event of our button. Make sure you have vue-resource included in your project which can then be used in our method by referencing this.$http:

uploadFile() {
  let formData = new FormData();
  formData.append('file', this.file);

  this.$http.post('/upload', formData).then(response => {
          console.log('Upload Complete!')
  })
}

Here we are using the FormData Interface and appending our file, then we are passing our FormData object as a parameter in our post request. Once the file has uploaded we are then logging an “Upload Complete!” message in our console.

Building the Progress Bar Component

None of that should seem too difficult, but it’s not exactly user friendly! We need to give some visual queue that the upload is in progress, and for that, we are going to use a bootstrap progress bar.

First let’s look at the basic bootstrap markup for our template:

<template>
  <div class="progress">
    <div class="progress-bar progress-bar-info progress-bar-striped" role="progressbar" style="width:0%">
    </div>
  </div>
</template>

That’s just an empty progress bar, but we need to make sure that we can set the progress bar percentage, which in bootstrap is done using a css width attribute, so let’s bind it to our component view model:

computed: {
  getProgress(){
    return Math.round(this.currentProgress) + "%";
  }
},
data(){
  return {
     currentProgress: 0
  }
}

Now we want to add v-bind to our progress bar so it updates based on our currentProgress value. We are using the computed here so we can round the percentage and add “%” to the end of progress, which is required for passing to the width attribute, the computed will now update whenever currentProgress updates:

<div class="progress-bar progress-bar-info progress-bar-striped" role="progressbar" :style="{width: getProgress}">

That’s great, but we need to be able to set the percentage from the parent, so let’s add a prop so we can pass this to our progress-bar component:

props: {
    progress: {
      default: 0,
      type: Number
    }
}

At this point the prop doesn’t do anything, because we’ve bound our progress bar to currentProgress (or rather the getProgress computed, which uses currentProgress), so we need to copy the passed progress prop to currentProgress. Usually, we would do that in the created hook, but we will need to update the progress dynamically, which means it needs to be reactive. To do that, let’s watch the progress prop and update the value of currentProgress whenever it changes:

watch: {
  progress(progress) {
    this.currentProgress = progress;
  }
}

That’s pretty much it, but before we’re done with our component let’s just add a couple of extra props so we can decide if we want to display progress percentage inside the progress bar and also set a message to display when the upload is complete, so out final component looks like this:

HTML

<template id="progress-bar">
  <div class="progress">
    <div class="progress-bar progress-bar-info progress-bar-striped" role="progressbar" :style="{width: getProgress}">
      <span v-if="progress < 100 && showProgressText">
      {{getProgress}}
      </span>
      <span v-if="progress >= 100 && showProgressText">{{progressCompleteText}}</span>
    </div>
  </div>
</template>

Vue Instance

 props: {
    progress: {
      default: 0,
      type: Number
    },
    showProgressText: {
      default: true,
      type: Boolean
    },
    progressCompleteText: {
      default: 'Complete!',
      type: String
    }
  },
  computed: {
    getProgress() {
      return Math.round(this.progress) + "%";
    }
  },
  watch: {
    progress(progress) {
      this.currentProgress = progress;
    }
  },
  data() {
    return {
      currentProgress: 0
    }
  }

If you’re interested you can see this in action on this JSFiddle: https://jsfiddle.net/xem7fsf5/

Displaying the Current Upload Progress

In the JSFiddle above, I’ve just mocked the upload, so now let’s look at updating our progress prop as the file uploads, which can be done using vue-resources’ progress callback, which we pass to our post request:

this.$http.post('/upload', formData, {
  progress: e => {
    this.progress = (e.loaded / e.total) * 100;
  }
}).then(response => {
  console.log(response);
})

This should be relatively self explanatory, we’re essentially calculating the progress percentage based on values returned to the progress callback. Here’s a JSFiddle to show you everything we’ve done so far: https://jsfiddle.net/3m4j4w51/

That’s already pretty nice, but we can’t cancel the upload once it’s started, so lets sort that out.

Canceling The Upload

vue-resource provides an abort method, which we can use on the request, we just need to set the request in our vue instance before the upload starts, and then implement a cancel method, so let’s add the before callback to our request so we can access the request and abort it at any point:

this.$http.post('/upload', formData, {
  before: request => {
    this.request = request;
  },
  progress: e => {
    this.progress = (e.loaded / e.total) * 100;
  }
}).then(response => {
  console.log(response);
}, error => {
  this.progress = 0;
})

We’ve also added the error callback, so that we can reset the progress if the user cancels.

Now let’s add a method to cancel the request using vue resources’ abort() method:

cancelUpload(){
  this.request.abort()
}

And simply attach that to a “Cancel” button:

<button class="btn btn-danger" @click="cancelUpload">
  Cancel
</button>

Here’s the JSFiddle: https://jsfiddle.net/zsjknjmu/

Prettifying the UI

That’s pretty much everything. However, our page doesn’t look great with all the components together like that, and the user can still interact with the page while the upload is in progress, so let’s finish up by making our uploader a little neater.

To do this we are simply going to overlay the progress bar and the cancel button on the page while it is uploading. For this we just need to add a bit of css and bind a few v-show's to data, so we can show and hide the overlay during the upload and also add a “success” or “failure” message when the upload finishes.

The end result is pretty slick!https://jsfiddle.net/vuetiful/ahh7L57q/

Advertisements