Back

Building a custom file upload component for Vue

Building a custom file upload component for Vue

When building applications for either the web or mobile, very often, the need to make a provision for file upload arises. Many use cases exist for file uploads, including 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 explore 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 many of the concepts can be extended and used for multiple file uploads.

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 available in Vue here

Project Setup

In this article, I will use 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

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 look similar to the image below.

1

If you have not already started, proceed to start your development server to get your project up and running in your browser. This 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.

Congrats if you have made it this far. I am proud of you! We will continue building out our upload component, 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>

For now, we have 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>

The above code will enable us to see what our component looks like now and in the future. If you followed the instructions correctly, you should see something similar to the image below when you visit your browser’s development URL in your browser. 2

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, we will define a few props to help us control how our component should behave. Props are a useful way to pass data from parent components 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/*",
    },
  },
  };
  • The maxSize prop helps our component be more dynamic by specifying the maximum file size our component can accept.
  • 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,
      },
    };
  },

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 defined our state, we can update our UI to better reflect what we have done.

First, we will head 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>

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

Having achieved our data setup, we can go on to define some logic we want to perform. First on the list will be to 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 extension
          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);
      }
    },
  },

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, extract the file size, file extension, and file name and then check whether or not the selected file is an image.

To see this work, we need to call the function somehow.

We will call this function from the 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)" />

Here, we listen to a change event on our input and 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.

3

Performing validations

Let us 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, we create a new errors array in our data object.

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

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>

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 directives 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 parameter of file, which will be the object for the uploaded file

We will 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;
      }
    },
  },

In our isFileSizeValid function, we performed a simple operation to check if the uploaded file size 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 and our isFileTypeValid. Finally, we defined the isFileValid function, which calls our earlier defined functions and passes arguments for fileSize and fileExtension. If any of the conditions fail, we push an error message into our errors array, which will be shown to the user.

To use our new functions, we will 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 extension
            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");
        }
      }
    },
    ...
}

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.

4

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

Previewing and sending the file

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 modify 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>

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

We need to modify our handleFileChange function to see this action. Update your code to look like this:

// 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 extension
            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");
        }
      }
    },
}

Above, we introduced some new pieces of code, but the most important of them is FileReader. This helps us read the uploaded file’s contents and use reader.readAsDataURL to generate a URL that we can use to preview our uploaded file. You can get a detailed breakdown of 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.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data. OpenReplay Start enjoying your debugging experience - start using OpenReplay for free.

Improving styles

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-extension {
  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>

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

5

Adding a “reset” option

We can also create a function to reset our data, giving the user a neat way to change their selected file without refreshing 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,
        };
      });
    },
}

Here, we have 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 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>
    </div>
  </div>
</template>

Sending the file

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

// FileUpload.vue

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

}

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 that will trigger this event when clicked. 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>

We need to modify our parent component to see it in action. 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>

Above, we have added some data to control our state. Firstly, we defined a file object to hold our received file, and we have fileSelected boolean to control our interface behavior. Then we have showFileSelect to 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 receiving the data from the component or perform any other action you intend to with this file.

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

6

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, extend the features covered here, and perhaps do something even more awesome with it.

Resources

You can find the complete code on GitHub.

You can also play around with a live demo here.

A TIP FROM THE EDITOR: For more on Vue3, check out our Vue 3 - The Evolution Of Vue and An ultimate guide to the Vue 3 Composition API articles.

newsletter