Explore a practical example of how to allow users to upload files to an Angular app, with steps like allowing multiple files, restricting file type, showing the successful uploads and adding a drag-and-drop area.
Web applications do more than just show or edit data; sometimes, we need to put files, images or PDFs onto the web. You’ve probably seen web forms where you can pick files from your computer. We’re going to use that feature and a bit of code to let users upload files to our Angular app.
At first, it might seem like we can do everything with the file picker in HTML. To find out how easy it is and if it’s the best way to solve real problems, let’s try it out with a practical example. We’ll see what we can do and when it might be better to use a different approach that won’t slow down development or make things harder for users.
Imagine a situation where a company wants to make a web page for people to upload photos from their trip to Menorca. We want to keep it simple, so here’s what we need:
Now, let’s get started!
In your terminal, run the following command to create a new Angular project named upload-menorca-photos:
ng new upload-menorca-photos
? Which stylesheet format would you like to use? CSS
? Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? No
cd upload-menorca-photos
Go to the upload-menorca-photos
folder, edit the app.component.html, and remove the default HTML to be ready for our next step.
Now that our Angular project is ready, the next step is to simulate an API call. This will help us mimic real-world behavior when dealing with file uploads and server interactions, without actually connecting to a live server.
Because it is an MVP (minimum viable product), we want to create a mock service that simulates the image upload process. We’ll use this service to generate fake image URLs after a simulated delay.
Create a new service called fake-image-upload
using the Angular/CLI in the src/app directory by running the following command
ng g s services/fake-image-upload
CREATE src/app/services/fake-image-upload.service.spec.ts (404 bytes)
CREATE src/app/services/fake-image-upload.service.ts (144 bytes)
We won’t delve deeply into how services and RxJS work. Instead, we will create a method called uploadImage with the parameter image of type File. This method returns an observable of type string, simulating a fake API image response with a delay of three seconds.
The final code looks like:
import { Injectable } from '@angular/core';
import { Observable, delay, of, timeout } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class FakeImageUploadService {
uploadImage(image: File): Observable<string> {
console.log(`we are uploading fake upload ${image.name}`);
return of('https://random.imagecdn.app/500/150').pipe(delay(3000));
}
}
Learn more about RxJS and Angular services.
We need to provide a component with an <input type="file">
to upload the image. It will use the fake service. Let’s start by generating a new component named image-upload
using the Angular CLI:
ng g c components/image-upload
CREATE src/app/components/image-upload/image-upload.component.html (27 bytes)
CREATE src/app/components/image-upload/image-upload.component.spec.ts (595 bytes)
CREATE src/app/components/image-upload/image-upload.component.ts (225 bytes)
CREATE src/app/components/image-upload/image-upload.component.css (0 bytes)
UPDATE src/app/app.module.ts (429 bytes)
Open the image-upload.component.ts file, inject the FakeImageUploadService
by using the inject function. Add a new method upload()
. This method will call the fake method from the service.
import { FakeImageUploadService } from './../../services/fake-image-upload.service';
import { Component, inject } from '@angular/core';
@Component({
selector: 'app-image-upload',
standalone: true,
imports: [],
templateUrl: './image-upload.component.html',
styleUrl: './image-upload.component.scss',
})
export class ImageUploadComponent {
fakeImageUploadService = inject(FakeImageUploadService);
}
However, the uploadImage
method expects a file. How do we know when the user has selected an image? To solve this, add a new property called selectedImages
of type FileList to store the selected image or images. Then, listen for the (change)
event and link it to a new method onImageSelected
.
In the onImageSelected
method, we will validate whether the input has a file selected and set it as the first item in selectedImages
.
The final code in the image-upload.component.ts file should look like this:
import { Component, inject } from '@angular/core';
import { FakeImageUploadService } from './../../services/fake-image-upload.service';
@Component({
selector: 'app-image-upload',
standalone: true,
imports: [],
templateUrl: './image-upload.component.html',
styleUrls: ['./image-upload.component.css'],
})
export class ImageUploadComponent {
fakeImageUploadService = inject(FakeImageUploadService);
selectedImages!: FileList;
onImageSelected(event: Event): void {
const inputElement = event.target as HTMLInputElement;
if (inputElement?.files && inputElement.files.length > 0) {
this.selectedImages = inputElement.files;
}
}
upload(): void {
this.fakeImageUploadService.uploadImage(this.selectedImages[0]);
}
}
Next, open and add the input and the button, and bind them to the image-upload.component.html markup. Bind the method and listen to the events. The final markup should look like:
<input type="file" (change)="onImageSelected($event)">
<button (click)="upload()">Upload Image</button>
Learn more about Input File.
Before using the ImageUploadComponent
, in the app.component, we must import the component in the imports sections.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { ImageUploadComponent } from './components/image-upload/image-upload.component';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
imports: [CommonModule, RouterOutlet, ImageUploadComponent],
})
export class AppComponent {
title = 'upload-menorca-photos';
}
Add the ImageUploadComponent
to the app.component.html
using the selector <app-image-upload>
:
<div>
<h1>Welcome to Image Upload App</h1>
</div>
<app-image-upload/>
Save your changes and run ng serve
to launch the app.
Yes, we now have a very basic upload function calling the fake service!
But is this MVP good enough? Nah, we have a lot of work ahead because we are professionals and must deliver our very best.
This is just the beginning of our adventure. We have some tasks to cover for the MVP. Let’s address them one by one.
By default, the file upload feature allows selecting only a single file. However, by adding the multiple boolean attribute, the file input allows users to select multiple files.
To implement this, open the image-upload.component.html file, add the multiple
attribute to the input element, save your changes and then reopen the app. The input will now permit the selection of multiple files.
<div class="upload-area">
<input type="file" multiple="true" (change)="onImageSelected($event)">
<button (click)="uploadImage()">Upload Image</button>
</div>
Is it as easy as it sounds? Not quite. To enable multiple file selection, we need to take a few additional steps beyond simply altering the HTML input.
We must make some adjustments to our code to facilitate multiple uploads, and these changes will also set the stage for implementing the drag-and-drop area seamlessly.
Let’s refactor our code. Open the generated image-upload.component.ts, and we’re going to make a few changes:
uploadFiles
method. It takes a parameter images
of type FileList. Use a for loop to iterate over the list of selected images and call the upload
function for each image, then print the response using console.log()
.upload
. Before uploading, validate if selectedImages
are available.The final code will look like this:
import { Component } from '@angular/core';
import { FakeImageUploadService } from './../../services/fake-image-upload.service';
@Component({
standalone: true,
selector: 'app-image-upload',
templateUrl: './image-upload.component.html',
styleUrls: ['./image-upload.component.css']
})
export class ImageUploadComponent {
selectedImages!: FileList;
fakeImageUploadService = inject(FakeImageUploadService);
onImageSelected(event: Event): void {
const inputElement = event.target as HTMLInputElement;
if (inputElement?.files && inputElement.files.length > 0) {
this.selectedImages = inputElement.files;
}
}
upload(): void {
if (this.selectedImages) {
this.uploadFiles(this.selectedImages);
}
}
private uploadFiles(images: FileList): void {
for (let index = 0; index < images.length; index++) {
const element = images[index];
this.fakeImageUploadService.uploadImage(element).subscribe((p) => {
console.log(p)
});
}
}
}
Save the changes, and then select multiple files (including a video) and check in the console to see the names of the files.
The next challenge is to disallow videos. We can achieve this by using the accept attribute, which accepts one or more file type specifiers as its value. Open the image-upload.component.html file and add the accept attribute with the value image/*
.
When the user clicks on the file explorer, it will only display images, helping to prevent user mistakes when uploading.
<input type="file" multiple="true" (change)="onImageSelected($event)" accept="image/*">
<button (click)="upload()">Upload Image</button>
Save the changes and reload the page to see the following result.
Perfect! Let’s continue with the next feature!
We need to display the images from the service response. To achieve this, we need to store each response in an array and iterate over it. Follow these steps in image-upload.component.ts:
imagesUrl: Array<string> = [];
uploadFiles
method: In the subscribe block, push the response to the imagesUrl array. private uploadFiles(images: FileList) {
for (let index = 0; index < images.length; index++) {
const element = images[index];
this.fakeImageUploadService.uploadImage(element)
.subscribe((p) => {
//push the response
this.imagesUrl.push(p)
});
}
}
Open the image-upload.component.html file and using @for to iterate over the imagesUrl
array.
<div class="images">
@for (image of imagesUrl; track image) {
<img [src]="image" alt="Uploaded Image">
}
</div>
Save the changes and you’ll see the fake image responses displayed once we get the fake response.
The final stage is to create the drag-and-drop area with automatic upload. To achieve this, we need to take several steps in the image-upload.component.ts file.
uploading
to enable a CSS animation during the loading phase.export class ImageUploadComponent { fakeImageUploadService = inject(FakeImageUploadService);
selectedImage!: FileList;
imagesUrl: Array<string> = [];
uploading = false;
..
uploading
property to true
when the upload starts and then switch it back to false
in the uploadFiles
method. private uploadFiles(images: FileList) {
this.uploading = true;
for (let index = 0; index < images.length; index++) {
const element = images[index];
this.fakeImageUploadService.uploadImage(element).subscribe((p) => {
this.imagesUrl.push(p);
this.uploading = false;
});
}
}
onDrop
event in the drag-and-drop area and also listen for the dragOver
event to prevent the browser from opening the file. onDrop(event: DragEvent): void {
event.preventDefault();
if (event?.dataTransfer?.files) {
this.uploadFiles(event.dataTransfer.files);
}
}
onDragOver(event: DragEvent): void {
event.preventDefault();
}
Next, add some CSS styles for the loading animation and the drag-and-drop area. Open the image-upload.component.css file and replace its current content with the following code:
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background-color: #f7f7f7;
}
.drag-drop-area {
border: 2px dashed #ccc;
padding: 40px;
text-align: center;
cursor: pointer;
}
.upload-area {
padding: 1rem;
}
.uploading {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
}
.uploading::after {
content: "";
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-top-color: #333;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.drag-drop-area p {
margin: 0;
}
.drag-drop-area:hover {
background-color: #f2f2f2;
}
.images {
padding-top: 2rem;
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.images > img {
border-radius: 0.5rem;
max-width: 100%;
height: auto;
}
The final stage is to create the drag-and-drop area with an automatic upload in the image-upload.component.html file. Listen for the drop
and dragover
events and link them to the methods created earlier.
<div>
with the class drag-drop-area and listen for two events: drop
and dragover
.uploading
class, which will display a loading indicator when the upload starts.<div class="drag-drop-area" [class.uploading]="uploading" (drop)="onDrop($event)" (dragover)="onDragOver($event)">
<p>Drag and drop to automatic upload</p>
</div>
Save the changes, open your web browser and navigate to http://localhost:4200. You will now see the Image Upload App with the drag-and-drop area and file input for image uploads.
Code: https://github.com/danywalls/upload-menorca-photos.
We have successfully created a basic version of an uploader and guided you through the process of building an Angular application capable of uploading multiple images. Throughout this tutorial, we have covered essential features, including drag-and-drop functionality, image validation and displaying successful uploads.
It’s important to recognize the challenges and limitations we encountered during this process, particularly with regard to user experience and the complexity of development.
User experience: While our image uploader covers the fundamentals, it may not provide the most intuitive and user-friendly experience. Users often expect additional features such as image previews, error handling and the ability to remove uploaded images. Enhancing these aspects can greatly improve the overall user experience.
Complexity of code: Building a feature-rich image uploader can be quite complex, especially when incorporating features like drag-and-drop support, file validation and progress tracking—the last of which we didn’t even get to in our MVP. This complexity can make the code more challenging to maintain and extend over time.
Scalability: Our current implementation does not address scenarios where users may need to upload a large number of images. Effectively handling large file uploads without affecting performance is a worthwhile challenge to explore.
In our next article, we will address these challenges and improve the image uploading experience using the Kendo UI for Angular Upload component. We will explore how it simplifies development, enhances user experience and can save time for you. Read it next: Building an Advanced Image Uploader with Kendo UI for Angular Upload.
Dany Paredes is a Google Developer Expert on Angular and Progress Champion. He loves sharing content and writing articles about Angular, TypeScript and testing on his blog and on Twitter (@danywalls).