0

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 = '';
}

  • what does the html look like between the parent and child components? In order for ngChanges to fire, it generally means you are passing some data from parent to child which changes, but I don't see any input variables in your code. – Rick Jun 21 '23 at 19:21
  • Hmm I'm not sure what you mean, the two components involved don't have a child-parent relationship with each other. One of them (PixSearchComponent) has a button which triggers the data fetch in the shared service (PixSearchService). The change to the array in the service class (the pixData property) then needs to be somehow detected by/pushed to the PixTableComponent (what I'm trying to accomplish) and displayed. Does that make sense? – public_void_kee Jun 22 '23 at 20:28
  • ok I'm still pretty confused, but somehow you need to get that array back to your component. changing the array in your service alone is not going to trigger anything in your component. – Rick Jun 23 '23 at 16:11
  • Right, so how to get the updated array into the component so that the changes to the array will be reflected in the component's table, that is the question. – public_void_kee Jun 27 '23 at 15:43

2 Answers2

1

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

Andres2142
  • 2,622
  • 2
  • 14
  • 19
  • Thanks for the info! Do you know how I can detect changes in the service's array and update the table when the change occurs? I actually want the process to be triggered when retrievePixData()/fetchData() is called via a button in a different component, as opposed to the fetch happening on init. – public_void_kee Jun 16 '23 at 15:04
  • Any thing you can tell me would be great. Thanks! – public_void_kee Jun 21 '23 at 16:43
0

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"
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jul 01 '23 at 11:52