I have build a component in Angular that imports an excel file and convert it into an array and then displays the content on the excel file on the page as a table. This works perfectly if I build the fuction in the component as follows:
data-import.compoent.ts
import { Component, OnInit } from '@angular/core';
import { DataImporterService } from 'src/app/services/data-importer.service';
import * as excelhandler from 'xlsx';
@Component({
selector: 'app-data-import',
templateUrl: './data-import.component.html',
styleUrls: ['./data-import.component.css'],
})
export class DataImportComponent {
// Properties
importedExcelData: any;
constructor() {}
importFile (event: any){
const fileToUpload: DataTransfer = <DataTransfer>event.target;
if (fileToUpload.files.length !== 1)
throw new Error('Can not upload multiple files');
const fileReader: FileReader = new FileReader();
fileReader.onload = (e: any) => {
const binstring: string = e.target.result;
const workbook: excelhandler.WorkBook = excelhandler.read(binstring, {
type: 'binary',
});
const worksheetName: string = workbook.SheetNames[0];
const worksheet: excelhandler.WorkSheet = workbook.Sheets[worksheetName];
this.importedExcelData = excelhandler.utils.sheet_to_json(worksheet, {
header: 1,
});
};
fileReader.readAsBinaryString(fileToUpload.files[0])
}
data-import.component.html
<input type="file" (change)="importFile($event)" multiple="false" />
<table>
<tbody>
<tr *ngFor="let row of importedExcelData">
<td style="margin-left:5em" *ngFor="let cell of row">{{ cell }}</td>
</tr>
</tbody>
</table>
The problem is that I am working at making my components as "presentational only" as possible - so the natural step would be to move this importFile
function into a service that returns an observable subscibed to in the component as follows:
data-importer.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import {} from 'rxjs/add/observable/fromPromise';
import * as excelhandler from 'xlsx';
@Injectable({
providedIn: 'root',
})
export class DataImporterService {
// Properties
importedExcelData: any
// Functions
importFile(event: any): Observable<any> {
const fileToUpload: DataTransfer = <DataTransfer>event.target;
if (fileToUpload.files.length !== 1)
throw new Error('Can not upload multiple files');
const fileReader: FileReader = new FileReader();
fileReader.onload = (event: any) => {
const binstring: string = event.target.result;
const workbook: excelhandler.WorkBook = excelhandler.read(binstring, {
type: 'binary',
});
const worksheetName: string = workbook.SheetNames[0];
const worksheet: excelhandler.WorkSheet = workbook.Sheets[worksheetName];
this.importedExcelData = excelhandler.utils.sheet_to_json(worksheet, {
header: 1,
});
fileReader.readAsBinaryString(fileToUpload.files[0]);
};
return this.importedExcelData
}
}
revised data-import.component.ts
import { Component} from '@angular/core';
import { DataImporterService } from 'src/app/services/data-importer.service';
import * as excelhandler from 'xlsx';
@Component({
selector: 'app-data-import',
templateUrl: './data-import.component.html',
styleUrls: ['./data-import.component.css'],
})
export class DataImportComponent {
// Properties
importedExcelData: any;
constructor(private dataService: DataImporterService) {}
importFile(event: any) {
this.dataService
.importFile(event)
.subscribe(
(importedExcelData: any) => (this.importedExcelData = importedExcelData)
);
}
}
The component template remains unchanged
The problem is that when try import excel files now with importFile funtion through a subscribed function I get the follow in my console
Having littered my source code with console logs every where what I can determine is that the service function is returning the importedExcelData
before the asynchronous fileReader.onload
has the opertunity to complete its work - therefore its returning an undefined observable, but after researching for several hours, I can't seem find a workable solution. As a last resort I am asking the StackOverFlow community for help. Any assistance would be appreciated.
Please let me know if I need to provide any additional information.
The idea to use an Observable to handle the async functions look fine. The only issue is, at the moment, no observable is created. You could create one using new Observable
construct.
Try the following
import { Observable } from 'rxjs';
importFile(event: any): Observable<any> {
return new Observable(subscriber => {
const fileToUpload: DataTransfer = <DataTransfer>event.target;
if (fileToUpload.files.length !== 1)
subscriber.error('Can not upload multiple files'); // <-- emit error
const fileReader: FileReader = new FileReader();
fileReader.onload = (event: any) => {
const binstring: string = event.target.result;
const workbook: excelhandler.WorkBook = excelhandler.read(binstring, {
type: 'binary',
});
const worksheetName: string = workbook.SheetNames[0];
const worksheet: excelhandler.WorkSheet = workbook.Sheets[worksheetName];
const importedExcelData = excelhandler.utils.sheet_to_json(worksheet, {
header: 1,
});
subscriber.next(importedExcelData); // <-- emit notification
subscriber.complete(); // <-- complete and close the subscription
};
fileReader.readAsBinaryString(fileToUpload.files[0]);
});
}
Since we are sending error notifications, you could pass an error
callback in the subscription to capture them.
importFile(event: any) {
this.dataService.importFile(event).subscribe({
next: (importedExcelData: any) => this.importedExcelData = importedExcelData,
error: (error: any) => console.log(error)
});
}
More info about Observables could be found here