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

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:

  1. Run the following command to create a new Angular project:
    ng new imagekit-angular-app
  2. Navigate to the newly created project directory:
    cd imagekit-angular-app/
  3. Install libraries (if not already):
    npm install
  4. 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:

https://github.com/imagekit-samples/tutorials/blob/angular-file-upload/src/app/upload-form/upload-form.component.css

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, and uploadStatus 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 input change event, the file is obtained as HTMLInputElement.files, which is essentially a fileList. We select a single file by accessing event.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 a drop event. It then passes the event to the onFileSelected method.
  • handleDragOver function is invoked when the dragover event occurs. It's essential to execute preventDefault() and stopPropagation() during dragover to ensure the proper functionality of the drop 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 the uploads 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.