Example: File Upload Modal
File Upload Modal example using a NgBootstrap modal with a NgBootstrap progress bar component to measure upload progress
Launching the FileUploadModal
const modalConfig: NgbModalOptions = {
windowClass: 'dialog small',
centered: true,
keyboard: false, // Prevents user from closing modal using ESC
backdrop: 'static' // Prevents user from closing modal when clicking the modal backdrop
};
let modalRef = this.modalService.open(FileUploadDialogComponent, modalConfig);
modalRef.componentInstance.uploadKey = 'SOME_UPLOAD_KEY_CONSTANT';
Component .ts
import { Component, Input, ViewChild, OnDestroy } from '@angular/core';
import { HttpEventType } from '@angular/common/http';
import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { DashboardServiceWrapper } from '../../api/services/dashboard.service';
import { ConfirmModalComponent } from '../../shared/components/confirm-modal.component';
/**
* Single file upload dialog component
*/
@Component({
selector: 'app-file-upload-dialog',
templateUrl: './file-upload-dialog.component.html',
styleUrls: ['./file-upload-dialog.component.scss']
})
export class FileUploadDialogComponent implements OnDestroy {
/**
* @ignore
*/
destroy = new Subject();
/**
* A string indicating supported file extension types. For example: '.xls,.txt,.csv'
*/
@Input() supportedFileExtensions: string;
/**
* A title to display in the dialog
*/
@Input() title: string;
/**
* A message to display in the dialog
*/
@Input() message: string;
/**
* A supported file upload key (e.g. 'FUNDS_ASSIGNMENT')
*/
@Input() uploadKey: string;
/**
* The reference to the hidden file input element in the DOM
*/
@ViewChild('fileInputElem') fileInputElem;
/**
* @ignore
*/
self = this;
/**
* File object selected by the file browser. This will be used to display file info such as the name and size.
*/
selectedFile: any;
/**
* A flag indicating whether or not to display the file upload progress bar
*/
showProgressBar: boolean;
/**
* Current file upload progress
*/
progressBarValue: number;
/**
* The state of the file upload action. Possible values: null, 'IN_PROGRESS', 'COMPLETE'
*/
uploadStatus: string;
/**
* The different possible upload action states
*/
uploadStatuses: any;
/**
* Used to display error messages in the dialog
*/
errorMsg: string;
/**
* @ignore
*/
constructor(private modalService: NgbModal, private activeModal: NgbActiveModal, private dashboardServiceWrp: DashboardServiceWrapper) {
this.supportedFileExtensions = '.csv';
this.title = "Upload File";
this.message = "Select a .CSV file to upload.";
this.uploadKey = null;
this.selectedFile = null;
this.showProgressBar = false;
this.progressBarValue = 0;
this.uploadStatus = null;
this.uploadStatuses = {
InProgress: 'IN_PROGRESS',
Complete: 'COMPLETE'
};
this.errorMsg = null;
}
/**
* Perform a little cleanup
*/
ngOnDestroy() {
// Clean up the subscriptions
this.destroy.next();
this.destroy.complete();
}
/**
* Performs a check for an in progress upload before closing the dialog
*/
closeDialog() {
if (this.uploadStatus === this.uploadStatuses.InProgress) {
const modalRef = this.modalService.open(ConfirmModalComponent);
modalRef.componentInstance.body = `<h4>An active upload is in progress. Are you sure you want to close this dialog?</h4>`;
modalRef.result.then((confirmed: boolean) => {
if (confirmed) {
this.activeModal.close(true);
}
}, () => { });
} else {
this.activeModal.close(true);
}
}
/**
* Called to open the file browser window by activating the hidden input's onclick.
*/
openFileBrowseDialog() {
this.errorMsg = null;
this.progressBarValue = 0;
this.fileInputElem.nativeElement.click();
}
/**
* Checks the validity of the file selected from the file browser and passes it to the upload API.
*/
handleFiles(event, ctrlRef) {
// References to 'this' from within this function are referring to the input
let files = event && event.target ? event.target.files : null;
let accept = event && event.target ? event.target.accept : null;
if(files && files.length) {
let file = files[0];
let fileNameParts = file.name.split('.');
let fileExt = '.' + fileNameParts[fileNameParts.length - 1];
let resetFiles = () => {
//Reset file input, so onchange will fire even if the same document is uploaded again.
try{
event.target.files = null;
} catch(e) {
//IE11 should drop us here. No additional handling for now.
console.warn(e);
}
event.target.value = null;
};
if(accept && accept.indexOf(fileExt) === -1) {
//Notify that the file type is not supported
ctrlRef.errorMsg = 'The file extension ' + fileExt + ' is not supported.';
resetFiles();
return;
}
//Take the first selected file
ctrlRef.selectedFile = file;
ctrlRef.uploadStatus = ctrlRef.uploadStatuses.InProgress;
// Call the appropriate upload service and close the modal upon success
if (this.uploadKey) {
this.dashboardServiceWrp.submitFileForUpload(file, this.uploadKey).pipe(takeUntil(this.destroy)).subscribe(
resp => {
// In order for the progress to work, the HTTP request has to be set up to report progress (see HttpEvent, HttpProgressEvent)
if(resp.type === HttpEventType.UploadProgress) {
this.progressBarValue = Math.round(100 * resp.loaded / resp.total);
} else if(resp.type === HttpEventType.Response) {
ctrlRef.uploadStatus = ctrlRef.uploadStatuses.Complete;
}
},
err => {
let errorMsg = '';
if (err && err.error && err.error.exception && err.error.exception.message) {
errorMsg = err.error.exception.message + '\n\n';
}
if (err && err.message) {
errorMsg += err.message;
}
this.errorMsg = errorMsg && errorMsg.length ? errorMsg : null;
ctrlRef.uploadStatus = null;
resetFiles();
});
} else {
this.errorMsg = 'An upload mode was not specified for this modal. File upload is not available for this modal at this time.';
resetFiles();
}
}
}
}
Component .html
<div class="modal-header">
<h1 class="modal-title">{{title}}</h1>
</div>
<div class="modal-body" aria-live="assertive">
<p *ngIf="!uploadStatus">{{message}}</p>
<div class="d-flex align-items-center" *ngIf="uploadStatus">
<i class="fas fa-file-alt flex-grow-0 flex-shrink-0 mr-2"></i>
<div class="flex-grow-1 flex-shrink-1">
<span class="mr-1">{{selectedFile.name}}</span>
<span>{{utilService.formatFileSize(selectedFile.size)}}</span>
</div>
<app-busy-spinner *ngIf="uploadStatus === uploadStatuses.InProgress"></app-busy-spinner>
</div>
<div class="mt-2" *ngIf="uploadStatus === uploadStatuses.InProgress">
<ngb-progressbar class="common-progress-bar" showValue="true" [value]="progressBarValue" height="1.5rem"></ngb-progressbar>
</div>
<ngb-alert class="mt-3" type="danger" [dismissible]="false" *ngIf="errorMsg">{{errorMsg}}</ngb-alert>
<ngb-alert class="mt-3" type="success" [dismissible]="false" *ngIf="uploadStatus === uploadStatuses.Complete">Your file has been successfully uploaded.</ngb-alert>
<input type="file" name="fileChooser" title="File Chooser" id="fileElem" #fileInputElem [accept]="supportedFileExtensions" (change)="handleFiles($event, self)" style="display:none">
</div>
<div class="modal-footer d-flex justify-content-center p-3">
<button type="button" class="btn-primary w-50 mr-3" (click)="openFileBrowseDialog()" *ngIf="!uploadStatus">Browse</button>
<button type="button" class="btn-primary w-50 mr-3 disabled" [disabled]="true" *ngIf="uploadStatus">
<span *ngIf="uploadStatus === uploadStatuses.InProgress">Uploading...</span>
<span *ngIf="uploadStatus === uploadStatuses.Complete">Uploaded</span>
</button>
<button type="button" class="btn-secondary w-50" (click)="closeDialog()" ngbAutoFocus>Close</button>
</div>
Component .scss
.modal-body {
font-size: 0.8rem;
}
Component spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';
import { NgbActiveModal, NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { FileUploadDialogComponent } from './file-upload-dialog.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DashboardServiceWrapper } from '../../api/services/dashboard.service';
import { DashboardService } from '../../api/api/dashboard.service';
describe('FileUploadDialogComponent', () => {
let component: FileUploadDialogComponent;
let fixture: ComponentFixture<FileUploadDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ HttpClientModule, NgbModule ],
declarations: [ FileUploadDialogComponent ],
providers: [{ provide: NgbModal }, { provide: NgbActiveModal }, DashboardServiceWrapper, DashboardService],
schemas: [ NO_ERRORS_SCHEMA ]// Ignore child components so we don't have to provide dependencies.
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FileUploadDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
App Styling
ngb-progressbar.common-progress-bar {
.progress-bar {
background-color: $brand-vms-primary;
}
}