Angular 7 Uploads Backed by Node.js

  • 2019-04-09 01:24 AM
  • 258

In this tutorial, you'll learn how to create an app with a backend built on Node.js and Express, and a front-end created with Angular 7.

Web development has become more dynamic with time, mostly due to the continued development of various languages, tools, and frameworks, one of them being Angular. The recent release of Angular 7 comes with new features, such as virtual scrolling, drag and drop, and some CLI updates, among others.

In this article, we will be building an application that shows how file upload works (specifically, image upload). It also uses the Angular Material and its CDK module to show the drag and drop feature introduced with Angular v7.

Below is a screenshot of what we will be building:

Getting Started: Configuring the Development Environment

  • For this tutorial, you can download and install the Angular IDE.
  • However, if you already have an Eclipse installation you are happy with, you can add Angular IDE to it from the Eclipse marketplace.
  • If you already have CodeMix installed, simply ensure you have the Angular extension Pack installed from the Extension Manager at Help > CodeMix Extensions.

Creating an Angular Project Using Angular IDE

We will create our application using the Angular IDE project wizard. We will be using Angular CLI version 7.3.6 and the latest version of all the tech libraries and stacks, as at the time of this writing. To create a new Angular project, navigate to File>New>Angular Project.

Angular application

The next step is to add the Angular Material module that also adds the CDK module (which contains the drag and drop feature in Angular v7). Note that this also optionally adds the animation module. We will include the Angular Material module by running the command below in Terminal+:

ng add @angular/material

At this stage, we will move on to pulling in some dependencies that we will use to build the application with the command below:

npm install --save express cors multer mkdirp 
  • Express is a Node.js module that simplifies the creation of a node server.
  • Cors is a Node.js module that provides a middleware to handle cross-origin resource sharing.
  • Multer is a Node.js middleware for handling “multipart/form-data,” which is primarily used for uploading files.
  • Mkdirp is a Node.js module for directory creation.

Setting Up the Backend Server

Now, we can begin the development of the application. First, we will create a server.js file in the root directory of our application. This file will contain the server setup, multer configuration, and the only route of the application. The route will accept the files submitted, save them, and return a path to the files.

const express = require('express');
const multer = require('multer');
const cors = require('cors');
const mkdirp = require('mkdirp');
const app = express();
const PORT = 5000;
const URL = `http://localhost:${PORT}/`;
app.use(express.static('public'))
var storage = multer.diskStorage({
    destination: (req, file, cb) => {
        const dir = './public/images/uploads';
        mkdirp(dir, err => cb(err, dir))
    },
    filename: (req, file, cb) => {
        cb(null, Date.now() + '-' + file.originalname)
    }
});
const upload = multer({ storage })
app.use(cors());
app.post('/upload', upload.single('image'), (req, res) => {
    if (req.file) {
        res.json({imageUrl: `${URL}images/uploads/${req.file.filename}`});
    }
    else{
        res.status("409").json("No Files to Upload.");
    } 
});
app.listen(PORT);
console.log('api runnging on port: ' + PORT);

In the code snippet above, we set up Express to load files in the public directory in the root of the project as static or public files. This allows the files to be rendered through requests to the file path from the root URL. For example, for a file image.jpg in the public directory, a request to http://localhost:5000/image.jpg from the browser will render the image.

Next, we set up the configuration for the multer middleware, which determines how and where the files are to be saved. In this case, we store the files in the public/images/uploads directory. In this setup, we made use of the mkdirp module to create the uploads directory, if it doesn’t exist.

Afterwards, we created the route to which the images will be posted. On the route definition, the multer middleware (object) is passed as a parameter. This helps the route to accept single file upload, with the expected file in the field name image. We then return the file’s path as part of the response, or return an error if no file is found.

We can run the application back end using the command given below:

node server.js

Setting Up the Front-End

Now that we have the application’s backend running, let’s begin the development of its front-end. For brevity’s sake, we will be building the entire application in just one component (i.e. the app component).

First, we need to register the drag and drop module from the @angular/cdk module in the app.module.ts file, as shown below:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from "@angular/common/http";
import { AppComponent } from './app.component';
import { DragDropModule } from "@angular/cdk/drag-drop" //<--- imported here
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    DragDropModule // <--- registered here
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Next up, we set up the app.component.ts file. This is where most of our application logic resides.

Here, we will set up the upload event handler, the image upload handler, as well as the drop event handler of the CDK drag and drop lists.

import { Component } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { HttpEventType } from "@angular/common/http";
import {moveItemInArray, transferArrayItem, CdkDragDrop} from "@angular/cdk/drag-drop"

// Image model which also holds the upload progress and the file
class ImageFile {
  file: File;
  uploadProgress: string;
}
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  images: ImageFile[] = []; //an array of valid images
  imageUrls: string[] = []; //an array of uploaded image urls
  favourites: string[] = []; //an array of favorite image urls
  message: string = null; //a string to report the number of valid images

  constructor(private http : HttpClient) { } //depedency injection

  selectFiles = (event) => { //image upload handler
    this.images = [];
    let files : FileList = event.target.files;
    for (let i = 0; i < files.length; i++) {
      if (files.item(i).name.match(/\.(jpg|jpeg|png|gif)$/)) { //image validity check
        this.images.push({file: files.item(i), uploadProgress: "0"});
      }
    }
    this.message = `${this.images.length} valid image(s) selected`;
  }
  uploadImages(){ //image upload hander
    this.images.map((image, index) => {
      const formData = new FormData();
      formData.append("image", image.file, image.file.name);
      return this.http.post('http://localhost:5000/upload', formData, {
        reportProgress: true,
        observe: "events"
      })
        .subscribe(event => {
          if (event.type === HttpEventType.UploadProgress ) {
            image.uploadProgress = `${(event.loaded / event.total * 100)}%`;
          } 
          if (event.type === HttpEventType.Response) {
            this.imageUrls.push(event.body.imageUrl);
          }
        });
    });
  }
  drop(event: CdkDragDrop<string[]>) { //cdkdrop event handler
    if (event.previousContainer !== event.container) {
      // this handles moving an item between to list.
      // here we can attach a server request to persist the changes
      transferArrayItem(
          event.previousContainer.data, // the list from which the item is picked
          event.container.data, // the list to which the item is to be placed
          event.previousIndex, 
          event.currentIndex
      );
    } else {
      // this handle when a list is being rearranged
      moveItemInArray(
          event.container.data, // list to be rearranged
          event.previousIndex, 
          event.currentIndex
      );
    }
  }
}

At the top of this file, we imported the modules needed for this component, which include the HttpClient and the HttpEventType. In Angular, the former is used to handle the request, much like the request handling using Axios. HttpEventType is used to check the response event type (uploadProgess event is when the request data is still being uploaded, while the response event is when the response data is sent to the client). We also imported three things from the CDK module. The first one is the moveItemArray method which is used to move items from one position to another (i.e. it rearranges the list). The second one is the transferArrayItem method which is used to transfer an item from one list to another. Finally, we imported the CdkDragDrop which is used to hint the type of event the drop event handler expects.

Next, we declared a model to describe the file struct which includes the file upload progress as a percentage (usually this class would be in a separate file and folder as a model).

Next, we defined three methods that handle specific events in the component, and they are as follows:

  • The selectFiles method is used to handle the change event of the input field. It accepts the event that has a FileList.
    Note: Although FileList has a length property, it is not an array; hence, it does not have high order methods, such as map, filter, etc.
    The selectFiles method filters the list and returns only valid images for upload.
  • The uploadImages method sends the images to the server one after the other, using the FormData object provided by JavaScript. In the subscribe method (more like the then method of JavaScript promises) we accept the response event and act accordingly. For example, if the event is an upload progress event, we get the percentage of completion and return it to be rendered to the user. When the request has been completed, we push the returned image URL to the array of uploaded image URLs.
  • The drop method is used to respond to the drag and drop event. This method handles an item moving up and down a particular list, or between two connected lists. In this method, we can attach server calls (to save the changes) when we move an item from the regular list to the favorites list (although in this application, changes are not persisted).

Next, we edit the app.component.html file. Here, we render the list of images and the input for the image upload. The file is edited as shown below:

<div>
  <br/>
  <!-- form -->
  <div class="col-sm-12">
    <h1>Image Uploader</h1><hr/>
    <button class="btn btn-primary" (click)="imageInput.click()">Select Images</button>
    <input class="form-control d-none" type="file" (change)="selectFiles($event)" multiple #imageInput/>
    <button class="btn btn-success float-right" (click)="uploadImages()">Upload</button>
    <hr>
    <p class="text-info" *ngIf="message"><strong>{{message}}</strong></p>
    <hr>

    <!-- upload progress -->
    <div class="col-12" *ngFor="let image of images">
      <div class="progress" style="margin-bottom: 10px" *ngIf="image.uploadProgress">
        <div class="progress-bar progress-bar-striped progress-bar" role="progressbar" aria-valuenow="75" aria-valuemin="0" naria-valuemax="100" [ngStyle]="{'width': image.uploadProgress }"></div>
      </div>
    </div>
  </div>

<!-- drag and drop list-->
  <hr/>
  <div class="row">
    <div cdkDropList [cdkDropListData]="imageUrls"
    [cdkDropListConnectedTo]="secondList" #firstList="cdkDropList"
    (cdkDropListDropped)="drop($event)" class="col-2 offset-2 card" style="min-height: 100px">
    <h4>Images</h4>
    <div *ngFor="let imageUrl of imageUrls" class="pop" cdkDrag>
      <img  src="{{imageUrl}}" class="img-thumbnail" alt="not available"/><br/>
    </div>
    </div>
    <div cdkDropList [cdkDropListData]="favourites"
    [cdkDropListConnectedTo]="firstList" #secondList="cdkDropList"
    (cdkDropListDropped)="drop($event)" class="col-2 offset-2 card" style="min-height: 100px">
    <h4>Favourites Images</h4>
    <div *ngFor="let imageUrl of favourites" class="pop" cdkDrag>
      <img  src="{{imageUrl}}" class="img-thumbnail" alt="not available"/><br/>
    </div>
    </div>
  </div>
</div>

Here the file input is hidden from the user and given an #imageInput handle which is used by the button shown to the user to trigger the click event of the file input. Also, the selectFiles method previously defined is attached to the on change event of the file input.

Next, the upload progress of the files is displayed. This is achieved using the upload progress attached to each ImageFile object in the images array.

Finally, we render the uploaded images using their URLs in the imageUrls and favorites array. For the first array, we attach the cdkDropList attribute to indicate that this is a list with the drag and drop functionalities handled by the CDK module. We then pass the array of image URLs to the cdkDropListData property for the CDK module to handle the changes using this array as its data target. We then connect it to the other list (in this case, the favorites list) using the cdkDropListConnectedTo property. Next, we attach the drop event handler using the cdkDropListDropped Angular created custom event. Finally, on each item, we attach the cdkDrag to make them draggable and droppable. We do the same for the favorites list and name it the #secondList. Now we should be able to drag items between lists, as well as up and down the same list.

Run the Application

Run the application from the server tab of the Angular IDE.

Now we are done with the development of this application. Congratulations!

Conclusion

In this article, we created a Node.js backend using Express, which accepts file input, saves it, and returns the file path for rendering. We built a front-end with the Angular framework, taking advantage of the Angular CDK’s drag and drop module.

As always, this is a simple application that can be improved by using data persistence, and breaking up the application into smaller, more manageable components. Moving the application’s data handling into a service would be a good step too.

*Originally published by George Anderson at https://dzone.com/articles/angular-7-uploads-backed-by-nodejs

Follow great articles on Twitter

Learn More

Angular 7 (formerly Angular 2) - The Complete Guide
Learn and Understand AngularJS
Angular Crash Course for Busy Developers
The Complete Angular Course: Beginner to Advanced
Angular (Angular 2+) & NodeJS - The MEAN Stack Guide
Become a JavaScript developer - Learn (React, Node,Angular)
Angular (Full App) with Angular Material, Angularfire & NgRx

Suggest