Can't get Angular 2 table component to pick up on changes to array in shared service class

71 Views Asked by At

I want to display an array of data fetched by a service in a table component after the service is triggered by a button elsewhere. I've tried to do it using ngOnChanges() but that doesn't appear to notice any changes to the array in the service class after init. I want the flow to be something like this:

PixSearchComponent button click (code not shown) --> PixSearchService data fetch triggered (got this part) --> updated array displayed in PixTableComponent

I did some logging/debugging and the service method is definitely being called. I know it's not something wrong with the table's field binding because I've tested that. Can anyone tell me how to in a sense push the updated array from the service to the table component so that the changes will be reflected in the table? Thanks.

pix-search.service.ts

import {
  HttpClient,
  HttpErrorResponse,
  HttpHeaders,
} from '@angular/common/http';
import { EventEmitter, Inject, Injectable, Optional } from '@angular/core';
import { catchError, map, tap, throwError } from 'rxjs';
import { IPix } from './model/IPix';

@Injectable({
  providedIn: 'root',
})
export class PixSearchService {

  constructor(private http: HttpClient) {}

  pixUpdated: EventEmitter<IPix[]> = new EventEmitter();

  setPixData(pixData: IPix[]) {
    this.pixData = pixData;
    return this.pixUpdated.emit(this.pixData);
  }

  getPixData()  {
    return this.pixData;
  }

  pixData!: IPix[];

  pixUrl: string = 'https://example.ckp-dev.example.com/example';

  retrievePixData(): void {
    const headers = new HttpHeaders({
      'x-api-key':
        'ewogICAgImFwaUtleSIgOiAiMTIzIiwKICAgICJ1c2VySWQiID3649807253098ESSBEZXZlbG9wZXIiCn0=',
    });

    this.setPixData(this.http
      .get<any>(this.pixUrl, {
        headers
      })
      .pipe(
        tap((data) => console.log('All:', JSON.stringify(data))),
        map((data: any) => data.results),
        catchError(this.handleError)
      ) as unknown as IPix[]);
  }

  handleError(err: HttpErrorResponse) {
    let errorMessage = '';
    if (err.error instanceof ErrorEvent) {
      errorMessage = `An error occurred: ${err.error.message}`;
    } else {
      errorMessage = `Server returned code:: ${err.status}, error message is: ${err.message}`;
    }
    console.error(errorMessage);
    return throwError(() => errorMessage);
  }
}

pix-table.component.ts

import {
  Component,
  Inject,
  Input,
  OnChanges,
  OnDestroy,
  OnInit,
  Optional,
} from '@angular/core';
import type { TableSize } from '@dauntless/ui-kds-angular/table';
import type { TableStickyType } from '@dauntless/ui-kds-angular/table';
import type { TableScrollType } from '@dauntless/ui-kds-angular/table';
import { CardElevation } from '@dauntless/ui-kds-angular/types';
import { PixSearchService } from '../pix-search.service';
import { Observable, Subscription } from 'rxjs';
import { IPix } from '../model/IPix';
import { IContract } from '../model/IContract';
import { IAudit } from '../model/IAudit';
import { ICapitation } from '../model/ICapitation';
import { IChangeRequest } from '../model/IChangeRequest';
import { IHnetAudit } from '../model/IHnetAudit';
import { IProduct } from '../model/IProduct';
import { IProvider } from '../model/IProvider';

@Component({
  selector: 'pix-table-component',
  templateUrl: 'pix-table.component.html',
  styleUrls: ['pix-table.component.css'],
  providers: [PixSearchService]
})
export class PixTableComponent implements IPix {
  constructor(private pixSearchService: PixSearchService) {
    this.pixSearchService.pixUpdated.subscribe((pix) => {
      this.pixRecords = this.pixSearchService.getPixData() as unknown as IPix[];
    });
  }

  columns = [
    'ID',
    'Network',
    'LOB',
    'HP Code',
    'Atypical',
    'TIN',
    'GNPI',
    'Org',
    'Business Unit Code',
    'National Contract',
    'National ContractType',
    'Contract Type',
    'Super Group',
    'Contract ID',
    'Amendment ID',
    'Contract Effective Date',
    'Contract Termination Date',
  ];

  rows: any;
  tableSize: TableSize = 'small';
  showHover = true;
  sticky: TableStickyType = 'horizontal';
  scrollType: TableScrollType = 'both';
  label = 'Payment Index Management';
  disabled = 'disabled';
  error = 'error';
  maxlength = 'maxlength';
  showCounter = false;
  elevation: CardElevation = 'medium';

  legacyConfigTrackerId!: number;
  contract!: IContract;
  audit!: IAudit;
  capitation!: ICapitation;
  changeRequest!: IChangeRequest;
  claimType!: string;
  deleted!: string;
  hnetAudit!: IHnetAudit;
  id!: string;
  noPayClassReason!: string;
  payClass!: string;
  product!: IProduct;
  provider!: IProvider;
  rateEscalator!: string;
  status!: string;
  selected: boolean = false;

  pixRecords: IPix[] = [];
  errorMessage: string = '';
}

2

There are 2 best solutions below

1
public_void_kee On BEST ANSWER

I ended up using a Subject() and subscription to alert the table component to the change in the service class' IPix array:

pix-search.component.ts

 constructor(private _pixSearchService: PixSearchService) {}

 //trigger update of the IPix array in the service class
 buttonClicked(button: any) {
   this._pixSearchService.getPixData();
  }

pix-table.component.ts

pixRecords$!: Observable<IPix[]>;

  constructor(private _pixSearchService: PixSearchService) {
    //subscribe to the service class' Subject()
    this._pixSearchService.invokeEvent.subscribe((pixData) => {
      console.log(pixData);
      this.pixRecords$ = pixData;
      console.log(JSON.stringify(pixData));
    });
  }

pix-search.service.ts

 public invokeEvent: Subject<any> = new Subject();

 pixData!: Observable<IPix[]>;

 getPixData(): void {
    const headers = new HttpHeaders({
      'x-api-key':
        'edfg45mFwaUtle345345yICAgICJ1c2VySWQiIDogIlBESSBEZfes39wZXIiCn0=',
    });

    //assign GET result to pixData array
    this.pixData = this.http
      .get<any>(this.pixUrl, {
        headers
      })
      .pipe(
        tap((data) => console.log(JSON.stringify(data))),
        map((data: any) => data.results),
        catchError(this.handleError)
      );
    //alert subscriber (pix-table.component.ts) of the change to pixData
    this.invokeEvent.next(this.pixData);
  }

Then in the html I used '| async' to allow iteration over the Observable

pix-table.component.html

*ngFor="let row of pixRecords$ | async"
2
Andres2142 On

EventEmitter is commonly used for communication between components, children to a parent component.

When it comes to services, Subjects are your best bet (Subject, BehaviorSubject, ReplaySubject, AsyncSubject).

For your particular case, the use of a BehaviorSubject might be enough, you could implement the following:

Service

@Injectable({
  providedIn: 'root',
})
export class PixSearchService {
  private pixUpdated = new BehaviorSubject<IPix[]>([]);

  constructor(private http: HttpClient) {}

  fetchData(): void {
    this.http.get(...).pipe(take(1))
    .subscribe(response => this.pixUpdated.next(response))
  }

  getData(): Observable<IPix[]> {
    return this.pixUpdated.asObservable();
  }

Component

@Componet({...})
export class PixTableComponent implements OnInit, IPix {
  dataSource$!: Observable<IPix[]>; 
 
  constructor(private pixService: PixSearchService) {}

  ngOnInit(): void {
    this.fetch();
    this.load(); // you can call this function whenever you need from another function
                 // without fetching again data from the backend
  }

  private fetch(): void {
    this.pixService.fetchData();  
  }

  private load(): void {
    this.dataSource$ = this.pixService.getData();
  }
}

I'm not sure if you are using Angular Material's table or just a standard table so here are two possible approaches for handling the table's data:

HTML

<!-- Angular Material Table -->
<table mat-table [dataSource]="dataSource$ | async">
 ....
</table>

<!-- standard table -->
<table>
 <thead>
   <tr>...</tr>
 </thead>
 <tbody>
  <tr *ngFor="let item of (dataSource$ | async)">
    ....
  </tr>
 <tbody>
</table>

With the use of the async pipe, you subscribe reactively without the need of handling the subscription

More information about subjects here

More information about Angular component's communication here