Posted on Apr 2nd 2019
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 Angular IDE.
- However, if you already have an Eclipse installation you are happy with, 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.
Optimize your Angular development with Angular IDE powered by CodeMix. If you’re new to Angular, use our new eLearning infrastructure to easily build an Angular front end, using Angular features along the way to make the job easier.
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.
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 which the images will be posted to. 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 back end 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 Angular IDE.
Now we are done with the development of this application – congratulations!
Conclusion
In this article, we created a Node.js back end 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.
The code for this application can be found in our GitHub repository.