Home > Software design >  Concat async observable data based on click event
Concat async observable data based on click event

Time:02-16

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 >
        <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 ><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(),
          );

        }

CodePudding user response:

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>

CodePudding user response:

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 >
  <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 >
      <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.

  • Related