Search code examples
javascriptangulartypescriptservicecomponents

Stuggling to use function with async onload method with in a service in Angular - same function works perfect in component?


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

enter image description here

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.


Solution

  • 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