Nice and clean file upload modal window made with vue + bootstrapvue

5/25/2021

Vue.js BootstrapVue

Straight to the full code!

The code is fairly simple and commented, no need for how to guide.

The modal consists of a drag&drop area where the user can drop files or click to choose from computer and a list of the files selected. The files are saved in an array together with a custom filename property the user can edit in order to rename the selected files before uploading them. Every time new files are dropped/selected, the previously added files are not lost. This way the user can add files separately.

<template>
  <!--v-model: to show/hide the modal-->
  <!--size: when one or more files are selected (filelist.length) expand the modal horizontally-->
  <b-modal
      v-model="showModal"
      id="add-attachments-modal"
      centered
      hide-footer
      title="File(s) Upload"
      :size="filelist.length ? 'lg' : ''">
    <b-form @submit="onSubmit">
      <b-row class="mt-2 px-3">

        <!--the file list column has different width based on boreakpoint-->
        <!--and is shown only if one or more files are selected-->
        <b-col sm="12" xl="8" v-if="this.filelist.length" v-cloak>

          <!--show a list of the selected files, with an input to (optionally) rename the file before upload-->
          <b-list-group>
            <b-list-group-item class="d-inline-flex p-2" v-for="(file, i) in filelist" :key="i">

              <!--file name input to rename the file before upload-->
              <b-form-input
                  placeholder="File name..."
                  v-model="filelist[i].fileName"
                  required
              />

              <!--file extension-->
              <span class="d-inline-flex align-items-center text-muted ml-1 mr-3">.{{ filelist[i].extension }}</span>

              <!--remove file from list (and from upload)-->
              <b-button variant="outline-danger" size="sm" @click="remove(filelist.indexOf(file))">
                <b-icon-trash></b-icon-trash>
              </b-button>

            </b-list-group-item>
          </b-list-group>

        </b-col>

        <!--this column contains the file drop area-->
        <!--its width is automatically set based on existance of the file list column and breakpoint-->
        <b-col class="file-drop-column my-4 my-xl-0 p-0">
          <div class="file-drop-area bg-light" @dragover="dragover" @drop="drop">

            <!--we need an actual file input in order to have the drag&drop functionality-->
            <!--and the File object(s) containing the files dropped-->
            <!--We keep it invisible because we want to display something different than a file input-->
            <input type="file" multiple name="file-input" id="file-input"
                   class="file-input invisible" @change="onChange"/>

            <!--clicking on this label will trigger the clicked event on the file input-->
            <!--this will cause the 'choose file' system dialog-->
            <label for="file-input" class="w-100 text-center cursor-pointer text-muted m-0 px-3 py-5">
              <p>
                <b-icon-cloud-upload-fill font-scale="3"></b-icon-cloud-upload-fill>
              </p>
              <div>
                <p class=mb-0>Drop your file(s) here</p>
                <p class=mb-0>or <span class=text-info>choose from computer</span></p>
              </div>
            </label>
          </div>
        </b-col>
      </b-row>

      <!--this div contains the two action buttons cancel and upload-->
      <!--It is totally possible to use the bootstrapvue footer buttons instead-->
      <div class="float-right mt-4">
        <b-button variant="secondary" @click="$bvModal.hide('add-attachments-modal')" class="mr-3">
          Cancel
        </b-button>
        <b-button type="submit" variant="success">
          Upload file(s)
        </b-button>
      </div>
    </b-form>
  </b-modal>

</template>

<script>
export default {
  name: "FileUpload",
  data() {
    return {
      showModal: true,
      filelist: [],     //we use a custom array to store the files because we want the user to rename the file before upload
    }
  },
  methods: {
    /*triggered when form is submitted (upload button clicked)*/
    onSubmit() {
      event.preventDefault();
      console.log('Perfect! Now implement some way to upload these files: ');
      console.log(this.filelist);
    },
    /*Every time a new file is selected (both by dropping or choosing from computer)
    * we push them inside our array. This way we keep the previously uploaded files
    * inside. A user can drop a file, then drop another one without losing the first one*/
    onChange(event) {
      const fileArray = [...event.target.files];  //put the files in a temporary array
      fileArray.forEach((file) => {
        const fileName = file.name.replace(/\.[^/.]+$/, "");  //extract filename (no extension) from the file full name
        const extension = file.name.split('.').pop();         //extract extensionfrom the file fullname
        this.filelist.push({fileName, extension, file});      //store selected files and additional data in our array
      })
    },
    /*Remove the file from the list. It will not be uploaded*/
    remove(i) {
      this.filelist.splice(i, 1);
    },
    dragover(event) {
      event.preventDefault();
    },
    drop(event) {
      event.preventDefault();
      this.onChange();
    }
  }
}
</script>

<style lang="scss">
div#add-attachments-modal {
  div.modal-dialog {
    transition: max-width 0.3s ease-out, transform 0.3s ease-out;
  }
  .col.file-drop-column {
    flex-shrink: 1;
  }
  div.file-drop-area {
    border: 2px dashed rgba(0, 0, 0, 0.25);
    input.file-input {
      position: absolute;
      height: 0;
    }
  }
}
</style>

Here is a quick video demonstrating functionality:

Was it useful?

Buy me a coffee :)