I’ve been trying to create a pagination.
The HTTP call should be triggered:
- when the formular has been changed (starting from page zero),
- when “Load More” button is pressed (button triggers BehaviorSubject
loadMore$
).
Whenever the button is pressed, new value is emitted with the current count of the results (resultCount
). With the help of resultCount
I can set the new page number.
Pagination:
pageSize: 10
,
resultCount: 0
page
will be 0
;
but if I trigger load with button:
resultCount: 10
, page
will be 1
For the merging data from previous http call and current http call I use scan
operator, which works also fine.
The problem starts, when I load multiple pages for the same form value and then change the form value again. loadMore$
is not set to 0
and it starts searching from the previous resultCount
.
STEPS
- Change the person type to
Natural
(has only 3 results) - Change the person type to different value (with more results, where we can trigger pagination)
- Trigger pagination (
resultCount
will be set to 10) - Change the person type back to
Natural
- BehaviorSubject does not reset its value, so
resultCount
is still10
, nothing shows up, as we only have 3 results and we’re asking for page 2 (or simply ignoring first 10 results) - click on the button will fix the problem as it will set
resultCount
to0
Bug simulation with the description
import { Component, OnInit } from '@angular/core';
import { ImportsModule } from './imports';
import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import {
BehaviorSubject,
Observable,
merge,
debounceTime,
switchMap,
filter,
of,
tap,
scan,
map,
} from 'rxjs';
import { PersonTypeModel } from './model/person-type.model';
@Component({
selector: 'dropdown-basic-demo',
templateUrl: './dropdown-basic-demo.html',
standalone: true,
imports: [ImportsModule, ReactiveFormsModule],
})
export class DropdownBasicDemo implements OnInit {
// separate formGroup as we want to add debounceTime only for subGroup
formGroup = new FormGroup({
personType: new FormControl<number | null>(null, Validators.required),
subGroup: new FormGroup({
name: new FormControl(''),
surname: new FormControl(''),
identificationNumber: new FormControl(''),
}),
});
readonly pageSize = 10;
result$!: Observable<{ data: PersonTypeModel[]; totalRecords: number }>;
loadMore$ = new BehaviorSubject<number>(0);
get formControls() {
return this.formGroup.controls;
}
readonly personTypeOptions = [
{ label: 'Firefighter', id: 1 },
{ label: 'Policeman', id: 2 },
{ label: 'Legal', id: 3 },
{ label: 'Natural', id: 4 },
];
ngOnInit() {
const personTypeChange$ = this.formControls.personType.valueChanges.pipe(
tap(() => this.formGroup.updateValueAndValidity()) // update value and validity (without this will be first change mark as invalid)
);
// combine two observables
this.result$ = merge(
personTypeChange$,
this.formControls.subGroup.valueChanges.pipe(debounceTime(300))
).pipe(
filter(() => this.formGroup.valid), // person type is required, check
switchMap(() => {
return this.loadMore$.pipe(
switchMap((resultCount) => {
return this.getPersonByPersonType(resultCount);
}),
scan(
(state, change) => ({
...state,
...change,
data: [...(state?.data ?? []), ...(change?.data ?? [])],
}) // scan previous and current result
)
);
}),
map((state) => ({
...state,
noMoreRecords: state.totalRecords === state.data.length,
}))
);
}
<form
[formGroup]="formGroup"
class="card flex flex-column gap-3 justify-content-center"
>
<p-dropdown
[options]="personTypeOptions"
optionValue="id"
placeholder="Select person type"
formControlName="personType"
/>
<div class="flex gap-3" formGroupName="subGroup">
<input pInputText placeholder="Name" formControlName="name" type="text" />
<input
pInputText
formControlName="surname"
placeholder="Surname"
type="text"
/>
<input
pInputText
formControlName="identificationNumber"
placeholder="Identification number"
type="text"
/>
</div>
</form>
<div *ngIf="result$ | async as result" class="flex flex-column gap-3">
<div *ngFor="let personType of result?.data">
{{ personType.name }} {{ personType.surname }} - {{ personType.description
}}
</div>
<button
pButton
(click)="loadMore$.next(result?.data?.length)"
[disabled]="result.noMoreRecords"
>
Load More
</button>
</div>
Here is the simplified stackblitz demo
I’ve tried to fix the problem by emitting new value whenever form changed this.loadMore$.next(0)
and adding debounceTime(1)
to this.loadMore$
stream to prevent duplicit HTTP calls.
Here is an updated example
It works, but maybe there is a better solution how to reset behavior subject. Or maybe even create stream in completely different way.
Thanks for any advices!