Any file Angular upload form now wants its own drag & drop system, to be able to insert files by dragging them over the affected area.
To do this with Angular we define a directive that manages the drop on a particular area of the page
// ./directives/dnd.directive.ts
import {
Directive,
Output,
EventEmitter,
HostBinding,
HostListener,
} from '@angular/core';
@Directive({
selector: '[appDnd]',
})
export class DndDirective {
@HostBinding('class.fileover') fileOver: boolean;
@Output() fileDropped = new EventEmitter<any>();
// Dragover listener
@HostListener('dragover', ['$event']) onDragOver(evt: DragEvent) {
evt.preventDefault();
evt.stopPropagation();
this.fileOver = true;
}
// Dragleave listener
@HostListener('dragleave', ['$event']) public onDragLeave(evt: DragEvent) {
evt.preventDefault();
evt.stopPropagation();
this.fileOver = false;
}
// Drop listener
@HostListener('drop', ['$event']) public onDrop(evt: DragEvent) {
evt.preventDefault();
evt.stopPropagation();
this.fileOver = false;
let files = evt.dataTransfer.files;
if (files.length > 0) {
this.fileDropped.emit(files);
}
}
}
Immediately after create the component that will be included in the page and that will take the files, shooting them through EventEmitter
on the page so let’s start with the typescript part.
The component has also been added a management of the same files (you cannot upload the same file twice), error handling and the possibility of recovering the list of files already loaded, in the case of a multi-step form (via the filesUploaded
property). In the case of a file list already loaded, it will not be possible to delete the single file. We will delete all the files in bulk.
// box-file/box-file.component.ts
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
import {
UPLOAD_ACCEPTED_FILE_EXTENSIONS,
UPLOAD_MAX_SIZE,
} from '../app.constants';
@Component({
selector: 'app-box-file',
templateUrl: './box-file.component.html',
styleUrls: ['./box-file.component.scss'],
})
export class BoxFileComponent implements OnInit {
files: any[] = [];
@Input() fileNames: string[] = [];
@Input() filesMinLimit: number = 1; // Files min limit.
@Input() filesMaxLimit: number = 2; // Files max limit.
@Output() filesEvent = new EventEmitter<any[]>();
filesSize: number;
isLocaleDelete: boolean = true; // Check if files are already loaded.
error: string;
ACCEPTED_FILE_EXTENSIONS = UPLOAD_ACCEPTED_FILE_EXTENSIONS.join(', ');
private errorFormatSize = `Verify format (.jpg, .png e .pdf) and size (max 5 MB).`;
getFileExtension = (fileName: string) =>
fileName.split('.').pop() === 'pdf' ? 'pdf' : 'img'; // For icon class.
constructor() {}
ngOnInit() {
if (this.fileNames?.length > 0) {
this.isLocaleDelete = false;
}
}
/**
* On file drop handler.
*/
onFileDropped(files: unknown) {
this.prepareFilesList(files as any[]);
}
/**
* Handle file from browsing.
*/
fileBrowseHandler(target: any) {
console.debug('Browse handler: ', target);
if (target) {
this.prepareFilesList(target.files);
}
}
/**
* Delete file from files list.
* @param index (File index)
*/
deleteFile(index: number) {
this.files.splice(index, 1);
this.fileNames.splice(index, 1);
this.filesEvent.emit(this.files);
}
/**
* Delete all files.
*/
deleteAll() {
this.isLocaleDelete = true;
this.files = [];
this.fileNames = [];
this.filesEvent.emit(null);
}
/**
* Convert Files list to normal array list.
* @param files (Files List)
*/
prepareFilesList(files: Array<any>) {
if (
this.isLocaleDelete &&
(!this.fileNames || this.fileNames?.length < 2)
) {
this.error = null;
this.isLocaleDelete = true;
for (const file of files) {
if (
this.fileNames &&
this.fileNames.some((f: string) => f === file.name)
) {
this.error = "Can't load equal files.";
} else if (
UPLOAD_ACCEPTED_FILE_EXTENSIONS.some(
(e: string) => file.name.lastIndexOf(e) >= 0
)
) {
file.progress = 0;
this.files.push(file);
this.fileNames.push(file.name);
} else {
this.error = this.errorFormatSize;
}
}
this.filesSize = this.files
.map((f: File) => f.size)
.reduce((s1, s2) => s1 + s2, 0);
if (this.filesSize > UPLOAD_MAX_SIZE) {
this.error = this.errorFormatSize;
this.deleteAll();
}
console.debug('Files: ', this.files);
this.files = this.files.slice(0, this.filesMaxLimit);
this.fileNames = this.fileNames.slice(0, this.filesMaxLimit);
// Emitter for uploaded files (must be or output null).
this.filesEvent.emit(
this.files.length >= this.filesMinLimit ? this.files : null
);
} else {
this.error = 'Files already loaded.';
}
// this.uploadFilesSimulator(0);
}
/**
* Format bytes in right unit.
* @param bytes (File size in bytes)
* @param decimals (Decimals point)
*/
formatBytes(bytes: number, decimals?: number) {
if (bytes === 0) {
return '0 Bytes';
}
const k = 1024;
const dm = decimals <= 0 ? 0 : decimals || 2;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
}
Set in the application the maximum size that the file must have and the extensions accepted.
// app.constants.ts
export const UPLOAD_ACCEPTED_FILE_EXTENSIONS = ['.jpg', '.pdf', '.png'];
export const UPLOAD_MAX_SIZE = 5 * 1024 * 1024; // 5 MB
For the template create the box with the hidden input field that uses the directive from earlier. We have created them for the file drop.
<!-- ./box-file/box-file.component.html -->
<div class="row">
<div class="col-sm-5 spacer-xs-bottom-20">
<div
class="docs-preview-container spacer-xs-bottom-05"
appDnd
(fileDropped)="onFileDropped($event)"
>
<div class="docs-preview equalize" style="min-height: 206px;">
<div class="docs-preview-img">
<svg
_ngcontent-ikb-c0=""
height="64"
viewBox="0 0 63 64"
width="63"
xmlns="http://www.w3.org/2000/svg"
>
<g _ngcontent-ikb-c0="" fill="#3B454F" fill-rule="nonzero">
<path
_ngcontent-ikb-c0=""
d="M42.656 15.135a1.953 1.953 0 0 1-1.391-.578L31.5 4.795l-9.765 9.762a1.97 1.97 0 1 1-2.785-2.785L30.106.616a1.97 1.97 0 0 1 2.785 0l11.157 11.156a1.97 1.97 0 0 1-1.392 3.363z"
></path>
<path
_ngcontent-ikb-c0=""
d="M31.5 36.791a1.97 1.97 0 0 1-1.969-1.969V2.01a1.97 1.97 0 0 1 3.938 0v32.812a1.97 1.97 0 0 1-1.969 1.969z"
></path>
<path
_ngcontent-ikb-c0=""
d="M55.781 63.041H7.22A7.225 7.225 0 0 1 0 55.822V41.385a4.599 4.599 0 0 1 4.594-4.594h7.234a4.567 4.567 0 0 1 4.402 3.276l2.814 9.382a.658.658 0 0 0 .628.467h23.656a.658.658 0 0 0 .628-.467l2.814-9.385a4.572 4.572 0 0 1 4.402-3.273h7.234A4.599 4.599 0 0 1 63 41.385v14.437a7.225 7.225 0 0 1-7.219 7.219zM4.594 40.729a.656.656 0 0 0-.657.656v14.437a3.286 3.286 0 0 0 3.282 3.282H55.78a3.286 3.286 0 0 0 3.282-3.282V41.385a.656.656 0 0 0-.657-.656h-7.234a.65.65 0 0 0-.628.467L47.73 50.58a4.628 4.628 0 0 1-4.402 3.274H19.672a4.567 4.567 0 0 1-4.402-3.276l-2.814-9.382a.65.65 0 0 0-.628-.467H4.594z"
></path>
</g>
</svg>
</div>
<div>
<span class="text-light h6">Drag & Drop here</span>
<p class="btn-container btn-container-center clearfix">
<label for="fileDropRef" class="btn btn-xs btn-primary">
Browse
</label>
</p>
</div>
</div>
<input type="file"
name="documents"
style="display: none;"
#fileDropRef
id="fileDropRef"
(change)="fileBrowseHandler($event.target)"
[accept]="ACCEPTED_FILE_EXTENSIONS"
class="form-control"
multiple="multiple" />
</div>
<span class="invalid-feedback d-block">
<span [innerHTML]="error"></span>
</span>
</div>
<div class="col-sm-7">
<ul class="list-file spacer-xs-top-05">
<li class="list-file-{{ getFileExtension(fileName) }} spacer-xs-bottom-30"
*ngFor="let fileName of fileNames; let i = index">
<a href="javascript:void(0)" title="See the file">{{ fileName }}</a>
<a class="file-remove spacer-xs-left-10"
href="javascript:void(0)"
(click)="deleteFile(i)"
*ngIf="isLocaleDelete"
title="Remove the file"
>(X)</a>
</li>
</ul>
<div (click)="deleteAll()" *ngIf="!isLocaleDelete && fileNames.length > 0">
<a class="file-remove"
href="javascript:void(0)"
id="delete-all"
title="Remove all documents">
Remove all
</a>
<a href="javascript:void(0)" class="spacer-xs-left-10">Remove all files</a>
</div>
</div>
</div>
Include styles for the box and for the file list.
/** BOX FILE */
.docs-preview-container {
background-color: #f6f6f6;
padding: 15px;
}
.col-sm-5 {
width: 41.66666667%;
float: left;
}
.docs-preview {
border: 1px dashed #ccc;
background-color: #f6f6f6;
background-position: 0 0, 50px 50px;
min-height: 206px;
text-align: center;
}
/** FILE LIST */
.col-sm-7 {
width: 58.33333333%;
float: left;
}
.list-file li {
text-align: left;
color: black;
padding: 3px 0 3px 3px;
background-repeat: no-repeat;
background-position: left 2px;
list-style: none;
margin-bottom: 5px;
background-size: 24px;
}
.list-file li a {
cursor: default;
color: #222427;
}
.list-file li a,
.list-file li a:hover,
.list-file li a:focus {
text-decoration: none;
color: #00328e;
outline: none;
}
.list-file li a.file-remove {
cursor: pointer;
display: inline-block;
width: 12px;
height: 100%;
background-repeat: no-repeat;
background-position: center;
background-size: 12px;
margin-left: 15px;
/* text-indent: -9999px; */
}
/** ERRORS */
.invalid-feedback {
font-size: 14px;
color: #d12e2e;
line-height: 24px;
}
The parent component will have to include both the app-box-file
tag component and the method to retrieve the manage files in the parent form.
// parent component - app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
filesToUpload: any[] = null;
filesUploaded: string[] = [];
/**
* Add files, to upload, to parent component.
* @param files
*/
addFilesToUpload(files: unknown) {
console.debug('DND files: ', files);
this.filesToUpload = (files as any[]);
}
}
<!-- parent component - app.component.html -->
<app-box-file
[fileNames]="filesUploaded || []"
(filesEvent)="addFilesToUpload($event)"></app-box-file>
You can also possibly insert the maximum and minimum number of files.
Finally include everything inside the module.
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { BoxFileComponent } from './box-file/box-file.component';
import { DndDirective } from './directives/dnd.directive';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: [AppComponent, BoxFileComponent, DndDirective],
bootstrap: [AppComponent],
})
export class AppModule {}
Below is the demo of the implementation.
That’s all for Angular drag & drop upload forms.
Try it at home!