Uploading file in Vue.js


6 years ago -  
TypeScriptVue.jsJavaScriptASP.NET Core

In this blog post I am going to show you how you can upload file using Vue.js. On the backend we will use ASP.NET Core MVC to expose an endpoint to the client to receive file. Let’s say we have a HTML form for saving a coffee, this form contains a file input to accept coffee’s image:

Vue Upload File

To allow the user to pick a file we need to have a reference to the file input field using ref attribute then we can easily access this input using $refs object inside our Vue instance this is like selecting an element using jQuery:

<input required type="file" ref="image" />

As you can see we named this input image so we can access the element this way:

this.\$refs.image

Before we can submit the form we can encapsulate functionality inside a service:

import axios from "axios";

class CoffeeService {
  saveCoffee(coffee: any, file: any): any {
    const formData = new FormData();
    for (const key in coffee) {
      if (coffee.hasOwnProperty(key)) {
        formData.append(key, coffee[key]);
      }
    }
    formData.append(file.name, file);

    return axios.post("/Coffees/SaveCoffee", formData);
  }
}

// Export a singletone instance in the global namespace
export const coffeeService = new CoffeeService();

This service has a method called saveCoffee which accepts two parameters one is the coffee itself and the second one is uploaded file then we are using FormData to combine these data together and sending it to the server using axios library. Keep in mind, this method returns a promise which means that the caller can use then syntax to get server’s response. So inside the CoffeeComponent we can use Coffeeservice like this:

<template>
  <div class="container">
    <h3>Add a new Coffee</h3>
    <form v-on:submit.prevent="submitted">
      <div class="form-group">
        <label class="control-label">Coffee Name</label>
        <input
          required
          type="text"
          class="form-control"
          name="Name"
          v-model="model.name"
        />
      </div>

      <div class="form-group">
        <label class="control-label">Coffee Type</label>
        <select
          class="form-control"
          name="CoffeeType"
          v-model="model.coffeeType"
        >
          <option
            v-for="coffeeType in coffeeTypes"
            :value="coffeeType.value"
            v-bind:key="coffeeType.value"
          >
            {{ coffeeType.name }}
          </option>
        </select>
      </div>

      <div class="form-group">
        <label class="control-label">Coffee Image</label>
        <input
          required
          type="file"
          multiple
          class="form-control"
          name="Image"
          @change="fileChange"
          ref="image"
        />
      </div>

      <button class="btn btn-primary" type="submit">Save</button>
    </form>
  </div>
</template>

<script lang="ts">
  import Vue from "vue";
  import { Component, Prop, Watch } from "vue-property-decorator";
  import { coffeeService } from "../services/CoffeeService";
  /// <reference path="./toaster.d.ts" />

  @Component({
    components: { UploadFileSimpleComponent },
  })
  export default class UploadFileSimpleComponent extends Vue {
    coffeeTypes = [
      { name: "Espresso", value: 0 },
      { name: "Latte", value: 1 },
      { name: "Mocha", value: 2 },
    ];

    model = {};

    submitted() {
      coffeeService
        .saveCoffee(this.model, (<any>this.$refs.image).files)
        .then(function (response: any) {
          toastr.success(response.data);
        })
        .catch(function (error: any) {
          toastr.error(error);
        });
    }
  }
</script>

Showing upload progress

Now we want to show upload progress, axios makes this easy all we need to do is add a third argument to the post method, this argument is actually an object, inside this object we can configure the request inside this object we have access to an event handler called onUploadProgress which we can do the calculation. let’s change the CoffeeService to this:

import axios from "axios";
import { eventBus } from "../main";

class CoffeeService {
  saveCoffee(coffee: any, file: any): any {
    const formData = new FormData();
    for (const key in coffee) {
      if (coffee.hasOwnProperty(key)) {
        formData.append(key, coffee[key]);
      }
    }

    formData.append(file.name, file);

    let startTime = Date.now();

    return axios.post("/Coffees/SaveCoffee", formData, {
      onUploadProgress: (uploadEvent) => {
        const queueProgress = Math.round(
          (uploadEvent.loaded / uploadEvent.total) * 100
        );
        const timeElapsed = Date.now() - startTime;
        const uploadSpeedFirst = uploadEvent.loaded / (timeElapsed / 1000);
        const uploadTimeRemaining = Math.ceil(
          (uploadEvent.total - uploadEvent.loaded) / uploadSpeedFirst
        );
        const uploadTimeElapsed = Math.ceil(timeElapsed / 1000);
        const uploadSpeed = uploadSpeedFirst / 1024 / 1024;

        eventBus.$emit("uploadData", {
          queueProgress,
          uploadTimeRemaining,
          uploadTimeElapsed,
          uploadSpeed,
        });
      },
    });
  }
}

// Export a singletone instance in the global namespace
export const coffeeService = new CoffeeService();

Inside the method we construct an object, this object will notify all subscribers with current progress. This notification is done using $emit method. We also need to update the template to show this progress:

<div v-if="uploadDetails.queueProgress > 0">
  <table class="table">
    <thead>
      <tr>
        <th width="15%">Event</th>
        <th>Status</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><strong>Elapsed time</strong></td>
        <td nowrap>{{uploadDetails.uploadTimeElapsed | number}} second(s)</td>
      </tr>
      <tr>
        <td><strong>Remaining time</strong></td>
        <td nowrap>{{uploadDetails.uploadTimeRemaining | number}} second(s)</td>
      </tr>
      <tr>
        <td><strong>Upload speed</strong></td>
        <td nowrap>{{uploadDetails.uploadSpeed | number}} MB/s</td>
      </tr>
      <tr>
        <td><strong>Queue progress</strong></td>
        <td>
          <div
            class="progress-bar progress-bar-info progress-bar-striped"
            role="progressbar"
            aria-valuemin="0"
            aria-valuemax="100"
            :aria-valuenow="uploadDetails.queueProgress"
            :style="{ 'width': uploadDetails.queueProgress + '%' }"
          >
            {{uploadDetails.queueProgress}}%
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</div>

The backend is a simple MVC controller, all it does is uploading the file and saving the coffee into the database:

public async Task<IActionResult> SaveCoffee(Coffee coffee)
{
// Uploading files
var fileName = await UploadFiles();

    // Saving data
    coffee.Image = fileName;
    _coffeeService.Add(coffee);
    _coffeeService.SaveChanges();

    return Json("Coffee has been saved!");

}

private async Task<string> UploadFiles()
{
var uploadsRootFolder = Path.Combine(\_environment.WebRootPath, "uploads");
if (!Directory.Exists(uploadsRootFolder))
{
Directory.CreateDirectory(uploadsRootFolder);
}

    var files = Request.Form.Files;
    foreach (var file in files)

    {
        if (file == null || file.Length == 0)
        {
            continue;
        }

        var filePath = Path.Combine(uploadsRootFolder, file.FileName);
        using (var fileStream = new FileStream(filePath, FileMode.Create))
        {
            await file.CopyToAsync(fileStream).ConfigureAwait(false);
            return file.FileName;
        }
    }

    return string.Empty;

}

Upload file using Vue.js

You can grab the working sample project from GitHub.