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
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>
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>
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/*",
},
},
};
- 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,
},
};
},
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>
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);
}
},
},
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)" />
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: [],
...
};
},
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 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 offileSize
and will handle validation for file sizeisFileTypeValid
which takes a parameter offileExtension
and will handle the validation for accepted file type(s)isFileValid
which will take the paramater offile
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;
}
},
},
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");
}
}
},
...
}
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>
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");
}
}
},
}
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>
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,
};
});
},
}
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>
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.
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 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>
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>
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.