Header illustration for Creating a file upload with Vue.js

Creating a file upload with Vue.js

Learn how to create a file upload with Vue.js and how to use it in your application.

Introduction

This is a short article meant to demystify the workings of a javascript file upload component. I've had to implement this in many different applications, and used pretty much all of the solutions out there. Some are great, others are overkill at best. I ran into the situation quite often that a much simpler solution would've saved me time while implementing, but also when maintaining. This article aims to show you how easy it can be to implement a file upload solution in Vue 3 yourself, and have more flexibility over all the options.

Often used solutions include some form of validation, allowing to delete uploaded files, drag and drop uploads, and support for multiple files. So we are going to build those features.

For this article I assume you have already set up a Vue 3 project, since I am always annoyed by articles that spend 50% of their content on setting up a default project. Straight to business here.

We start off with creating a new Vue Component. I called mine Uploads.vue and I put in components folder where it will be auto imported by Nuxt.

Starting simple

So let's start with the simplest solution we can find: A file input. This is an HTML element with type="file". A quick glance at the properties that this input element can take, and you can already notice that a lot of the magic you might've thought a library was handling for you is just a simple property on the actual input element. We can wrap this in a form and submit the form on a button click.

The code would look something like this



<template>
  <div>
    <input
      ref="input"
      type="file"
    >
    <button @click="uploadFile">
      Upload
    </button>
  </div>
</template>
  
<script setup lang="ts">
const input = ref<HTMLDivElement>()

const uploadFile = () => {
// make a request
} 
</script>

All fine and dandy so far. This is a good base to built on. What we're going to do first, is explore the options to add support for drop zones. This feature will enable the user to select one or multiple files, drag them over the input and upload them straight away.

Drag and drop support

To do this, we need drag and drop support. But that's super difficult right? Wrong. We are going to reach for the dropzone from vue-use. A super lightweight dropzone that does this, and only this: https://vueuse.org/core/useDropZone/#usedropzone

It's 629B at the time of writing, so super small. Make sure you install @vueuse/core.

Essentially what we're going to do is wrap a large div container around the input field, catch the dragged and dropped files from there, and forcefully put them in the input field.


<template>
  <div
    ref="dropZoneRef"
    class="col-span-full"
  >
    <div class="text-center">
      <div class="mt-4 flex text-sm leading-6 text-gray-600">
        <input
          ref="input"
          type="file"
          multiple
          name="file-upload"
          class="sr-only"
          @change="(e) => onFileChange(e)"
        >
      </div>
    </div>
  </div>
  </div>
</template>

<script setup lang="ts">
import { useDropZone } from '@vueuse/core'

const dropZoneRef = ref<HTMLDivElement>()
const input = ref<HTMLInputElement>();

const onDrop = (files: File[] | null) => {
    void handleFileChange(files ?? [])
}

const onFileChange = async ($event: Event) => {
    const target = $event.target as HTMLInputElement;
    await handleFileChange(Array.from(target.files ?? []));
}

const handleFileChange = async (files: File[]) => {
// send the files to your backend
}

const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)

</script>

Okay, lots to unpack here but bear with me. It's not difficult at all. First off, I added some tailwind classes for styling. You don't have to use it and you can definitely bring your own style.

As you can see in the script part of the component, the dropzone does the heavy lifting. With isOverDropZone, we get a boolean that turns true when the user hovers of the dropzone. We'll use it for styling in the next step. The onDrop is the callback that we put in that will get triggered when the user drops the file.

This boolean, we can use to give the user an indication of dragging and dropping. If the boolean is true, add a green border; else make it gray. This is really easy with Vue dynamic classes and would look something like this:


<template>
  <div
    ref="dropZoneRef"
    class="col-span-full"
  >
    <div
      class="flex justify-center rounded-lg border border-dashed border-gray-900/25 px-6 py-10"
      :class="[
        isOverDropZone ? 'bg-gray-300 border-green/25 border-2' : 'bg-white',
        errors.length > 0 ? 'border-red-600' : 'border-gray-300'
      ]"
    >
      // the rest of the component 
    </div>
  </div>
</template>

I have also added a ref with errors, that we can use to show a red border.

Now most of the validation I handle in the backend. If the function handleFileChange returns any errors, I put them in the errors ref and it will automagically show a red border around the input, indicating something went wrong. You can also show some text below the input.

Adding customization props

At the moment this component is already quite useful, and definitely resembles a full fledged upload component. However, there's a few things that are missing. When you want to reuse this component in multiple places in your application, you probably want to configure some of the values of the component differently each time you reuse it. That's were props come in, to set some of the rules and boundaries of this component.

I use the following props for this component: accepts and maxsize. There are quite a few more props I can think of, but I want to keep this fairly simple for the sake of this article. The prop definition looks like this:


const props = defineProps({
    maxsize: {
        type: Number,
        default: 30000000
    },
    accept: {
        type: String,
        default: ''
    }
  })

I use both props to display the information for the user, and don't use them for validation at all. Maxsize is pretty self explanatory and accepts is just a string saying: This component accepts '.pdf, .png and .jpg' in a label like manner.

Wrapping it up

Okay, we've covered quite a lot of ground. The component so far looks like this:

Uploads component

And the code looks like this:


<template>
  <div
    ref="dropZoneRef"
    class="col-span-full"
  >
    <div
      class="flex justify-center rounded-lg border border-dashed border-gray-900/25 px-6 py-10"
      :class="[
        isOverDropZone ? 'bg-gray-300 border-green/25 border-2' : 'bg-white',
        errors.length > 0 ? 'border-red-600' : 'border-gray-300'
      ]"
    >
      <div class="text-center">
        <div class="mt-4 flex text-sm leading-6 text-gray-600">
          <label
            for="file-upload"
            class="relative cursor-pointer rounded-md font-semibold text-primary-400 focus-within:outline-none focus-within:ring-2 focus-within:ring-primary-700 focus-within:ring-offset-2 hover:text-primary-300"
            @click="open()"
          >
            <input
              ref="input"
              type="file"
              :accept="accept"
              multiple
              name="file-upload"
              class="sr-only"
              @change="(e) => onFileChange(e)"
            >
          </label>
        </div>
        <p class="text-xs leading-5 text-gray-600">
          {{ accept.length > 0 ? `Accepts ${props.accept} up to ${useFileSize(props.maxsize)}` : '' }}
        </p>
      </div>
    </div>
    <p
      v-if="errors.length > 0"
      class="text-xs leading-5 text-red-600 w-full text-center"
    >
      <span
        v-for="error in errors"
        :key="error"
      >
        {{ error }}
      </span>
    </p>
  </div>
</template>
  
<script setup lang="ts">
import { useDropZone } from '@vueuse/core'
import { AxiosError } from 'axios';
import { useI18n } from 'vue-i18n';

const props = defineProps({
  maxsize: {
    type: Number,
    default: 30000000
  },
  accept: {
    type: String,
    default: ''
  },
})

const emit = defineEmits({
  uploaded: () => true,
})

const { t } = useI18n()

const dropZoneRef = ref<HTMLDivElement>()
const input = ref<HTMLInputElement>();
const errors = ref<string[]>([])

const onDrop = (files: File[] | null) => {
  void handleFileChange(files ?? [])
}

const onFileChange = async ($event: Event) => {
  const target = $event.target as HTMLInputElement;
  await handleFileChange(Array.from(target.files ?? []));
}

const handleFileChange = async (files: File[]) => {
  errors.value = []
  if (files.length === 0) return;

  for (let file of files) {
    try {
      // Make a request to your backend here
      emit('uploaded');
    } catch (e: unknown) {
      const error = e as AxiosError
      if (error.response?.status === 422) {
        const validationError = ((error.response.data as { errors: unknown }).errors ?? []) as { field: string, message: string }[]
        errors.value.push(...validationError.map(e => e.message))
        return
      }
      if (error.response?.status === 413) {
        errors.value.push(t('ui.upload.error.file_too_large'))
      }
      throw new Error(error.message)
    }
  }
}

const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)

const open = () => {
  input.value?.click();
}
</script>

I hope this component give you a good base to built up on. If you have any idea's to improve this or have any questions about implementation, please drop me a message!

Creating, updating and maintaining PDF's in any codebase can be a pain. You have to make sure all the packages stay up to date, eventually you run in to some limitations of any library that leaves you unable to design the document exactly how you want it

This is why we created PDFdesignAPI.com, an easy visual document designer connected to an API. Design your perfect document, send the data to the API and get a beautifull PDF in return.

Want to know more? Try it today for free.

Start for free.

Dont wait any longer, start your free trial today and start creating your own documents.