0

My page display a list of opportunities based on web service, after that, i want to load more opportunities after clicking on the "Load More" button, which i couldn't find the best way to keep the same observable 'opportunities'

this is my code source :

import { Component, OnInit } from "@angular/core";
import {  Observable } from "rxjs";
import { Opportunity } from "../../models/opportunity.model";
import { OpportunityService } from "../../services/opportunity.service";

@Component({

template : `
<div class="container-lg">
        <mat-toolbar><span i18n>Opportunities</span></mat-toolbar>
        <app-cards *ngIf="opportunities$ | async as opportunities; else searchLoading" [opportunities]="opportunities"></app-cards>
        <button mat-raised-button (click)="loadOportunities()" color="primary">Load More</button>

        <ng-template #searchLoading>
          <div class="center"><mat-progress-spinner mode="indeterminate"></mat-progress-spinner></div>
        </ng-template>
      </div>




`,
styleUrls : ['opportunities.page.scss']


})

export class OpportunitiesPage implements OnInit {



  offset: number = 0;
  limit: number = 12;

  opportunities$?: Observable<Opportunity[]> ;

  constructor(private opportunityService : OpportunityService){

  }

  ngOnInit(): void {

    this.opportunities$ = this.opportunityService.getSeacrhOpportunities(this.offset , this.limit);

  }

  loadOportunities(): void{
    this.offset =  this.offset+this.limit;
    //TODO concat newest with oldest opportunities
}
}

for the opportunity service :

 getSeacrhOpportunities(offset: number , limit : number): Observable<Opportunity[]> {
          let param: any = {'offset': offset , 'limit' : limit , 'locale' : this.locale};
          return this.httpClient.get<APIData<Opportunity[]>>(`${environment.apiUrl}/ws/opportunities`, {params: param}).pipe(
            map((response) => response.data),
            catchOffline(),
          );

        }
Mirlo
  • 625
  • 9
  • 26

2 Answers2

1

You can store the offset and limit in a BehaviorSubject and then pipe (listen) for changes in that variable

export class OpportunitiesPage {
  load$ = new BehaviorSubject<{ offset: number; limit: number }>({
    limit: 12,
    offset: 0,
  });

  opportunities$ = this.load$.pipe(
    map((x) => this.opportunityService.getSeacrhOpportunities(x.offset, x.limit))
  );

  constructor(private opportunityService: OpportunityService) {}

  loadOportunities(): void {
    // just setting some values dont know what you want to happend
    this.load$.next({ limit: 10, offset: 10 });
  }
}

In this case the type will result in a Observable<Observable<Opportunity[]>>, we dont want that and that can be resolves using switchMap over map resulting in the type Observable<Opportunity[]>

  //will be of type Observable<Opportunity[]>
  opportunities$ = this.load$.pipe(
    switchMap(x => this.getSeacrhOpportunities(x.limit, x.offset))
  )

Then just access it as usual

html:

<div *ngif="opportunities$ | async as opportunities"></div>
PEPEGA
  • 2,214
  • 20
  • 37
  • getSeacrhOpportunities(offset: number , limit : number): Observable so opportunities$ will be an Observable> is that fine or no ? – Mirlo Feb 15 '22 at 11:46
  • Correct, that can cause some issues, but then you should be able to use a switchmap instead of only map – PEPEGA Feb 15 '22 at 11:48
  • while getSeacrhOpportunities() return an Observable the opportunities$ will be of type Observable> or should i change the way* i retrieve data ? – Mirlo Feb 15 '22 at 11:52
  • @Mirlo Updated the snippet – PEPEGA Feb 15 '22 at 11:53
  • I believe the author wants to concatenate subsequent results, this will just show the new results. – Chris Hamilton Feb 15 '22 at 12:04
  • your solution seems logical, but after clicking on the button, the WS is never called ! – Mirlo Feb 15 '22 at 12:18
  • Good solution, concatenation can be achieved using the `scan` operator ie: `concatenated$ = opportunities$.pipe((acc,curr) => acc.concat(curr));` then subscribe to `concatenated$` in your template using `async` pipe and avoiding manual subscribe – wlf Feb 15 '22 at 12:50
  • concatenated$ = this.opportunities$.pipe((acc,curr) => acc.concat(curr)); could not working cause opportunities$ is type of Observable – Mirlo Feb 15 '22 at 14:28
1

Since you want to append old and new data, you should use a variable to store your data instead of using an async pipe in the html. We can use a variable to indicate loading as well.

  offset: number = 0;
  limit: number = 12;

  opportunities: Opportunity[] = [];

  loading = true;

  constructor(private opportunityService: OpportunityService) {}

  ngOnInit(): void {
    this.loadOportunities();
  }

  loadOportunities(): void {
    this.loading = true;
    this.opportunityService
      .getSeacrhOpportunities(this.offset, this.limit)
      .subscribe((newOpps) => {
        this.opportunities = this.opportunities.concat(newOpps);
        this.loading = false;
      });
    this.offset += this.limit;
  }
<div class="container-lg">
  <mat-toolbar><span i18n>Opportunities</span></mat-toolbar>
  <app-cards [opportunities]="opportunities"></app-cards>
  <button mat-raised-button (click)="loadOportunities()" color="primary">
    Load More
  </button>

  <ng-container *ngIf="loading">
    <div class="center">
      <mat-progress-spinner mode="indeterminate"></mat-progress-spinner>
    </div>
  </ng-container>
</div>

I would have the progress spinner appear underneath or floating on top.

Making the subscription more robust

To make this more robust, you can prevent the user from making multiple parallel requests, and only increment the offset if the subscription is successful. You can also add error handling, and set loading to false when the subscription completes, successfully or unsuccessfully.

  loadOportunities(): void {
    if (this.loading) return;
    this.loading = true;
    this.opportunityService
      .getSeacrhOpportunities(this.offset, this.limit)
      .subscribe({
        next: (newOpps) => {
          this.opportunities = this.opportunities.concat(newOpps);
          this.offset += this.limit;
        },
        error: (err) => {
          console.log(err);
          //Error handling
        },
        complete: () => (this.loading = false),
      });
  }

It's not necessary to unsubscribe here. See this question to learn more: Angular/RxJS When should I unsubscribe from `Subscription`

If ever you want to cancel a request you can use unsubscribe() to do so.

Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
  • your solution works but the offset is never incremented ! – Mirlo Feb 15 '22 at 12:05
  • 1
    I noticed that bug, I updated the code. I initially declared the observable in `ngOnInit` which attached the numbers themselves rather than a reference to the variables. – Chris Hamilton Feb 15 '22 at 12:06
  • Thanks Chris, your solution works correctly, on this way each click generate a subscription, is that normal ? or should i unsubscribe after the finish of the event ? – Mirlo Feb 15 '22 at 12:17
  • 1
    It's not necessary to unsubscribe when it comes to http requests, since the subscriptions will be cleaned up after a single response. You can however unsubscribe in order to cancel a request. I agree it's dangerous to let the user create multiple requests at a time, so you may want to disable the button when `loading` is true. – Chris Hamilton Feb 15 '22 at 13:11
  • I've edited my answer for more info. – Chris Hamilton Feb 15 '22 at 13:26