2

I'm currently trying to create a small loading bar for when I select a file to upload in my form but I'm really struggling to make sense out of it. Most examples I find online show an upload loading example when we hit a button to 'upload' because it comes with an xmlhttprequest or similar, but in my case, I want to show the loading progress right when I select the file before I hit the button that sends the form. Therefore once I add the file to my input a loading percentage should appear. I'd really appreciate if someone could give an indication as to what needs to be done here.

FileUpload.vue - file upload input component

 <div class="container">
    <!--UPLOAD-->
      <h3 class="form-input-title">{{labelText}}</h3>
      <div class="dropbox">
        <img src="../../assets/img/icon/curr/ic_download.svg"
             class="download-icon"
             alt="download icon"/>
        <input :name="fieldName"  
               :accept="accept"
               @change="loadFile($event)"
               type="file"
               class="input-file">
          <p v-if="!isLoading && !isSuccess">{{text}}</p>
          <p v-if="isLoading">Uploading files... {{uploadLoadPercentage}}</p>
          <p v-if="isSuccess">{{fileName}}</p>
      </div>
  </div>


<script>
  import {ref, watch} from '@vue/composition-api';
  import Validator from "@/utils/yo-validator/YoValidator";

  export default {
    name: 'FileUpload',

    setup(props) {
      /** validator returned error messages **/
      const {errorMessages, validateFieldByData, setFieldData} = Validator.register(props);

      /** watch api error text from parent **/
      const apiError = ref('');
      watch(() => props.apiErrorText, (newVal) => {
        apiError.value = newVal.trim();
      });

      const isLoading = ref(false);
      const isSuccess = ref(false);
      const uploadFailed = ref(false);
      let uploadLoadPercentage = ref(0);
      const fileName = ref('');
      /** watch input in the template **/
      const loadFile = async (event) => {
        // TODO: File loading > When the file is selected, check how long the file (in bytes) takes to upload
        // If the loading is successful proceed without any errors

        // validate the image
        validateFieldByData(event.target.files);

        // define data
        const data = props.isMultiple ? event.target.files : event.target.files[0];

        // set the name of the file that has been uploaded.
        fileName.value = event.target.files[0].name;

        // Once loading and validation is completed, save the data
        saveFile(data);
      };

      const saveFile = (data) => {
        // Once saveFile is triggered, the upload value will be successful.
        isSuccess.value = true;

        // update form data
        setFieldData(data);
        // call the parent if needed
        updateParent(data);
      }
  
      // update parent component
      const updateParent = (data) => {
        if (props.callback) {
          props.callback(data);
        }
      };
      const clearFile = (event) => {
        // clear the value to trigger change event for uploading the same image
        event.target.value = null;
      };

      return {
        errorMessages,
        apiError,
        loadFile,
        clearFile,
        isLoading,
        isSuccess,
        uploadFailed,
        fileName,
        uploadLoadPercentage
      }
    }
  }
</script>
thelandog
  • 674
  • 1
  • 9
  • 25

1 Answers1

4

First of all, progress for the file upload has to be tracked when you upload it to some server or to some storage space, normally you make a POST or PUT request, with axios, for example, which has a onUploadProgress callback, with which you can upload your progress tracker, I will not go into detail here, since there are countless examples for this, like: The answer to this SO question, which details this nicely.

If you are in a special case, like not sending this through a simple HTTP request, but through something like firebase, they have builtin tools for this as well, but if you give more detail maybe i would be able to help you more in depth with your problem :D

UPDATE: Okay, sorry, my applogies, I totally misunderstood, what you needed:

< script >
import { ref } from 'vue';

export default {
  setup() {
    const reader = new FileReader();
    const fileUrl = ref(null);
    const totalSize = ref(0);
    const currentProgress = ref('0%');

    function handleEvent(event) {
      if (['loadend', 'load'].includes(event.type)) {
        console.log('finished loading file');
        currentProgress.value = 'Finished loading file';
        fileUrl.value = reader.result;
      }
      if (event.type === 'progress') {
        currentProgress.value = `${(event.loaded / totalSize.value).toFixed(2) * 100}%`;
        console.log('Progress: ', currentProgress.value);
        console.log('Bytes transferred: ', event.loaded, 'bytes');
      }
      if (event.type === 'loadstart') {
        totalSize.value = event.total;
      }
    }

    function addListeners(reader) {
      reader.addEventListener('loadstart', handleEvent); 
      reader.addEventListener('load', handleEvent);
      reader.addEventListener('loadend', handleEvent);
      reader.addEventListener('progress', handleEvent);
      reader.addEventListener('error', handleEvent);
      reader.addEventListener('abort', handleEvent);
      //dont do this, make diffrent functions for every 
      //event listener please, your code's readability will be 100% better, 
      //i am on a bus rn, but will make it prettier later :D
    }

    function handleSelected(e) {
      console.log(e);
      const selectedFile = e.target.files[0];
      if (selectedFile) {
        addListeners(reader);
        reader.readAsDataURL(selectedFile);
      }
    }
    return {
      handleSelected,
      fileUrl,
      currentProgress
    };
  }
}; <
/script>
<template>
    <div>
        <div>
            <label for="img">Choose an image:</label>
            <input type="file" id="img" name="img" accept="image/*" @change="handleSelected" />
        </div>
        <img v-if="fileUrl" :src="fileUrl" class="preview" height="200" alt="Image preview..." />
        <div>
            <span>Progress: {{ currentProgress }}</span>
        </div>
    </div>
</template>

Just make a Reader object, which is there to handle these types of situations, and listen for the events emitted by it. Hope this helps, if you have any more questions, please ask :D And this is the output to the console, you can use this data to display on the UI as well:

Output to console:

NOTE: In my opinion for images you dont need this progress indicator, since they dont take a long time to upload and will show 100% for most of images(the console output is for a file that is 500mb large), but if you include the http request to upload the file here, then i think it would be ok, if you want to upload regular files, than this is expected and needed.

Komi
  • 450
  • 5
  • 14
  • Eventually I will be uploading the file to a server via POST, however for this particular example I would like to show the loading progress before I actually submit the form and upload the file to the server. For this reason I'm thinking that the loading progress has to be handled on the client side. I came across this [example](https://www.htmlgoodies.com/html5/responding-to-html5-filereader-events/#fbid=Lz2is5BC9k1), but I'm really not sure how I can replicate this for my scenario. – thelandog Jun 30 '21 at 14:48
  • 1
    Thanks a lot, this is exactly what I needed and after seeing your implementation I got a better understanding of how this should work. Thanks again! – thelandog Jul 01 '21 at 09:43