In this blog post, we'll learn how to add file-uploading functionality to an Angular application. We'll break down the process into simple steps and create a customized user interface for uploading files. This interface will include features like drag-and-drop, a progress bar to track file upload progress and validation checks.
Additionally, we'll set up a backend service using Node.js. This service will provide a POST API endpoint that can handle file upload. For simplicity, we'll store the uploaded files in a local directory. You can see the working app here and the whole application is hosted on the GitHub repository at https://github.com/imagekit-samples/tutorials/tree/angular-file-upload.
The final results will look like this:
This guide walks you through the following topics:
- Setting up an angular application
- Creating a customized file upload interface
- Setting up the backend
- Implementing a progress bar
- Adding basic validation
Setting up an angular application
In this tutorial, we'll be using:
- Node version 20.
- Angular version 17.
We'll start by creating a new project using Angular's ng new <project name>
command-line tool.
Here are the steps:
- Run the following command to create a new Angular project:
ng new imagekit-angular-app
- Navigate to the newly created project directory:
cd imagekit-angular-app/
- Install libraries (if not already):
npm install
- Start the application by running:
npm start
Open your web browser and go to http://localhost:4200/ to see the dummy app created by Angular CLI.
For simplicity, let's remove everything from src/app/app.component.html
so we can start fresh. Next, let's begin by adding the basic <input/>
element to pick files. You can use the following code in src/app/app.component.html
:
<h1>Angular image & video upload</h1>
<input type="file" name="file" />
As we can see above this upload button is very simple, it just opens a file picker dialog. We will instead use a custom file picker component and hide the default file picker input.
Custom file upload interface
Let's make a new component called upload-form
using the ng generate
command provided by the Angular CLI.
ng generate component upload-form
Now, let's make changes to our upload-form
component. First, open the upload-form/upload-form.component.html
file and insert the following code:
<form class="upload-form">
<h1>Upload File</h1>
<label
for="file"
(dragover)="handleDragOver($event)"
(drop)="handleDrop($event)"
>
<i class="ph ph-upload"></i>
<span>
Drag & drop or
<span>browse</span>
your files
</span>
</label>
<input
id="file"
type="file"
name="file"
(change)="onFileSelected($event)"
/>
<div class="result" [style.display]="outputBoxVisible ? 'flex' : 'none'">
<i class="ph ph-file"></i>
<div class="file-details">
<span class="file-name">{{ fileName }}</span>
<ng-container *ngIf="uploadStatus === 200 || uploadStatus === undefined">
<span class="file-size">{{ fileSize }}</span>
</ng-container>
</div>
<div class="upload-result" [style.display]="uploadStatus ? 'flex' : 'none'">
<span>{{ uploadResult }}</span>
<ng-container *ngIf="uploadStatus === 200; else error">
<i class="ph ph-check-circle"></i>
</ng-container>
<ng-template #error>
<i class="ph ph-x-circle"></i>
</ng-template>
</div>
</div>
</form>
Next, we'll give our component some style by adding CSS code to the upload-form/upload-form.component.css
file. You can find the CSS code in this GitHub repository:
To integrate icons, insert the provided script tag into the index.html
file. We're utilizing phosphor-icons for this purpose.
<script src="https://unpkg.com/@phosphor-icons/web"></script>
Now, let's include our upload-form
component in our src/app/app.component.html
file. Use the following code:
<h1>Angular image & video upload</h1>
<app-upload-form />
We've built our UI, but it won't function properly until we create necessary functions like onFileSelected
and handleDrop
, which we used earlier in our UI. Now, let's proceed to update our upload-form/upload-form.component.ts
file.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-upload-form',
standalone: true,
imports: [CommonModule],
templateUrl: './upload-form.component.html',
styleUrl: './upload-form.component.css',
})
export class UploadFormComponent {
outputBoxVisible = false;
progress = `0%`;
uploadResult = '';
fileName = '';
fileSize = '';
uploadStatus: number | undefined;
constructor() {}
onFileSelected(event: any) {
this.outputBoxVisible = false;
this.progress = `0%`;
this.uploadResult = '';
this.fileName = '';
this.fileSize = '';
this.uploadStatus = undefined;
const file: File = event.dataTransfer?.files[0] || event.target?.files[0];
if (file) {
this.fileName = file.name;
this.fileSize = `${(file.size / 1024).toFixed(2)} KB`;
this.outputBoxVisible = true;
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload', true);
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
this.uploadResult = 'Uploaded';
} else if (xhr.status === 400) {
this.uploadResult = JSON.parse(xhr.response)!.message;
} else {
this.uploadResult = 'File upload failed!';
}
this.uploadStatus = xhr.status;
}
};
xhr.send(formData);
}
}
handleDragOver(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
}
handleDrop(event: DragEvent) {
event.preventDefault();
if (event.dataTransfer) {
const file: File = event.dataTransfer.files[0];
this.onFileSelected(event, event.dataTransfer.files[0]);
}
}
}
In the upload-form-component.ts
file, we're doing the following tasks:
- We've created variables like
outputBoxVisible
,progress
,uploadResult
,fileName
,fileSize
, anduploadStatus
to save the various file information and manage different UI states. - We obtain files using a file picker through the change event or directly through the drop event when a file is dropped into the file input UI. In both cases, we utilize the
onFileSelected
method, which accepts an event as input. - The
onFileSelected
method retrieves the file from the event based on its type. When triggered by the inputchange
event, the file is obtained asHTMLInputElement.files
, which is essentially a fileList. We select a single file by accessingevent.target.files[0]
. - In the case of a drop event, the file is obtained from the dataTransfer property of the event, which contains a list of files. We access a single file using
event.dataTransfer.files[0]
. - This method extracts the file name and size, appends the file as form data, and then initiates an
XMLHttpRequest
to/api/upload
. - After sending the request, it updates the result based on the status received from the API.
handleDrop
function is invoked when a file is dragged and dropped into the file input container, triggering adrop
event. It then passes the event to theonFileSelected
method.handleDragOver
function is invoked when thedragover
event occurs. It's essential to executepreventDefault()
andstopPropagation()
duringdragover
to ensure the proper functionality of thedrop
operation.
Here's how our application will look now. If we attempt to select a file, it won't work because we haven't set up a backend service for it yet. Let's create a backend service to provide an endpoint at http://localhost:4000/upload
.
Setting up the backend
Now we will create a node.js application to accept a file and store it in a local disk. Run the below commands in the terminal to create a node application.
mkdir server
cd server
npm init -y
npm install express multer cors
Let’s create a server.js
file and add the below code to it.
const express = require("express");
const multer = require("multer");
const path = require("path");
const cors = require("cors");
const app = express();
const PORT = 4000;
app.use(cors());
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "uploads/");
},
filename: function (req, file, cb) {
cb(
null,
file.fieldname + "-" + Date.now() + path.extname(file.originalname)
);
},
});
const upload = multer({
storage: storage
});
app.post("/upload", upload.single("file"), (req, res) => {
if (!req.file) {
return res.status(400).json({ message: "No file uploaded" });
}
res.status(200).json({
message: "File uploaded successfully",
filename: req.file.filename,
});
});
app.use("/uploads", express.static("uploads"));
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
In the server.js
file, we're configuring a Node.js server with Express to manage file uploads, and it's set to listen on port 4000.
- We are using the
cors
middleware to enable Cross-Origin Resource Sharing, which permits the server to specify origins from which browsers can safely load resources. This is necessary because our Angular application is running on http://localhost:4200, while our server is running on http://localhost:4000. Since they are not running on the same origin, the browser would block the requests without CORS headers. - We have defined a POST route ("/upload") where files can be uploaded. It uses a multer to handle a single file upload.
- Multer is used as a middleware to manage
multipart/form-data
. We've configured it to save files in theuploads
directory and to change the file name when uploading.
Let’s create a directory server/uploads
to store files uploaded. To run the backend service run the following command inside the server
directory.
node server.js
Now, when we run our application and choose a file, it should work smoothly. The file will be uploaded successfully, and we can verify this by checking the uploads
directory, where we'll find the stored file.
With the current setup, we have to execute two different commands to run our backend and frontend separately. Instead, we can simplify this by using the concurrently package and setting up a proxy. To install concurrently
, use the following command:
npm i --save-dev concurrently
Now, let's update the scripts
section in the package.json
file.
"scripts": {
"ng": "ng",
"start": "concurrently \"npm run start:server\" \"npm run start:angular\"",
"start:server": "cd server && npm start",
"start:angular": "ng serve --proxy-config proxy.conf.json",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
Now, create a file named proxy.conf.json
and add the following code to it:
{
"/api/**": {
"target": "http://localhost:4000",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/api": ""
}
}
}
Now, update the angular.json
file. Inside the projects/imagekit-angular-app/architect/serve
section, add the following code:
"options": {
"proxyConfig": "./proxy.conf.json"
},
Now, we can start both our Angular app and backend by running the command:
npm start
Progress bar
Now, let's incorporate a progress bar into our component to track the file upload progress. Let's start with a progress bar UI add the below code to upload-form/upload-form.component.html
file.
<div class="file-details">
<span class="file-name">{{ fileName }}</span>
<div class="progress-bar">
<div class="progress" [style.width]="progress"></div>
</div>
<span class="file-size">{{ fileSize }}</span>
</div>
Next, let's capture the upload progress using the xhr.upload.onprogress
function and display it in our UI. Add the following code to upload-form/upload-form.component.ts
file.
xhr.upload.onprogress = (progressEvent) => {
if (progressEvent.lengthComputable) {
const progress = (progressEvent.loaded / progressEvent.total) * 100;
this.progress = `${Math.round(progress)}%`;
}
};
Now our custom upload component would appear like this.
Adding basic validation
Let's implement some simple checks regarding file size and file type on the Node.js backend. We'll update the server.js file to only accept images and videos with a size less than 2MB. We'll restrict the file size using the multer limits property and add a fileFilter
function to validate files based on MIME type.
We will also add an error handling middleware to send errors in the API response received from multer. Use the following code:
const express = require("express");
const multer = require("multer");
const path = require("path");
const cors = require("cors");
const app = express();
const PORT = 4000;
app.use(cors());
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "uploads/");
},
filename: function (req, file, cb) {
cb(
null,
file.fieldname + "-" + Date.now() + path.extname(file.originalname)
);
},
});
const fileFilter = (req, file, cb) => {
// Check if file is an image or video
if (
file.mimetype.startsWith("image/") ||
file.mimetype.startsWith("video/")
) {
cb(null, true);
} else {
cb(new Error("Only image and video files are allowed"));
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: { fileSize: 2000000 },
});
app.post("/upload", upload.single("file"), (req, res) => {
if (!req.file) {
return res.status(400).json({ message: "No file uploaded" });
}
res.status(200).json({
message: "File uploaded successfully",
filename: req.file.filename,
});
});
app.use("/uploads", express.static("uploads"));
// Error handling middleware
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
// Multer error occurred (e.g., file size exceeded)
return res.status(400).json({ message: err.message });
} else if (err) {
// Other errors (e.g., unsupported file type)
return res.status(400).json({ message: err.message });
}
next();
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Below, we can observe our validation in action based on file size and type.
That's it. We have a beautiful file upload component ready.
Conclusion
In this tutorial, we learned how to create a beautiful file upload component in Angular, as well as how to set up an API in Node.js for file uploads using multer.
- You can see the working app here.
- The whole application is hosted on the Github repository at https://github.com/imagekit-samples/tutorials/tree/angular-file-upload