I inherited some Angular/ng-boostrap code that defined a table with static data that works fine. Now we need to fetch the data from an API call so I tried to modify it as below. I used the answer from this question as a base.
here is a minimal version showcasing the problem. (https://stackblitz.com/~/github.com/cherfim/angular-problem if the link is not working, copy paste will work)
executions.service.ts where the API call is made:
import { Injectable, PipeTransform } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { debounceTime, delay, map, switchMap, tap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
interface SearchResult {
jobs: any[];
total: number;
}
interface State {
page: number;
pageSize: number;
searchTerm: string;
startIndex: number;
endIndex: number;
totalRecords: number;
}
const compare = (v1: string | number, v2: string | number) => v1 < v2 ? -1 : v1 > v2 ? 1 : 0;
/**
* Table Data Match with Search input
* @param job field value fetch
* @param term Search the value
*/
function matches(job: any, term: string, pipe: PipeTransform) {
return true;
}
@Injectable({
providedIn: 'root'
})
export class AdvancedService {
fetchedJobs: any[] = [];
fetchedJobs$: Observable<any[]>;
private _loading$ = new BehaviorSubject<boolean>(true);
private _search$ = new Subject<void>();
private _jobs$ = new BehaviorSubject<any[]>([]);
private _total$ = new BehaviorSubject<number>(0);
private _state: State = {
page: 1,
pageSize: 10,
searchTerm: '',
startIndex: 0,
endIndex: 9,
totalRecords: 0
};
constructor(private pipe: DecimalPipe,
private http: HttpClient) {
this.fetchedJobs$ = this.http.get<any[]>(`https://api.publicapis.org/entries`).pipe(
tap((res: any[])=>{
this.fetchedJobs=res;
}));
this._search$.pipe(
tap(() => this._loading$.next(true)),
debounceTime(200),
switchMap(() => this._search()),
delay(200),
tap(() => this._loading$.next(false))
).subscribe(result => {
this._jobs$.next(result.jobs);
this._total$.next(result.total);
});
this._search$.next();
}
/**
* Returns the value
*/
get jobs$() { return this._jobs$.asObservable(); }
get total$() { return this._total$.asObservable(); }
get loading$() { return this._loading$.asObservable(); }
get page() { return this._state.page; }
get pageSize() { return this._state.pageSize; }
get searchTerm() { return this._state.searchTerm; }
get startIndex() { return this._state.startIndex; }
get endIndex() { return this._state.endIndex; }
get totalRecords() { return this._state.totalRecords; }
/**
* set the value
*/
set page(page: number) { this._set({ page }); }
set pageSize(pageSize: number) { this._set({ pageSize }); }
set startIndex(startIndex: number) { this._set({ startIndex }); }
set endIndex(endIndex: number) { this._set({ endIndex }); }
set totalRecords(totalRecords: number) { this._set({ totalRecords }); }
set searchTerm(searchTerm: string) { this._set({ searchTerm }); }
private _set(patch: Partial<State>) {
Object.assign(this._state, patch);
this._search$.next();
}
/**
* Search Method
*/
private _search(): Observable<SearchResult> {
const { pageSize, page, searchTerm } = this._state;
return this.fetchedJobs$.pipe(
map((res: any[]) => {
let jobs = Object.values(res);
// 2. filter
jobs = jobs.filter(job => matches(job, searchTerm, this.pipe));
const total = jobs.length;
// 3. paginate
this.totalRecords = jobs.length;
this._state.startIndex = (page - 1) * this.pageSize + 1;
this._state.endIndex = (page - 1) * this.pageSize + this.pageSize;
if (this.endIndex > this.totalRecords) {
this.endIndex = this.totalRecords;
}
jobs = jobs.slice(this._state.startIndex - 1, this._state.endIndex);
return { jobs, total } as SearchResult;
}));
}
}
app.component.ts where the subscription is done:
import { Component, OnInit, ViewChildren, QueryList } from '@angular/core';
import { Observable } from 'rxjs';
import { AdvancedService } from './executions.service';
import { CommonModule, DecimalPipe } from '@angular/common';
@Component({
selector: 'app-component',
standalone: true,
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
imports: [CommonModule],
providers: [AdvancedService, DecimalPipe]
})
/**
* Datatable Component
*/
export class AppComponent implements OnInit {
tables$: Observable<any[]>;
total$: Observable<number>;
testbooks: any[] = [];
constructor(public service: AdvancedService) {
this.tables$ = service.jobs$;
this.total$ = service.total$;
}
ngOnInit(): void {
this.tables$.subscribe((data: any) => {
console.log(data.length);
this.testbooks = data;
});
}
}
then using it in the template with
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table id="datatable" class="table table-bordered dt-responsive nowrap w-100 datatables">
<tbody *ngFor="let job of testbooks;let i=index;">
<tr>
<td>
<div class="card">
<div class="card-body">
<h1>test</h1>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- end card body -->
</div>
<!-- end card -->
</div>
<!-- end col -->
</div>
<!-- end row -->
I see in the browser console that he call is made, data fetched and displayed, my problem is that the API is being called non stop and data refreshed as a result (like once a second).
what am I doing wrong ?
In the _search
method in AdvancedService
, you are calling the totalRecords
and endIndex
setters (lines 118 and 122 in StackBlitz). These setters call the _set
method, which next
s the _search$
Subject, which then causes the _search$
stream subscription inside the service constructor to run.
I do not think you should be next
ing the _search$
Subject when setting startIndex
, endIndex
and totalRecords
. Something like this should work:
set startIndex(startIndex: number) {
this._set({ startIndex }, false);
}
set endIndex(endIndex: number) {
this._set({ endIndex }, false);
}
set totalRecords(totalRecords: number) {
this._set({ totalRecords }, false);
}
private _set(patch: Partial<State>, triggerSearch = true) {
Object.assign(this._state, patch);
if (triggerSearch) {
this._search$.next();
}
}