Drag and Drop Files in the Browser and Upload to a Server - PWA Ionic App


#1

Hi All,

How do we drag & drop files in our PWA (broswer-based) Ionic apps and upload those files to a server?

This tutorial’s going to explain the answer…

There will be 2 parts:

  1. a Node server waiting for our files that we’re trying to upload, and
  2. an Ionic app that’s going to allow dragging & dropping & uploading of these files.

This was written in

Ionic 3.20.0
Node 9.6.1
NPM 5.7.1

Let’s start with the non-Ionic part, our Node server:

In a command prompt, type

mkdir TestServer                  # test server's base dir
cd TestServer                     # move into the dir
mkdir uploads                     # subdir where our uploads will be saved to
npm init                          # create a blank Node project (when prompted, the default settings are fine)
npm install multer --save         # handles our multi-part form data
npm install nodemon --save     # handles live-reloads of our Node server, making debugging easier
npm install express --save        # server magic

Create a file called app.js inside the TestServer directory, and paste in this code:

var express = require('express'),
multer      = require('multer'),
fs          = require('fs'),
app         = express(),
http        = require("http"),
multer      = require('multer'),
server      = http.createServer(app),
router      = express.Router(),
path        = require('path');

app.use(function (req, res, next) {
                res.header("Access-Control-Allow-Methods", "POST, PUT, OPTIONS, DELETE, GET");
                res.header('Access-Control-Allow-Origin', 'http://localhost:3001');
                res.header('Access-Control-Allow-Origin', 'http://localhost:8100');
                res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
                //res.header("Access-Control-Allow-Origin", "*");
                res.header("Access-Control-Allow-Credentials", true);
                next();
});

var middleware = [
                express.static(path.join(__dirname, './uploads/'))
]

app.use(middleware);

var storage = multer.diskStorage({
                destination: function (req, file, callback) {
                                callback(null, './uploads/')
                },
                filename: function (req, file, callback) {
                                var datetimestamp = Date.now();
                                console.log("Filename " + file.originalname);
        callback(null, file.originalname)
                },
                onFileUploadStart: function (file) {
    console.log(file.originalname + ' is starting ...');
                },
                onFileUploadComplete: function (file) {
    console.log(file.fieldname + ' uploaded to  ' + file.path);
                }
});

var upload = multer().any('selectedFile')
//**THIS IS MANDATORY, WITHOUT THIS NOT WORK**

app.use(multer({
                storage:storage
}).any('selectedFile'));
  

app.get('/', (req, res) => res.end('Hello root level!!'))
app.get('/ryan', (req, res) => res.end('Hello Ryan!'))

app.get('/api', function (req, res) {
                res.end('You are using a GET, you need to use a POST');
});

app.post('/api', function (req, res, next) {
  upload(req,res,function(err){
    if(err){
      res.status(500).json({'success':false});
      return;
    }
    res.status(200).json({success:true,message:"File was uploaded successfully !"});
  });
});

var PORT = process.env.PORT || 3001;

app.listen(PORT, function () {
  console.log('Working on port ' + PORT);
});

We’re allowing for CORS requests from port 3001 (this Node server) and port 8100 (our Ionic app).

To run, go to the command prompt and type

nodemon ./app.js localhost:3001

You can test that the server is running by pointing a browser to http://localhost:3001

You can test with curl using the following command:

curl localhost:3001/api -F "image=@\\my\path\to\an\image.png"

Now it’s time to work in Ionic!

In another command prompt (use a separate window because our app.js server is going to print the filenames as they’re uploaded)…

ionic start IonicUploader blank
# (you don't need to add iOS/Android/other things when asked during the initial build process)
cd IonicUploader
npm install ng2-file-upload --save

The important changes in app.module.ts are:

import { FileUploadModule } from 'ng2-file-upload';

and

imports: [
    ...
    ...
    FileUploadModule
  ],

The important additions in home.ts are:

import { NgClass, NgStyle} from '@angular/common';
import { FileUploader } from 'ng2-file-upload';;

and:

directives: [NgClass, NgStyle]

Here is the complete home.ts file:

import { Component } from '@angular/core';
import { NgClass, NgStyle} from '@angular/common';
import { FileUploader } from 'ng2-file-upload';

const URL = 'http://localhost:3001/api/';
 
@Component({
	selector: 'home-page',
	templateUrl: './home.html',
	directives: [NgClass, NgStyle]
})

export class HomePage {
	
	public uploader:FileUploader = new FileUploader({url: URL});
	public hasBaseDropZoneOver:boolean = false;
	public hasAnotherDropZoneOver:boolean = false;

	public fileOverBase(e:any):void {
		this.hasBaseDropZoneOver = e;
	}

	public fileOverAnother(e:any):void {
		this.hasAnotherDropZoneOver = e;
	}
	
	public showFunction(): void {
		var x = document.getElementById("hiddenDiv");
		if (x.style.display === "none") {
			x.style.display = "block";
		} else {
			x.style.display = "none";
		}
	}

	public showFileModified() : void {
		var input = document.getElementById('filename');
		if (!input) {
			alert("Um, couldn't find the filename element.");
		}
		if (!input) {
			alert("Um, couldn't find the filename element.");
		}
		else if (!input.files) {
			alert("This browser doesn't seem to support the `files` property of file inputs.");
		}
		else if (!input.files[0]) {
			alert("Please select a file before clicking 'Show Modified'");
		}
		else {
			file = input.files[0];
			alert("The last modified date of file '" + file.name + "' is " + file.lastModifiedDate);
		}
  }
  
  public DisplaySize(item:any): string {
		let b: number = 0;
		
		if (item && item.file)
			b = item.file.size;

		if (b < 1024)
			return b + " bytes";
		
		b /= 1024;								// convert to KB
		if (b < 1024)
			return b.toFixed(2) + " KB";

		b /= 1024;								// convert to MB
		return b.toFixed(2) + " MB";
	}
}

And here is the home.html file:

<ion-content style="background-color: #842024" >
	<div class="container">
		<div class="row">
			<div class="col-md-3">
				<div ng2FileDrop
				[ngClass]="{'nv-file-over': hasBaseDropZoneOver}"
				(fileOver)="fileOverBase($event)"
				[uploader]="uploader"
				id="file"
				class="well my-drop-zone">
				<h5 class="drag-label"> Drag File(s) Here To Upload </h5>
				</div>	
				
				<br/>
					<input type="file" id="file" name="file" ng2FileSelect [uploader]="uploader"  multiple/><br/>
			</div>
		</div>
	</div>
	
	<hr />
	<div class="container" id="containerdiv">
		<div class="col-md-9" id="resultsdiv">
			<div class="results">
				<p style="font-size:15px; text-align:center; color: #842024; text-transform: uppercase">Staging Area</p>		
				<p> File Queue: {{ uploader?.queue?.length }} </p><br/>
				
				<div class="progressdiv">
					<button type="button" class="btn btn-success btn-s"
						(click)="uploader.uploadAll()" [disabled]="!uploader.getNotUploadedItems().length">
							<span class="glyphicon glyphicon-upload"></span> Upload all
						</button>
						
					<button type="button" class="btn btn-warning btn-s"
					(click)="uploader.cancelAll()" [disabled]="!uploader.isUploading">
						<span class="glyphicon glyphicon-ban-circle"></span> Cancel all
					</button>
					
					<button type="button" class="btn btn-danger btn-s"
					(click)="uploader.clearQueue()" [disabled]="!uploader.queue.length">
						<span class="glyphicon glyphicon-trash"></span> Remove all
					</button>
				
					<br/><br/><br/>
					<div id="hiddenDiv">
						<div class="resultsSummary" id="SpanContainer">
						<span class="block" style="float:left!important;text-transform:uppercase"><br/>Upload Log</span> <br/><br/><br/>
							<div *ngFor="let item of uploader.queue" >						
								<div class="row" *ngIf="item.isSuccess">
									<br/>
									<span class="block">
										<hr style= "border-bottom: 1px solid gray; width:20%"/>
										{{ response }} - {{ item?.file?.name }}  {{ item?.file?.lastModifiedDate }} 
									</span>
									<br/><br/>
								</div>	
							</div>
						</div>
					</div>
				</div>
				
				<table class="table">
					<thead>
						<tr>
							<th width="10%">Filename</th>
							<th width="10%">Size</th>
							<th width="10%">Progress</th>
							<th width="10%">Status</th>
							<th width="10%">Actions</th>

						</tr>
					</thead>
				
					<tbody>
						<tr *ngFor="let item of uploader.queue">
							<td><strong>{{ item?.file?.name }}</strong></td>
							<td> 
								{{ DisplaySize(item) }}
							
							</td>
						<td>
						<div class="progress" style="margin-bottom: 0;">
							<div class="progress-bar progress-bar-striped bg-danger" role="progressbar"  style="width: 75%" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" [ngStyle]="{ 'width': item.progress + '%' }"></div>
						</div>
						</td>
						 <td class="text-center">
							<span *ngIf="item.isSuccess"><i class="glyphicon glyphicon-ok" style="color:red"></i></span>
							<span *ngIf="item.isCancel"><i class="glyphicon glyphicon-ban-circle"></i></span>
							<span *ngIf="item.isError"><i class="glyphicon glyphicon-remove"></i></span>
						</td>
						<td no-wrap>
							<button type="button" class="btn btn-danger btn-s" (click)="item.remove()">
								<span class="glyphicon glyphicon-trash"></span>
							</button>
						</td>
					</tr>
					</tbody>
				</table>
			</div>
		</div>
	</div>	
</ion-content>
		
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
  
<style>
	#containerdiv { margin-top: 50px }
	#hiddenDiv {display:none;}
	#resultsdiv { border: dotted 3px gray; width: 70%; background-color: white; border-radius: 5px }
	#containerdiv h3 { color: white; text-transform: uppercase; font-size:17px; bottom:480px; position: absolute; float:right; right: 380px }
	#SpanContainer { width:500px; height: 300px; overflow-x: hidden; overflow-y: scroll; margin:auto; float: left; position:fixed !important }
	span.block { display: inline-block; width: 300px; }
	.my-drop-zone { border: dotted 3px #842024; height: 250px; background-color: white; border-radius: 5px; width:800px; margin-top:10px; margin-left: -40px }
	.container { width: 80% }
	.nv-file-over { border: solid 3px black; } 
	.drag-label { color: #cccccc; margin-top: 110px; text-align: center }
	.title-label { margin-left:100px; color: black; text-transform:uppercase }
	.navbar { width:100% !important }
	.results { margin-top: 30px; margin-left: 30px; color: darkgray }
	.results p { color: #191919; float: center; text-transform: none; font-weight: 200; line-height: 1em; font-size:12px; font-weight:bold; margin-left:-30px; margin-top: -10px }
	.results th { color: black; text-transform: none; font-weight: 100; line-height: 1em; font-style: italic; opacity: 0.8; font-size:17px }
	.dropbtn { background-color: darkred; color: white; padding: 16px; font-size: 12px; border: none }
	.dropdown { position: relative; display: inline-block }
	.dropdown-content { display: none; position: absolute; background-color: darkred; min-width: 160px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); z-index: 1 }
	.dropdown-content a { color: black; padding: 12px 16px; text-decoration: none; display: block }
	.dropdown-content a:hover {  }
	.dropdown:hover .dropdown-content { display: block }
	.dropdown:hover .dropbtn { background-color: black }
	.upload-title { position:absolute; bottom: 100px !important; right: 100px !important; white-space:nowrap; }
	.resultsSummary { width:550px !important; height: 360px !important; border-radius:10px; background-color: #842024; border: 1px solid black; color:white; font-size: 15px; text-align: center !important; float:right !important; position:fixed !important; right:-10px !important; top: 0px; }
</style>

The home.scss file is

page-home {
	#containerdiv { margin-top: 50px }
	#resultsdiv { border: dotted 3px gray; width: 70%; background-color: white; border-radius: 5px }
	#containerdiv h3 { color: white; text-transform: uppercase; font-size:17px; bottom:480px; position: absolute; float:right; right: 380px }
	.my-drop-zone { border: dotted 3px #842024; height: 250px; background-color: white; border-radius: 5px; width:800px; margin-top:10px; margin-left: -40px }
	.container { width: 80% }
	.nv-file-over { border: solid 3px black; } 
	.drag-label { color: #cccccc; margin-top: 110px; text-align: center }
	.title-label { margin-left:100px; color: black; text-transform:uppercase }
	.navbar { width:100% !important }
	.results { margin-top: 30px; margin-left: 30px; color: darkgray }
	.results p { color: #191919; float: center; text-transform: none; font-weight: 200; line-height: 1em; font-size:12px; font-weight:bold; margin-left:-30px; margin-top: -10px }
	.results th { color: black; text-transform: none; font-weight: 100; line-height: 1em; font-style: italic; opacity: 0.8; font-size:17px }
	.progressdiv { position:absolute; bottom: 100px; right:-350px }
	.dropbtn { background-color: darkred; color: white; padding: 16px; font-size: 12px; border: none }
	.dropdown { position: relative; display: inline-block }
	.dropdown-content { display: none; position: absolute; background-color: darkred; min-width: 160px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); z-index: 1 }
	.dropdown-content a { color: black; padding: 12px 16px; text-decoration: none; display: block }
	.dropdown-content a:hover { background-color: #ddd }
	.dropdown:hover .dropdown-content { display: block }
	.dropdown:hover .dropbtn { background-color: black }
	 html, body { height: 100%; background: #842024 }	
}

I haven’t spent the time to consolidate the style in home.html into home.scss.

From here, you’ll need to include the following files in at the same level as home.ts/html/scss:

file-drop.directive.spec.ts
file-drop.directive.ts
file-item.class.ts
file-like-object.class.ts
file-select.directive.ts
file-type.class.ts
file-uploader.class.ts

All of these files can be downloaded from here.

And that’s it! You now have a drag-and-drop file uploader!

Thanks,
Ryan


File Upload from Browser
#2

This post is only a month old, and already it’s not current? Thank you for trying to share!
I’m on Ionic 3.20.0 also, but getting this warning when adding directives to @Component in home.ts.

Argument of type '{ selector: string; templateUrl: string; directives: (typeof NgClass | typeof NgStyle)[]; }'
is not assignable to parameter of type 'Component'.
  Object literal may only specify known properties, and 'directives' does not exist in type 'Component'.

I found this post from over a year ago, so not sure if it’s relevant:


#3

Thanks for this tutorial. Works great!


#4

@Nimai, have you found the bug in your code? I’d appreciate if you could post it so others aren’t thrown off by your comment that the code is out of date. I worked hard on this tutorial.

The code still works fine. :slight_smile: