Custom fileuploader in javascript

Learn how to create custom fileuploader with Jquery, ES6 and Bootstrap4.

We will create a file uploader with custom design and an option to preview selected files and remove them.

Demo

Check out the live demo here.

Implementation

  • We will use html5 fileuploader to upload the files.
  • Then with the help of Bootstrap popover, we will preview the selected files.
  • While previewing the file we will provide an option to remove the selected file.
  • As Jquery is one of the dependencies for Bootstrap popover, we will use to ease our work.

Dependencies

<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
    
<!-- Font Awesome -->
<script defer src="https://use.fontawesome.com/releases/v5.7.2/js/all.js" integrity="sha384-0pzryjIRos8mFBWMzSSZApWtPl/5++eIfzYmTgBBmXYdhvxPc+XcFEk+zJwDgWbP" crossorigin="anonymous"></script>

<!-- jQuery library -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<!-- Popper JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>

<!-- Latest compiled JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>

HTML layout for fileuploader

<div class="custom-file-picker"> 
    <div class="picture-container form-group">
        <h4 class="info_text">Upload Proof of Physical Address</h4>
        <div class="picture">
            <span class="icon"><i class="fas fa-file-upload"></i></span>
            <input type="file" class="wizard-file" multiple id="a8755cf0-f4d1-6376-ee21-a6defd1e7c08">
            <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 37 37" xml:space="preserve">
                <path class="circ path" style="fill:none;stroke:#77d27b;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:10;" d="M30.5,6.5L30.5,6.5c6.6,6.6,6.6,17.4,0,24l0,0c-6.6,6.6-17.4,6.6-24,0l0,0c-6.6-6.6-6.6-17.4,0-24l0,0C13.1-0.2,23.9-0.2,30.5,6.5z"></path>
                <polyline class="tick path" style="fill:none;stroke:#77d27b;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:10;" points="11.6,20 15.9,24.2 26.4,13.8 "></polyline>
            </svg>
        </div>     
    </div>
    <div class="popover-container text-center">
        <p data-toggle="popover" data-id="a8755cf0-f4d1-6376-ee21-a6defd1e7c08" class="btn-popover" data-original-title="" title="">
            <span class="file-total-viewer">0</span> Files Selected <input type="button" value="view" href="javascript:void(0)" class="btn btn-success btn-xs btn-file-view">
        </p>
    </div>
</div>

Explanation

  • We have created a container named custom-file-picker.
  • In this we have our custom file uploadpicture-container and our popover previewer popover-container.
  • Every file picker has unique id a8755cf0-f4d1-6376-ee21-a6defd1e7c08 and its corresponding popover refers to that id data-target="a8755cf0-f4d1-6376-ee21-a6defd1e7c08" to preview the files.

Styling our components

.picture-container{
    position: relative;
    cursor: pointer;
    text-align: center;
    margin-bottom: 15px;
}

.picture{
    width: 100px;
    height: 100px;
    background-color: #fff;
    color: #FFFFFF;
    margin: 5px auto;
    overflow: hidden;
    -webkit-transition: all 0.2s;
    -moz-transition: all 0.2s;
    -o-transition: all 0.2s;
    transition: all 0.2s;
}

.picture > .icon{
    width: 100%;
    height: 100%;
    display: inline-block;
    color: #37474F;
    border: 4px solid #CCCCCC;
    -webkit-border-radius: 50%;
    -moz-border-radius: 50%;
    -o-border-radius: 50%;
    border-radius: 50%;
    -webkit-transition: all 0.2s;
    -moz-transition: all 0.2s;
    -o-transition: all 0.2s;
    transition: all 0.2s;
}

.picture:hover .icon{
    border-color: greenyellow;
}

.picture > .icon > svg{
    height: 1.4em;
    font-size: 4em;
}

.picture > svg{
    width: 100%;
    height: 100%;
}

.picture input[type="file"]{
    cursor: pointer;
    display: block;
    height: 100%;
    left: 0;
    opacity: 0 !important;
    position: absolute;
    top: 0;
    width: 100%;
    z-index: 100;
    right: 0;
    bottom: 0;
}

/*svg tick animation*/
.circ {
    opacity: 0;
    display: none;
    stroke-dasharray: 130;
    stroke-dashoffset: 130;
    -webkit-transition: all .75s;
    -moz-transition: all .75s;
    -ms-transition: all .75s;
    -o-transition: all .75s;
    transition: all .75s;
}
.tick{
    stroke-dasharray: 50;
    stroke-dashoffset: 50;
    -webkit-transition: stroke-dashoffset .4s 0.5s ease-out;
    -moz-transition: stroke-dashoffset .4s 0.5s ease-out;
    -ms-transition: stroke-dashoffset .4s 0.5s ease-out;
    -o-transition: stroke-dashoffset .4s 0.5s ease-out;
    transition: stroke-dashoffset .4s 0.5s ease-out;
}
.drawn > svg .path{
    display: block;
    opacity: 1;
    stroke-dashoffset: 0;
}

.drawn{
    border-color: #fff;
}

[data-toggle="popover"]{
    cursor: pointer;
}

span.popover-content-remove {
    padding-left: 10px;
    color: red;
    cursor: pointer;
    float: right;
}

.pb10{
    padding-bottom: 10px;
}

.popover-header{
    text-align: center;
}

.popover{
    min-width: 200px;
}

Handling the functionality

Now we have styled our components it is time to handle the functionality. We will use Jquery with ES6 to make things easy.

Storing the files

We will create a global variable to store the files.

//Global object to store the files
let fileStorage = {};

We will use this variable to store all the files of the corresponding file picker with the help of its id.

Now we will create a function which will manage the storing of the file and displaying the count of the files. This function will take id and array of files as input.

//Store the file for particular filepicker
let storeFile = (id, files) => {
  fileStorage[id] = files;

  //Update the file count
  $(`[data-id="${id}"] > .file-total-viewer`).text(files.length);
}

$(`[data-id="${id}"] > .file-total-viewer`).text(files.length); will update the file count in popover previewer.

Handling the file picking

We have our function ready to update the count and store the files. We will just pass data to this function once the files are selected or changed.

$(document).ready(function(){
   //Handle the file change
   $("input[type='file']").change(function(e){
      //Get the id
      let id = e.target.id;

      //Get the files
      let files = e.target.files;

      //Store the file
      storeFile(id, files);

      //Show the complete icon
      $(this).siblings('.icon').hide();
      $(this).parent().removeClass('drawn');
      setTimeout(() => {
         $(this).parent().addClass('drawn');
      }, 50); 
   });
});

Once the files are selected we will show the complete animation with svg to notify users that files are changed.

Right now we have our file stored and count visible. Lets create the file previewer with bootstrap popover.

//Show file list
$('[data-toggle="popover"]').popover({
    html: true,
    title: "Files",
    placement:"bottom",
    content: function () {
      //Get the id of the file picker
      let id = $(this).attr('data-id');
                
      //Get all the files of this filepicker
      let items = fileStorage[id];

      //Preview the file 
      let template = '<div class="row">';
      if(items && items.length){
        for(let val of items){
           template += "<div class='col-12 pb10'><span class='popover-content-file-name'>" + val.name + "</span><span class='popover-content-remove' data-target='" + id + "' data-name='" + val.name + "' data-type='upload'><i class='fas fa-trash'></i></span></div>"
         }
       }else{
         template += "<div class='col-12 pb10'><span class='popover-content'>No file found</span></div>";
       }

       template += '</div>';
       return template;
     }
});

Bootstrap provides us a method to dynamically generate the content of the popover. So we attach the popover to [data-toggle="popover"]. Learn more about it here.

How it works

  • Every time a popup is about to render it will use it’s [data-target] id and pull all the files from the fileStorage.
  • If there are files then render those files along with the delete button.
  • If there is no file then show some message.

Now in case, you have multiple file uploader and you want only one popover to be open at a time when you add the following code.

 //Prevent multiple popover
 $('body').on('click', function (e) {
     $('[data-toggle="popover"],[data-original-title]').each(function () {
         //the 'is' for buttons that trigger popups
         //the 'has' for icons within a button that triggers a popup
         if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.popover').has(e.target).length === 0) {
            (($(this).popover('hide').data('bs.popover') || {}).inState || {}).click = false;  // fix for BS 3.3.6
         }
    });
});

If you select some file and click on view you should be able to view it. Now the last thing we will do is handle the deletion of files.

Deleting the file

We have provided the id of the file picker to the delete button through data-target and name of the file through data-name. Every time delete icon is clicked we will use these values to remove the files.

//Delete files
$(document).on('click', '.popover-content-remove', function (e) {
    //Get the id whose file to delete
    let id = $(this).attr('data-target');

    //Get the name of the file to delete
    let name = $(this).attr('data-name');

    //Confirm delete
    let isDelete = confirm("Do you really want to delete this file?");  

    //If confirmed
    if (isDelete) {
       //Remove the requested file
       let files = Object.values(fileStorage[id]);
       let newArr = files.filter((e) => { return e.name !== name; });

       //Update the list
       storeFile(id, newArr);

        //If there is no file then show No file
        if(newArr.length === 0){
              $(this).parent().parent().append("<div class='col-12 pb10'><span class='popover-content'>No file</span></div>");
         }

         //Remove the current file
         $(this).parent().remove();
      }
});

As we are dynamically generating the content of the popover and its does not already exist in the DOM. We cannot assign a event to it. So we have to use a work around of assigning event on the DOM and checking if delete icon is clicked with $(document).on('click', '.popover-content-remove', function (e) {});.

How it works

  • Once the delete icon is clicked we will ask for a confirmation from the user.
  • If user wants to proceed then we are fetching the id and the name assigned to delete button through data-target and data-name.
  • We are removing that particular file using filter() method.
  • Once the file is removed from the array then we are updating its count by passing the value to our helper function storeFile(id, newArr);.
  • Also we remove the element from the popover. If the array is empty then show some message.

Note: You should provide a unique id to each file picker and its popover previewer.


Complete code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Custom File picker</title>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
    
    <!-- Font Awesome -->
    <script defer src="https://use.fontawesome.com/releases/v5.7.2/js/all.js" integrity="sha384-0pzryjIRos8mFBWMzSSZApWtPl/5++eIfzYmTgBBmXYdhvxPc+XcFEk+zJwDgWbP" crossorigin="anonymous"></script>

    <!-- jQuery library -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

    <!-- Popper JS -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>

    <!-- Latest compiled JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>

    <style>
.picture-container{
    position: relative;
    cursor: pointer;
    text-align: center;
    margin-bottom: 15px;
}

.picture{
    width: 100px;
    height: 100px;
    background-color: #fff;
    color: #FFFFFF;
    margin: 5px auto;
    overflow: hidden;
    -webkit-transition: all 0.2s;
    -moz-transition: all 0.2s;
    -o-transition: all 0.2s;
    transition: all 0.2s;
}

.picture > .icon{
    width: 100%;
    height: 100%;
    display: inline-block;
    color: #37474F;
    border: 4px solid #CCCCCC;
    -webkit-border-radius: 50%;
    -moz-border-radius: 50%;
    -o-border-radius: 50%;
    border-radius: 50%;
    -webkit-transition: all 0.2s;
    -moz-transition: all 0.2s;
    -o-transition: all 0.2s;
    transition: all 0.2s;
}

.picture:hover .icon{
    border-color: greenyellow;
}

.picture > .icon > svg{
    height: 1.4em;
    font-size: 4em;
}

.picture > svg{
    width: 100%;
    height: 100%;
}

.picture input[type="file"]{
    cursor: pointer;
    display: block;
    height: 100%;
    left: 0;
    opacity: 0 !important;
    position: absolute;
    top: 0;
    width: 100%;
    z-index: 100;
    right: 0;
    bottom: 0;
}

/*svg tick animation*/
.circ {
    opacity: 0;
    display: none;
    stroke-dasharray: 130;
    stroke-dashoffset: 130;
    -webkit-transition: all .75s;
    -moz-transition: all .75s;
    -ms-transition: all .75s;
    -o-transition: all .75s;
    transition: all .75s;
}
.tick{
    stroke-dasharray: 50;
    stroke-dashoffset: 50;
    -webkit-transition: stroke-dashoffset .4s 0.5s ease-out;
    -moz-transition: stroke-dashoffset .4s 0.5s ease-out;
    -ms-transition: stroke-dashoffset .4s 0.5s ease-out;
    -o-transition: stroke-dashoffset .4s 0.5s ease-out;
    transition: stroke-dashoffset .4s 0.5s ease-out;
}
.drawn > svg .path{
    display: block;
    opacity: 1;
    stroke-dashoffset: 0;
}

.drawn{
    border-color: #fff;
}

[data-toggle="popover"]{
    cursor: pointer;
}

span.popover-content-remove {
    padding-left: 10px;
    color: red;
    cursor: pointer;
    float: right;
}

.pb10{
    padding-bottom: 10px;
}

.popover-header{
    text-align: center;
}

.popover{
    min-width: 200px;
}
    </style>
</head>
<body> 
<div class="custom-file-picker"> 
    <div class="picture-container form-group">
        <h4 class="info_text">Upload Proof of Physical Address</h4>
        <div class="picture">
            <span class="icon"><i class="fas fa-file-upload"></i></span>
            <input type="file" class="wizard-file" multiple id="a8755cf0-f4d1-6376-ee21-a6defd1e7c08">
            <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 37 37" xml:space="preserve">
                <path class="circ path" style="fill:none;stroke:#77d27b;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:10;" d="M30.5,6.5L30.5,6.5c6.6,6.6,6.6,17.4,0,24l0,0c-6.6,6.6-17.4,6.6-24,0l0,0c-6.6-6.6-6.6-17.4,0-24l0,0C13.1-0.2,23.9-0.2,30.5,6.5z"></path>
                <polyline class="tick path" style="fill:none;stroke:#77d27b;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:10;" points="11.6,20 15.9,24.2 26.4,13.8 "></polyline>
            </svg>
        </div>     
    </div>
    <div class="popover-container text-center">
        <p data-toggle="popover" data-id="a8755cf0-f4d1-6376-ee21-a6defd1e7c08" class="btn-popover" data-original-title="" title="">
            <span class="file-total-viewer">0</span> Files Selected <input type="button" value="view" href="javascript:void(0)" class="btn btn-success btn-xs btn-file-view">
        </p>
    </div>
</div>
</body>
<script>
    //Global object to store the files
    let fileStorage = {};

    $(document).ready(function(){
        $("input[type='file']").change(function(e){
           //Get the id
           let id = e.target.id;

           //Get the files
           let files = e.target.files;

           //Store the file
           storeFile(id, files);

           //Show the complete icon
           $(this).siblings('.icon').hide();
           $(this).parent().removeClass('drawn');
           setTimeout(() => {
               $(this).parent().addClass('drawn');
           }, 50); 
        });
    

    //Store the file for particular filepicker
    let storeFile = (id, files) => {
        fileStorage[id] = files;

        //Update the file count
        $(`[data-id="${id}"] > .file-total-viewer`).text(files.length);
    }

    //Show file list
    $('[data-toggle="popover"]').popover({
            html: true,
            title: "Files",
            content: function () {
                //Get the id of the file picker
                let id = $(this).attr('data-id');
                
                //Get all the files of this filepicker
                let items = fileStorage[id];

                //Preview the file 
                let template = '<div class="row">';
                if(items && items.length){
                    for(let val of items){
                        template += "<div class='col-12 pb10'><span class='popover-content-file-name'>" + val.name + "</span><span class='popover-content-remove' data-target='" + id + "' data-name='" + val.name + "' data-type='upload'><i class='fas fa-trash'></i></span></div>"
                    }
                }else{
                    template += "<div class='col-12 pb10'><span class='popover-content'>No file found</span></div>";
                }

                template += '</div>';
                return template;
            },
            placement:"bottom"
    });

    //Prevent multiple popover
    $('body').on('click', function (e) {
        $('[data-toggle="popover"],[data-original-title]').each(function () {
            //the 'is' for buttons that trigger popups
            //the 'has' for icons within a button that triggers a popup
            if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.popover').has(e.target).length === 0) {
                (($(this).popover('hide').data('bs.popover') || {}).inState || {}).click = false;  // fix for BS 3.3.6
            }
        });
    });

    //Delete files
    $(document).on('click', '.popover-content-remove', function (e) {
        //Get the id whose file to delete
        let id = $(this).attr('data-target');

        //Get the name of the file to delete
        let name = $(this).attr('data-name');

        //Confirm delete
        let isDelete = confirm("Do you really want to delete this file?");  

        //If confirmed
        if (isDelete) {
            //Remove the requested file
            let files = Object.values(fileStorage[id]);
            let newArr = files.filter((e) => { return e.name !== name; });

            //Update the list
            storeFile(id, newArr);

            //If there is no file then show No file
            if(newArr.length === 0){
              $(this).parent().parent().append("<div class='col-12 pb10'><span class='popover-content'>No file</span></div>");
            }

            //Remove the current file
            $(this).parent().remove();
            
        }
    });
});
</script>
</html>

Leave a Reply

Your email address will not be published. Required fields are marked *