Building a vue.js drag-and-drop file component

3 years ago
#frontend#tailwind#vue.js#javascript

I was recently tasked with a drag-and-drop file upload component for a guest-entries form. In this post we'll explore how to build just that with vue.js.

(scroll down to see a live demo)

Jesse ramirez Yi SD 1e J 1g unsplash

Creating the markup

This is the easy part (really!). The main principle we'll be using is: hide our file input field (visually only, don't actually display: none; it) and wrap our drag-and-drop zone inside a label field.

We're using Tailwind to create a simple HTML component. I've already included the necessary vue attributes:

<div class="flex items-center justify-center w-full h-screen text-center" id="app">
  <div class="p-12 bg-gray-100 border border-gray-300" @dragover="dragover" @dragleave="dragleave" @drop="drop">
    <input type="file" multiple name="fields[assetsFieldHandle][]" id="assetsFieldHandle" 
      class="w-px h-px opacity-0 overflow-hidden absolute" @change="onChange" ref="file" accept=".pdf,.jpg,.jpeg,.png" />
  
    <label for="assetsFieldHandle" class="block cursor-pointer">
      <div>
        Explain to our users they can drop files in here 
        or <span class="underline">click here</span> to upload their files
      </div>
    </label>
    <ul class="mt-4" v-if="this.filelist.length" v-cloak>
      <li class="text-sm p-1" v-for="file in filelist">
        ${ file.name }<button type="button" @click="remove(filelist.indexOf(file))" title="Remove file">x</button>
      </li>
    </ul>
  </div>
</div>

Now if you would run this, these couple of lines already work! But some things are missing:

  • We can't see which files are selected (because we hid the input element).
  • Since we can't see the files, we also can't remove them
  • Our area as a whole is clickable, but we still have to support drag-and-drop

Vue is a great framework to simplify all this functionality. Let's have a look at the code:

new Vue({
  el: '#app',
  delimiters: ['${', '}'], // Avoid Twig conflicts
  data: {
    filelist: [] // Store our uploaded files
  },
  methods: {
    onChange() {
      this.filelist = [...this.$refs.file.files];
    },
    remove(i) {
      this.filelist.splice(i, 1);
    },
    dragover(event) {
      event.preventDefault();
      // Add some visual fluff to show the user can drop its files
      if (!event.currentTarget.classList.contains('bg-green-300')) {
        event.currentTarget.classList.remove('bg-gray-100');
        event.currentTarget.classList.add('bg-green-300');
      }
    },
    dragleave(event) {
      // Clean up
      event.currentTarget.classList.add('bg-gray-100');
      event.currentTarget.classList.remove('bg-green-300');
    },
    drop(event) {
      event.preventDefault();
      this.$refs.file.files = event.dataTransfer.files;
      this.onChange(); // Trigger the onChange event manually
      // Clean up
      event.currentTarget.classList.add('bg-gray-100');
      event.currentTarget.classList.remove('bg-green-300');
    }
  }
});

In just a couple of lines we've added all the necessary ux features for a drag-and-drop file component.

Craft Tips

A couple of things are worth mentioning if you are planning to use this in a Craft guest-entries setup.

  • Make sure you add enctype="multipart/form-data" to your form.
  • I would strongly suggest you check the maxUploadFileSize setting, and build in some validation to prevent massive file uploads from your users.

This setup only starts uploading when you hit the submit button. In some cases this would be disadvantageous. But I would argue that in most cases, this is actually a good thing, as it prevents unnecessary file uploads on your server for users that decide not to submit the form.

The component in action