Home > OS >  Chaining observable results
Chaining observable results

Time:06-27

I am trying to fetch the customerID from the angular class "UserFacade" and then using the retrieved customerID to retrieve the points for this customer using a http client GET call to a REST endpoint.

The approach I am using is creating 2 variables user$ and point$ which must be initialized correctly which will guarantee me that the points are received correctly. Is there a better way implement this logic for example not using these two variables user$ and point$ and directly retrieve them via let's say function getPts():number?

The HTML:

<div >
  <ng-container *ngIf="user$ | async as user">
    <ng-container *ngIf="point$ | async as point">
         <p>  {{getCustomerPoints()}}  </p> 
    </ng-container>
  </ng-container>

</div>

This is where main logic resides:

import { Component, OnInit } from '@angular/core';
import { AuthService, CmsComponent, User, UserIdService, } from '@spartacus/core';
import { CmsComponentData } from '@spartacus/storefront';
import { UserAccountFacade } from '@spartacus/user/account/root';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { switchMap } from 'rxjs/operators';
import { PointsRetrievalService } from '../points-retrieval.service';
import { FOOTER_VALUE, HEADER_VALUE } from './component-labels';


export interface CustomerPoints {
  points: number;
}


export interface CmsSlpCustomUserPointsComponent4 extends CmsComponent {
  title: string;
  headerText: string;
  footerText: string;
}

@Component({
  selector: 'app-userpoints',
  templateUrl: './userpoints.component.html',
  styleUrls: ['./userpoints.component.scss']
})
export class UserpointsComponent implements OnInit {
  public point$!: Observable<CustomerPoints>;
  user$: Observable<User | undefined> | undefined;
  data$: Observable<CmsSlpCustomUserPointsComponent4> = this.component.data$;
  private customerPointsRetrieved = 0
  constructor(
    public component: CmsComponentData<CmsSlpCustomUserPointsComponent4>, private pointsRetrievalService: PointsRetrievalService,
    protected auth: AuthService,
    protected userAccount: UserAccountFacade,
    protected userIdService: UserIdService,
  ) {}


  ngOnInit() : void {
    this.user$ = this.auth.isUserLoggedIn().pipe(
      switchMap(isUserLoggedIn => {
        if (isUserLoggedIn) {
     
          this.userAccount.get().subscribe(
            (user) =>
            {
              if(user && user?.customerId)
              {
                this.point$ = this.pointsRetrievalService.getPointsForCustomerId(user?.customerId) 
                this.point$.subscribe(
                  (userPoints) =>
                  {
                    console.log("New points:"  userPoints.points)
                    console.log(userPoints.points)
                    this.customerPointsRetrieved = userPoints.points
                  }
                )
              }
            }
          )
      
          return this.userAccount.get();  
        } else {
          return of(undefined);
        }
      })
    );

  }



getCustomerPoints()
{
  return this.customerPointsRetrieved;
}

}

Service retrieving the points via http:


import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { UserAccountFacade } from '@spartacus/user/account/root';
import { BASE_SITE_ID, CUSTOMER_ID, ELECTRONICS_SITE, HTTP_JSON_HEADERS, REST_URL } from './userpoints/rest-connection-constants';
import { CustomerPoints } from './userpoints/customer-points-interfaces';
@Injectable({
  providedIn: 'root'
})

export class PointsRetrievalService{

  constructor(private http: HttpClient,
    protected userAccount: UserAccountFacade) {

  }

  getPointsForCustomerId(userID: string): Observable<CustomerPoints>
   {
    {
      console.log("User id received:"   userID)
      const url = `${REST_URL}${userID}`;
      //const url = this.mainRestUrl   this.userIdUsed   this.fieldParams;
      console.log(url);

      let params = new HttpParams().set(CUSTOMER_ID, userID).set(BASE_SITE_ID, ELECTRONICS_SITE);

      const options = { params: params, headers: HTTP_JSON_HEADERS }

      return this.http.get<CustomerPoints>(url, options);
    }

  }


}


CodePudding user response:

I don't see the point of user$ and point$ streams.

Also, the logic in ngOnInit seems a bit too complicated (usually nested subscribes are not a good pattern).

I've made some changes but I can't really test anything, so if you have issues let me know. Maybe you can adapt it to work.

import {
    Observable,
    of,
    Subject,
} from 'rxjs';
import {
    switchMap,
    takeUntil,
    tap,
} from 'rxjs/operators';

import {
    Component,
    OnDestroy,
    OnInit,
} from '@angular/core';
import {
    AuthService,
    CmsComponent,
    UserIdService,
} from '@spartacus/core';
import { CmsComponentData } from '@spartacus/storefront';
import { UserAccountFacade } from '@spartacus/user/account/root';

import { PointsRetrievalService } from '../points-retrieval.service';

export interface CustomerPoints {
    points: number;
}


export interface CmsSlpCustomUserPointsComponent4 extends CmsComponent {
    title: string;
    headerText: string;
    footerText: string;
}

const customerPointsKey = 'CUSTOMER_POINTS_KEY';

@Component({
    selector: 'app-userpoints',
    templateUrl: './userpoints.component.html',
    styleUrls: ['./userpoints.component.scss']
})
export class UserpointsComponent implements OnInit, OnDestroy {
    data$: Observable<CmsSlpCustomUserPointsComponent4> = this.component.data$;
    private customerPointsRetrieved = localStorage.getItem(customerPointsKey) ?? 0;
    private _destroyed$ = new Subject<void>();
    constructor(
        public component: CmsComponentData<CmsSlpCustomUserPointsComponent4>, private pointsRetrievalService: PointsRetrievalService,
        protected auth: AuthService,
        protected userAccount: UserAccountFacade,
        protected userIdService: UserIdService,
    ) { }

    ngOnInit(): void {
        this.auth.isUserLoggedIn().pipe(
            switchMap(isUserLoggedIn => {
                if (isUserLoggedIn) {
                    return this.userAccount.get().pipe(
                        switchMap(user => {
                            if (user && user?.customerId) {
                                return this.pointsRetrievalService.getPointsForCustomerId(user.customerId).pipe(
                                    tap(userPoints => {
                                        console.log("New points:" userPoints.points)
                                        console.log(userPoints.points)
                                        this.customerPointsRetrieved = userPoints.points;
                                        localStorage.setItem(customerPointsKey, userPoints.points);
                                    })
                                )
                            }

                            return of(undefined);
                        })
                    )
                }

                return of(undefined);
            }),
            takeUntil(this._destroyed$),
        ).subscribe();
    }

    ngOnDestroy(): void {
        this._destroyed$.next();
    }

    getCustomerPoints() {
        return this.customerPointsRetrieved;
    }

}

<div >
  <p>{{getCustomerPoints()}}</p> 
</div>

CodePudding user response:

You've missed the point of pipe, you can pass any number of arguments, each is a step in the pipeline. The result of a previous step is passed to the next step. You don't subscribe inside the pipeline, you create the pipeline and then subscribe.

So an observable that returns the user points look like this:

  userPoints$: Observable<Points[] | undefined> = this.auth
    .isUserLoggedIn()
    .pipe(
      switchMap((isUserLoggedIn) =>
        isUserLoggedIn ? this.userAccount.get() : of(undefined)
      ),
      switchMap((user: User | undefined) => {
        if (!user || !user.customerId) return of(undefined);
        return this.pointsRetrievalService.getPointsForCustomerId(
          user.customerId
        );
      }),
      map((userPoints: UserPoints | undefined) => userPoints?.points)
    );

Note: switchMap and mergeMap will pass the result of the observable to the next step, not the observable itself. The result of the final step is what ultimately gets returned.

You can add steps by calling pipe on another observable as well. So if you want a separate observable to return just the user you can do this:

  user$: Observable<User | undefined> = this.auth
    .isUserLoggedIn()
    .pipe(
      switchMap((isUserLoggedIn) =>
        isUserLoggedIn ? this.userAccount.get() : of(undefined)
      )
    );

  userPoints$: Observable<Points[] | undefined> = this.user$.pipe(
    switchMap((user: User | undefined) => {
      if (!user || !user.customerId) return of(undefined);
      return this.pointsRetrievalService.getPointsForCustomerId(
        user.customerId
      );
    }),
    map((userPoints: UserPoints | undefined) => userPoints?.points)
  );

You can initialize these outside of any function, nothing will be executed until you subscribe.

You can subscribe to the new observables manually, or you can use the async pipe in html to automatically subscribe / unsubscribe and inject the data into your view.

  ngOnInit(): void {
    // Requests just user
    this.user$.subscribe((user) => {
      console.log(user);
    });

    // Requests user and then user points
    this.userPoints$.subscribe((points) => {
      console.log('New points:'   points);
      this.customerPointsRetrieved = points;
    });
  }

OR

<h1>User</h1>
<pre>{{ user$ | async | json }}</pre>
<h1>User Points</h1>
<pre>{{ userPoints$ | async | json }}</pre>

Notice how | is also a pipe operator, ie. the result of the previous step is passed to the next.

Just keep in mind every instance of userPoints$ | async in html will be a separate request, you can create a template variable if it appears more than once in the view.


Also worth mentioning the async await variant to request the data once instead of subscribing.

  async ngOnInit(): void {
    const user = await firstValueFrom(this.user$);
    const userPoints = await firstValueFrom(this.userPoints$);
    console.log(user, userPoints);
  }
  • Related