Building A Custom File Upload Component For Vue

General Overview

When building applications for either the web or mobile, very often, the need to make a provision for file upload arises. There are many use cases for file uploads and these may include profile picture upload for an account, document upload for account verification, etc. I was spurred to build a custom file upload component after experiencing a snag while working on a project. I needed some custom features but I could not find them. Then I thought to myself, hey! I will build them.

In this article, I will be exploring the basics of creating a simple, yet useful file upload component for your next VueJS application. This article focuses on a single file upload, but a lot of the concepts can be extended and used for multiple file uploads as well.

Prerequisites

While this article is designed to be beginner friendly, there are a few skills the reader is expected to possess:

  • Basic HTML, CSS and JavaScript
  • Basic understanding of some ES6+ concepts
  • Basic VueJS. I will be using Vue 3 together with the options API. You can read more about the different APIs avaialble in Vue here

Project Setup

In this article, I will be using the Vue CLI to set up my project. If you already have the CLI installed, navigate to your preferred directory, open it up in your terminal and run the following command

vue create file-upload

Enter fullscreen mode

Exit fullscreen mode

You will be required to choose a few options after running this command. For this project I am using the default presets, but you can choose to add more items if you want to expand beyond the scope of this article.

If everything works fine, your vue project should be created and should look very similar to the image below

;

If you have not already started, proceed to start your development server to get your project up and running in your browser. This is can be achieved by navigating to your project root directory and running npm run serve. This will start up a development server on http://localhost:8080.

Great! Now let’s build out our component features

Congrats, if you have made it this far. I am proud of you! We will continue to build out our upload component which is where most of our logic will take place.

Creating our component markup

To spin up our Upload component, proceed to create a new file inside the components folder called FileUpload.vue and paste in the following code:

<template>
  <div class="file-upload">
    <div class="file-upload__area">
      <input type="file" name="" id="" />
    </div>
  </div>
</template>

<script>
export default {
  name: "FileUpload",
};
</script>

<style scoped>
.file-upload {
  height: 100vh;
  width: 100%;
  display: flex;
  align-items: flex-start;
  justify-content: center;
}
.file-upload .file-upload__area {
  width: 600px;
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px dashed #ccc;
  margin-top: 40px;
}
</style>

Enter fullscreen mode

Exit fullscreen mode

For now, what we have is just some basic markup with an input element and some basic styling to improve the visual appearance of our component. We will proceed to register this component and render it so we can see what we have so far.

//App.vue
<template>
  <div>
    <FileUpload />
  </div>
</template>

<script>
import FileUpload from "@/components/FileUpload.vue";

export default {
  name: "App",
  components: {
    FileUpload,
  },
};
</script>

Enter fullscreen mode

Exit fullscreen mode

The above code will enable us see what our component looks like now and going forward. If you have followed the instructions correctly, you should see something similar to the image below when you visit your development url in your browser.

Writing our upload logic

Now that we have our markup set up, we will proceed to write some logic to handle our file upload.

First off, we will define a few props which will help us control how our component should behave. Props are a useful way to pass data from parent component to child components and vice-versa in Vue. You can read more about props here.

Go ahead and add the code below to your File Upload components

// FileUpload.vue
export default {
  ...
  props: {
    maxSize: {
      type: Number,
      default: 5,
      required: true,
    },
    accept: {
      type: String,
      default: "image/*",
    },
  },
  };

Enter fullscreen mode

Exit fullscreen mode

  • The maxSize prop helps our component to be more dynamic by specifying the maximum file size that can be accepted by our component.
  • The accept prop allows us to define the type of files that should be permitted for upload.

After our props are set up, we can go ahead to define some data to help us perform our upload operation. We will update our code to look like this by defining some initial state.

// FileUpload.vue
  ...
  data () {
    return {
      isLoading: false,
      uploadReady: true,
      file: {
        name: "",
        size: 0,
        type: "",
        fileExtention: "",
        url: "",
        isImage: false,
        isUploaded: false,
      },
    };
  },

Enter fullscreen mode

Exit fullscreen mode

Here, we define some initial state for our component which we will use to update our UI and perform certain logic for when files are selected. You can read more about Vue options data state here.

Now that we have our state defined, we can proceed to update our UI to better reflect what we have done so far.

First off, we would head over to App.vue to update our component declaration and specify values for our component props. Copy and replace the code in App.vue with

//App.vue

<template>
  <div>
    <FileUpload :maxSize="5" accept="png" />
  </div>
</template>

<script>
import FileUpload from "@/components/FileUpload.vue";

export default {
  name: "App",
  components: {
    FileUpload,
  },
};
</script>

Enter fullscreen mode

Exit fullscreen mode

Here we set our maxSize to 5 and tell our File Upload component to only accept .png files

Having achieved our data set up, we can go on to define some logic we want to perform. First on the list will be to actually handle what happens when the user chooses a file. To achieve this, we create a function that will handle the upload. In vue, we can do this by creating a handleFileChange function within our methods (more on this here) object in our FileUpload.vue component. Go ahead and add the block of code below

// FileUpload.vue

  methods: {
    handleFileChange(e) {
      // Check if file is selected
      if (e.target.files && e.target.files[0]) {
        // Get uploaded file
        const file = e.target.files[0],
          // Get file size
          fileSize = Math.round((file.size / 1024 / 1024) * 100) / 100,
          // Get file extention
          fileExtention = file.name.split(".").pop(),
          // Get file name
          fileName = file.name.split(".").shift(),
          // Check if file is an image
          isImage = ["jpg", "jpeg", "png", "gif"].includes(fileExtention);
        // Print to console
        console.log(fileSize, fileExtention, fileNameOnly, isImage);
      }
    },
  },

Enter fullscreen mode

Exit fullscreen mode

The above code helps us handle our initial file upload logic and extracts certain information from the selected file. First, we need to perform our first validation to be sure a file was selected and then we get the uploaded file and extract the file size, file extension, file name and then check whether or not the selected file is an image.

To actually see this work, we need to call the function in some way.

We will call this function from input element using the @change event listener in Vue which is similar to onchange in regular JavaScript. We will update our input element to look like this

<input type="file" name="" id="" @change="handleFileChange($event)" />

Enter fullscreen mode

Exit fullscreen mode

Here, we listen to a change event on our input and then call our handleFileChange function. After this, we can go ahead to test what we have achieved by uploading a file from our file directory. If you have been following the discourse, you should see an output similar to the screenshot below

Let us move on to perform some validation based on the data we have in our props. Remember, in our prop, we set a max file size of 5 and told our component to only accept png files. When a file is selected, we want to handle these validations.

First of all, we create a new errors array in our data object.

// FileUpload.vue
  data() {
    return {
      errors: [],
      ...
    };
  },

Enter fullscreen mode

Exit fullscreen mode

And then we update our markup to be able to render any possible error that occurs.

// FileUpload.vue
<template>
  <div class="file-upload">
    <div class="file-upload__area">
      <div>
        <input type="file" name="" id="" @change="handleFileChange($event)" />
        <div v-if="errors.length > 0">
          <div
            class="file-upload__error"
            v-for="(error, index) in errors"
            :key="index"
          >
            <span>{{ error }}</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

Enter fullscreen mode

Exit fullscreen mode

In our markup, we loop through our errors array to check if any errors exist and print that out on our UI. For this operation, we take advantage of two built-in Vue directive called v-for and v-if. More details about these can be found here.

Next up we create three new functions:

  • isFileSizeValid which takes a parameter of fileSize and will handle validation for file size
  • isFileTypeValid which takes a parameter of fileExtension and will handle the validation for accepted file type(s)
  • isFileValid which will take the paramater of file which will be the object for the uploaded file

We will proceed to add these functions together with their logic in our methods object. Update your code to look like what is seen below

// FileUpload.vue
  methods: {
    ...
    isFileSizeValid(fileSize) {
      if (fileSize <= this.maxSize) {
        console.log("File size is valid");
      } else {
          this.errors.push(`File size should be less than ${this.maxSize} MB`);
      }
    },
    isFileTypeValid(fileExtention) {
      if (this.accept.split(",").includes(fileExtention)) {
        console.log("File type is valid");
      } else {
        this.errors.push(`File type should be ${this.accept}`);
      }
    },
    isFileValid(file) {
      this.isFileSizeValid(Math.round((file.size / 1024 / 1024) * 100) / 100);
      this.isFileTypeValid(file.name.split(".").pop());
      if (this.errors.length === 0) {
        return true;
      } else {
        return false;
      }
    },
  },

Enter fullscreen mode

Exit fullscreen mode

In our isFileSizeValid function we basically performed a simple operation to check if the size of the uploaded file is less than or equal to the value set in our prop. We also checked if the file type is an accepted file type defined in our accept prop as well as in our isFileTypeValid. Finally, we defined the isFileValid function which basically calls our earlier defined functions and passes arguments for fileSize and fileExtension. If any of the condition fails, we basically push an error message into our errors array which will be printed to the user

To use our new functions, we would need to make slight modifications to our handleFileChange function. Update your code to look like the code below.

// FileUpload.vue
methods: {
    handleFileChange(e) {
      this.errors = [];
      // Check if file is selected
      if (e.target.files && e.target.files[0]) {
             // Check if file is valid
        if (this.isFileValid(e.target.files[0])) {
          // Get uploaded file
          const file = e.target.files[0],
            // Get file size
            fileSize = Math.round((file.size / 1024 / 1024) * 100) / 100,
            // Get file extention
            fileExtention = file.name.split(".").pop(),
            // Get file name
            fileName = file.name.split(".").shift(),
            // Check if file is an image
            isImage = ["jpg", "jpeg", "png", "gif"].includes(fileExtention);
          // Print to console
          console.log(fileSize, fileExtention, fileName, isImage);
        } else {
          console.log("Invalid file");
        }
      }
    },
    ...
}

Enter fullscreen mode

Exit fullscreen mode

Here, we have modified our code to check if all validations are passed and then allow the user to select the file. Go ahead and test this out to be sure everything looks right. If it does, you should see outputs similar to the screenshot below.

Great to see you have made it this far! I’m proud of you.

The final phase will be to preview our uploaded file and send this data to our parent component.

To do this, firstly, we need to make some modifications to our component markup. Update your markup to look like this:

// FileUpload.vue
<template>
  <div class="file-upload">
    <div class="file-upload__area">
      <div v-if="!file.isUploaded">
        <input type="file" name="" id="" @change="handleFileChange($event)" />
        <div v-if="errors.length > 0">
          <div
            class="file-upload__error"
            v-for="(error, index) in errors"
            :key="index"
          >
            <span>{{ error }}</span>
          </div>
        </div>
      </div>
      <div v-if="file.isUploaded" class="upload-preview">
        <img :src="file.url" v-if="file.isImage" class="file-image" alt="" />
        <div v-if="!file.isImage" class="file-extention">
          {{ file.fileExtention }}
        </div>
        <span>
          {{ file.name }}{{ file.isImage ? `.${file.fileExtention}` : "" }}
        </span>
      </div>
    </div>
  </div>
</template>

Enter fullscreen mode

Exit fullscreen mode

In the new block of code added, we checked if the file has been selected and we hid the input element and showed a new set of elements which will help us preview the selected file. We also checked if the selected file is an image so we could render the image and then finally we displayed the name of the selected file.

To see this action, we need to make some modifications to our handleFileChange function. Update your code to look like:

// FileUpload.vue
methods:{
  handleFileChange(e) {
      this.errors = [];
      // Check if file is selected
      if (e.target.files && e.target.files[0]) {
        // Check if file is valid
        if (this.isFileValid(e.target.files[0])) {
          // Get uploaded file
          const file = e.target.files[0],
            // Get file size
            fileSize = Math.round((file.size / 1024 / 1024) * 100) / 100,
            // Get file extention
            fileExtention = file.name.split(".").pop(),
            // Get file name
            fileName = file.name.split(".").shift(),
            // Check if file is an image
            isImage = ["jpg", "jpeg", "png", "gif"].includes(fileExtention);
          // Print to console
          console.log(fileSize, fileExtention, fileName, isImage);
          // Load the FileReader API
          let reader = new FileReader();
          reader.addEventListener(
            "load",
            () => {
              // Set file data
              this.file = {
                name: fileName,
                size: fileSize,
                type: file.type,
                fileExtention: fileExtention,
                isImage: isImage,
                url: reader.result,
                isUploaded: true,
              };
            },
            false
          );
        } else {
          console.log("Invalid file");
        }
      }
    },
}

Enter fullscreen mode

Exit fullscreen mode

Above, we introduced some new piece of code, but the most important of them all is the FileReader. This helps us read the contents of the uploaded file and use reader.readAsDataURL to generate a URL which we can use to preview our uploaded file. You can get a detailed breakdown on all the features of the File Reader here.

We then update our file object with appropriate data which we will use to update our user interface.

We can also add some basic CSS to improve the visuals of our preview. Update your style section with the code below,

// FileUpload.vue
...
<style>
.file-upload .file-upload__error {
  margin-top: 10px;
  color: #f00;
  font-size: 12px;
}
.file-upload .upload-preview {
  text-align: center;
}
.file-upload .upload-preview .file-image {
  width: 100%;
  height: 300px;
  object-fit: contain;
}
.file-upload .upload-preview .file-extention {
  height: 100px;
  width: 100px;
  border-radius: 8px;
  background: #ccc;
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0.5em auto;
  font-size: 1.2em;
  padding: 1em;
  text-transform: uppercase;
  font-weight: 500;
}
.file-upload .upload-preview .file-name {
  font-size: 1.2em;
  font-weight: 500;
  color: #000;
  opacity: 0.5;
}

</style>

Enter fullscreen mode

Exit fullscreen mode

You can proceed test this out and if everything works fine, you should see a screen similar the one below.

We can also create a function to reset our data which gives the user a neat way to change their selected file without having to refresh the page.

To do this, we will need to create a new function called resetFileInput and update our code to look like this:

// FileUpload.vue
methods:{
     ...
     resetFileInput() {
      this.uploadReady = false;
      this.$nextTick(() => {
        this.uploadReady = true;
        this.file = {
          name: "",
          size: 0,
          type: "",
          data: "",
          fileExtention: "",
          url: "",
          isImage: false,
          isUploaded: false,
        };
      });
    },
}

Enter fullscreen mode

Exit fullscreen mode

Here, we have basically reset our state to its default. We can then update our markup with a button to call this function. Update your markup to look like:

// FileUpload.vue

<template>
  <div class="file-upload">
    <div class="file-upload__area">
      <div v-if="!file.isUploaded">
        <input type="file" name="" id="" @change="handleFileChange($event)" />
        <div v-if="errors.length > 0">
          <div
            class="file-upload__error"
            v-for="(error, index) in errors"
            :key="index"
          >
            <span>{{ error }}</span>
          </div>
        </div>
      </div>
      <div v-if="file.isUploaded" class="upload-preview">
        <img :src="file.url" v-if="file.isImage" class="file-image" alt="" />
        <div v-if="!file.isImage" class="file-extention">
          {{ file.fileExtention }}
        </div>
        <span class="file-name">
          {{ file.name }}{{ file.isImage ? `.${file.fileExtention}` : "" }}
        </span>
        <div class="">
          <button @click="resetFileInput">Change file</button>
        </div>
      </div>
    </div>
  </div>
</template>

Enter fullscreen mode

Exit fullscreen mode

Finally, we can then send the contents of our selected file to our parent component. To do this, we first create a new function called sendDataToParent and we add the code below to it.

// FileUpload.vue

methods:{
    ...
    sendDataToParent() {
      this.resetFileInput();
      this.$emit("file-uploaded", this.file);
    },

}

Enter fullscreen mode

Exit fullscreen mode

Above, we created a custom event listener (more info on this here ) called file-uploaded which we will listen for in our parent component and then send the selected file when the event is triggered. We also reset our state.

We will also need to call our new function to trigger this event. To do this, we will update our markup with a button which when clicked, will trigger this event. We can update our markup to look like this.

// FileUpload.vue

<template>
  <div class="file-upload">
    <div class="file-upload__area">
      <div v-if="!file.isUploaded">
        <input type="file" name="" id="" @change="handleFileChange($event)" />
        <div v-if="errors.length > 0">
          <div
            class="file-upload__error"
            v-for="(error, index) in errors"
            :key="index"
          >
            <span>{{ error }}</span>
          </div>
        </div>
      </div>
      <div v-if="file.isUploaded" class="upload-preview">
        <img :src="file.url" v-if="file.isImage" class="file-image" alt="" />
        <div v-if="!file.isImage" class="file-extention">
          {{ file.fileExtention }}
        </div>
        <span class="file-name">
          {{ file.name }}{{ file.isImage ? `.${file.fileExtention}` : "" }}
        </span>
        <div class="">
          <button @click="resetFileInput">Change file</button>
        </div>
        <div class="" style="margin-top: 10px">
          <button @click="sendDataToParent">Select File</button>
        </div>
      </div>
    </div>
  </div>
</template>

Enter fullscreen mode

Exit fullscreen mode

To see it in action, we will need to make some modifications to our parent component. To achieve this, we will navigate to our App.vue file and update our code to look like this:

<template>
  <div>
    <div>
      <p>Upload a file</p>
      <button @click="showFileSelect = !showFileSelect">Select a file</button>
    </div>
    <div v-show="showFileSelect">
      <FileUpload :maxSize="1" accept="png" @file-uploaded="getUploadedData" />
    </div>

    <div v-if="fileSelected">
      Successfully Selected file: {{ file.name }}.{{ file.fileExtention }}
    </div>
  </div>
</template>

<script>
import FileUpload from "@/components/FileUpload.vue";

export default {
  name: "App",
  components: {
    FileUpload,
  },
  data() {
    return {
      file: {},
      fileSelected: false,
      showFileSelect: false,
    };
  },
  methods: {
    getUploadedData(file) {
      this.fileSelected = true;
      this.showFileSelect = false;
      this.file = file;
    },
  },
};
</script>
<style>
#app {
  text-align: center;
}
</style>

Enter fullscreen mode

Exit fullscreen mode

Above, we have added some data to control our state. Firstly, we defined a file object to hold our received file, we have fileSelected boolean to control our interface behaviour and then we have showFileSelect to basically toggle our File Upload component.

In our markup, we also added a new code. A button to toggle our File Upload component, a custom event listener which listens for a file-uploaded event and triggers a getUploadedData function. In our getUploadedData, we simply performed a user interface logic and then set the data received from our component to our parent’s file object.

It is important to note that from here, you could also proceed to upload this file to a backend server after you have received the data from the component or perform any other type of action you intend to with this file.

If everything is done right, you should have a similar experience to this:

Conclusion

Congrats! You have made it to the end and I hope you were able to learn some new tricks and tips from this. Take it up as a challenge and extend the features covered here and perhaps do something even more awesome with it.

Resources

You can find the complete code for this on GitHub.

You can also play around with a live demo here.