DEV Community

Mayvid14
Mayvid14

Posted on • Edited on

File uploads in Angular 10+ (or JavaScript in general)

Recently, I was working on a side project where I had to upload multiple files. It was a while back that I last worked on file uploads in Angular. So my first instinct was to search for some resources on file uploads in Angular. As expected, I found a lot of them (e.g Angular 10 Multiple Image Upload with Preview Example, How to upload single or multiple files the easy way with FormData). I found these resources very helpful but they did not solve my problem completely, although they helped me reach a solution.

The problem statement

The task had a pretty straight-forward requirement.

  • User should be able to upload multiple images.
  • User should be able to view the images they uploaded.
  • User should be able to delete multiple uploaded images.

So basically, it is CRUD for images (without the U part, so should it be CRD?). For this post, I won't go into details of backend implementation, the read and delete parts, as I had no major issue implementing them. The thing I found interesting was the creation/upload part.

Generic first steps

I must say that the requirement was stated in very simple words, User should be able to upload multiple images. When we hear file uploads, the first thing that comes to mind is the <input type="file" /> tag.

The <input type="file" /> tag

The input tag with file type is used to open a file browser window in which you can select file/files you want to upload. This is the primary requirement for file uploads. This tag can have more attributes to control it's behavior. Following are the attributes that I used.

  • multiple: "true": This lets us upload multiple files at once.
  • accept: "image/*": This restricts the type of files to images only.

So my input tag looked like this:

<input type="file" accept="image/*" multiple="true">
Enter fullscreen mode Exit fullscreen mode

UX perspective

From a user's point of view, whenever I want to upload file/files, I should know what images I have uploaded. A preview of the uploaded images is the best way to achieve this. Luckily, there were abundant resources, and I ended up using the following:

<!-- The html is something like -->
<input type="file" accept="image/*" multiple="true" (change)="onFileChange($event)">
Enter fullscreen mode Exit fullscreen mode
// The component.ts file
onFileChange(event): void {
    const reader = new FileReader();
    reader.onload = (readerEvent: any) => {
        const content = readerEvent.target.result;
        // Do something with the content, use in src of an img
    };
    reader.readAsDataURL(event.target.files[0]);
}
Enter fullscreen mode Exit fullscreen mode

For multiple files, this will be:

for (let i = 0; i < event.target.files.length; i++) {
    const reader = new FileReader();
    const currentFile = event.target.files[i];

    reader.onload = (readerEvent: any) => {
        const content = readerEvent.target.result;
        // Using the content
    };

    reader.readAsDataURL(currentFile)
}
Enter fullscreen mode Exit fullscreen mode

Now what if I don't like one of the images I selected, or what if I wanted to add another image to the queue of images I selected prior? One can say these cases need no attention as the requirement doesn't state these, but from an end-user's perspective, I would want these features as a bare minimum. And once you think about it, you can see that you cannot achieve this by just using the above snippets.

The hurdle

Had I not thought of the UX and cared only about completing the requirements, my task would be over in 1 hour and I would have had more sleep, but alas. Jotting down the points we want to achieve:

  • Have the capability to add one or more files to the upload queue.
  • Have the capability to remove one or more files from the upload queue.

So what's stopping us? The answer is input tag. The input tag can neither add more files in the list of selected files, nor remove them. So how will we achieve the above improvements?

The approach

Let's take baby steps and work out one problem at a time. We will select the first one first.

Add one or more files to the upload queue

As we have seen, the input tag cannot add files to the queue. Let's say I select 2 images, img1 and img2. Now I want to add another image img3 to my queue. If I select the image by clicking on the input tag and selecting my file, I see that now I only have img3. The other two are gone.

This is frustrating because if I had 10+ images, not in any order, and if I wanted to add one more, I had to add all 11 of them again (10 from earlier and 1 that I wish to add to the list).

We can see that the event.target.files which is the FileList cannot keep track of the earlier uploaded files. So why not keep track of them using an array. So I changed my code to store the files in an array.

this.images = [];
...
for (let i = 0; i < event.target.files.length; i++) {
    const reader = new FileReader();
    const currentFile = event.target.files[i];

    reader.onload = (readerEvent: any) => {
        const content = readerEvent.target.result;
        this.images.push({
            id: Date.now(),
            src: content,
            file: currentFile
        });
    };

    reader.readAsDataURL(currentFile)
}
Enter fullscreen mode Exit fullscreen mode

Now my images array has the list of files I upload, along with properties id to uniquely identify them, and src which has their content (used to display the preview by looping through the images array). And now I can just upload more images and they get appended to the images array.

Deleting one or more files

Now that we have the array, we can delete the images by splicing it. Following is my code for Angular, which can be modified to use in vanilla javascript as well.

<!-- image.component.html -->
<div *ngFor='let image of images; let index = i'>
    <img [src]="image.src">
    <i class="remove-button" (click)="removeImage(image.id)"></i>
</div>
Enter fullscreen mode Exit fullscreen mode
// image.component.ts
removeImage(id: number): void {
    const index = this.images.findWhere(image => image.id === id);
    this.images.splice(index, 1);
}
Enter fullscreen mode Exit fullscreen mode

So is it over yet 🙄?

Since we have achieved the desired, is it over? Well, no. An issue with this approach is that if I upload an image, say img1 and then delete it, then we think that it has been deleted, but in reality, it is still present in event.target.files. And so, if I try to upload img1 again, it does nothing, because there is no change in file and so our function does not trigger.

The approach that I used might not be an optimal one, and I welcome any suggestions/feedback on it. The issue is the FileList of the input tag. So after each upload, I just reset it using this.imageForm.reset() where this.imageForm is my Reactive Form. The files are already in the array, and since the input has no files, if I try to remove a file and upload it again, it works.

One may say "What if I upload the same image again without deleting the first one? Wouldn't that make two copies of the same file to be uploaded?" And I don't think that's a problem. Again, if you disagree, I would love to hear your points.

And so I achieved my "Multiple upload, multiple delete" for images. I hope this helps someone who has similar requirements 😁😁.

Top comments (2)

Collapse
 
gyanendrakushwaha profile image
Senty

Hi , I want to select and upload images from a folder, I have tried field but it opens the folder where images are present. I want to first open all images in my modal then upload image by clicking on it.

Collapse
 
mayvid14 profile image
Mayvid14

Hello. Browsers are not meant to read files unless they are uploaded by the user. It would be a security issue if they could do so on their own.

If you are building your app for desktops using something like electron, then you can use node.js to do the same.