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:
- a Node server waiting for our files that we’re trying to upload, and
- 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