Submitting drag and drop files and showing upload progress

Constantin Druc ยท 27 Jun, 2021

[00:00] As a starting point we have most of the HTML in place. The only dynamic part is here where we listen to the dragover and dragleave events to toggle this dragging property, and then we use this property to switch between indigo and grey borders. So if I take a file, and hover it, the border color changes. To grab the files when they are dropped, we can set up a listener for the drop event, however when you do this, make sure you add the prevent modifier for all three events, otherwise the files will be opened in the browser.

[00:43] Now, let's add this on dropped files method, and we'll receive an event, and to get the dropped items as an array we can do let files = event.dataTransfer.items. Let's console log this and see what we get.

[01:07] If I open the console and drop some files, we'll see that we get an array of data transfer items. However, one problem is that this also works when I drag a link, for example. So we need to somehow filter the dropped items to only get file types, and we can do that using filter, item, and then only return items where kind equals file. And now if we drop the link we get an empty array; if we drop files we get the files.

[01:47] Another problem is, in order to make this work we need actual file objects not data transfer items. Luckily we can transform data transfer items into file objects. We can do, map item item get as file, and now, if I refresh and drop some files, we see we get an array of file objects - which is what we needed.

[02:16] One thing you may have noticed is that the border didn't switch back to gray, so we need to do this.dragging = false, so when the files are dropped, dragging is set to false; which will turn the border gray again. Let me refresh and here it is.

[02:43] Now that we have the files, it would be nice to display them as a list under the form and see their upload progress. To do that, we need to define a data property that will hold the files and then push the files objects into it. So here we can do: files for each, and here I want to push the file at the start of the array, so I'll use this media dot shift, and because we need to add a bit more data we'll need to push a new object, and progress zero.

[03:23] We can now go in our template and loop through the media items. Let's close this up, and do ul li, v-for, item index in media - we'll set the index as the key and do div, item.file.name to get the name, and we'll do the same for the progress.

[03:56] If I go into browser, refresh, and add the files, here they are. Let's style it a bit, we'll give it some margin, set the background to white, round the corners, and divide the items with the border - and let's add some shadow.

[04:18] And for the list items we can do p-3 to give it some padding, flex items-center to vertically align the items, and justify-between to push them to the edges. Now for the file name we can do class text-sm text-gray-700, and for the upload progress, it would be nice to have something like a loader that goes from 0 to 100. To do that we'll need a parent div that will represent the whole bar and the child div that will represent the loaded part.

[05:00] Here I will paste in the classes and explain how they work just to save some time and not bore you to death. Ok, so we have a parent div that holds everything together - this represents the gray bar. Its position is set to relative so it can hold this absolutive right here that represents the loaded part of the bar. Here we'll need to adjust the width, and we can simulate that by setting the style width 40, for example, and here it is - the bar is loaded 40.

[05:39] Finally, we have the percentage text right here which needs to be relative and have a greater z-index so it shows up in front of the loaded bar. So if we set this to 70, the text appears in front of the loaded bar. Without this it will be hidden.

[06:04] Moving on, let's register a new endpoint where we can post the files. It won't actually store them, we'll be using it as a placeholder. Do /media and an empty function. Let's name it media.store, and now we can go back to our component and use axios to make a post request with each file.

[06:36] We'll do this media for each media, and then axios post this route media.store and we'll need to send form data. So let's create it, and append the file, which will be media.file and then add it here. Now, to watch the upload progress we can do on upload progress and we'll receive an event - and then we can set media.progress equals to math round because we want a round number, and we'll do event loaded times 100 divided by event total - and this will give us the percentage of the upload progress.

[07:29] Let's test it out. I'll go inside the template and replace this hard-coded value with the actual progress, and now if we refresh, open the inspector to slow down the network and drop the files, here they are.

[08:01] Next up, let's do some error handling so if we get any error we'll do catch, and if we get an error, we'll do media dot error and say "upload fail, please try again later". But if we receive a 422 response, it's a validation error and we need to set that instead. So we'll do if error has a response status equals 422 media.error equals error.response.data.errors.file[0] to get the first error. And of course we need to add error to our media object, and the initial value will be null. And then, display it in our template.

[09:03] We'll do div and v-if="item.erro" display there. Let's style this a bit, and since we are here we can also tell the progress bar to only be visible if the item progress is less than 100 and we don't have any errors.

[09:22] To test it out I'll go in the browser and set the network to offline and drop some files. And we receive the first kind of error, so "upload fail please try again later", and to receive a validation error, we need to go to web.php and add some validation rules. Let's say validate file and we'll set the max to one kilobyte, so of course it will fail. Let me bring this back up, refresh and upload, here it is: "the file must not be greater than one kilobytes".

[10:01] One thing we missed is that whenever we drop files we push them into this media array and then we loop through the array and send a post request. However, that also means that if I drop files twice, all the items in the media array will be re-uploaded. Here's what I mean. If I refresh this and clear out the requests, I drop two files, two requests. If I drop another two files, four requests are made and so on.

[10:34] So we need to filter the media items and to only submit those that haven't been uploaded. And we can do that by adding a new property called uploaded, and set it to false and then we'll filter the media items to only submit the ones that haven't been uploaded. So we'll do filter media and return media dot uploaded, and now if the post request is successful we need to set media uploaded to true.

[11:07] So we'll do then media dot uploaded equals true, and now if I go back to the web routes we'll need to remove this so the request can be successful. And refresh this, clear the requests, two more files so the requests are no longer duplicated.

[11:45] The last thing we need to do is to make this select files button work. To do that I'll go and find the input, and it should be here, and say on input change, trigger the on selected files method. I'll go down and add this new method, and to get the files we can do let files = event.target.files, and now instead of duplicating this whole part right here, we can extract the part that deals with the processing and uploading the files into a new method.

[12:38] So what we will do is grab this part and this part, and call this upload files method that will receive the files, and let's define the method here, and paste everything in. And basically, we'll need to make the same call on selected files. And now if we refresh the browser, click select files, select the two files, and they are uploaded. However, if we try and select the same two files again they won't be uploaded because we also need to clear the input after we upload the files. So right here, we need to do this, and to get a reference of the input we need to add ref; right here we can do ref files and let's also name these files, and here we can do this refs files value equals null and if we try this again, so refresh, select two files, upload, select them again, upload. So that works.

[14:07] Finally, let's add an edit link for the uploaded files and check everything again. I'll go here and say inertia link ref edit, and we'll style it a bit underline, and it will only be visible if the file has been uploaded.

[14:34] Now if I go into browser, refresh, slow down the internet, and pick up some files, well... it's working, but this edit link takes a while to appear. that's because the item progress reaches 100 before we even receive a response back. So let's replace this with not item dot uploaded and try again. I'll bring back the internet, refresh, slow it down, pick some files, and here it is - it now waits a bit for the response to get back and then it shows the edit link.