javascriptangulartypescriptrxjsangular19

How to update recursive code for Angular 19


I have a recursive JavaScript that I am using in angular 19 to pull data from YouTube. The script is working, but is there a better way to do it?

public getSeriesList() {
  this.record.webTubeSeries = [];
  this._seqNum = 0;
  this.getUrlYouTube()
  .pipe(first())
    .subscribe({
      next: (res: any) => {
        this.parseVideoList(res["items"]);
        if (res['nextPageToken']) {
          let repeatGetNextVideoPage = (_token: string) => {
            this.getNextVideoPage(_token)
              .subscribe(
                (result: any) => {
                  this.parseVideoList(result["items"]);
                  if (result["nextPageToken"]) {
                    repeatGetNextVideoPage(result["nextPageToken"]);
                  }
                }
              ),
              (err: any) => {
                console.log("HTTP Error", err.message)
              }
          }
          repeatGetNextVideoPage(res["nextPageToken"]);
        }
      }
    }
  );
}
private getNextVideoPage(_token: string) {
  let url = (this.urlYouTube + this.videoListId + '&pageToken=' + _token);
  return this.http.get(url);
}
private parseVideoList(result: any) {
  for (let v of result) {
    var item = new WebtubeSeries;
    item.videoTitle = v.snippet.title;
    item.videoId = v.snippet.resourceId.videoId;
    if (v.snippet.thumbnails) {
      item.urlThumbNailDefault = v.snippet.thumbnails.default.url;
      item.urlThumbNailMedium = v.snippet.thumbnails.medium.url;
      item.urlThumbNailHigh = v.snippet.thumbnails.high.url;
      item.widthDefault = v.snippet.thumbnails.default.width.toString();
      item.heightDefault = v.snippet.thumbnails.default.height.toString();

      item.widthMedium = v.snippet.thumbnails.medium.width.toString();
      item.heightMedium = v.snippet.thumbnails.medium.height.toString()

      item.widthHigh = v.snippet.thumbnails.high.width.toString()
      item.heightHigh = v.snippet.thumbnails.high.height.toString();
      item.seqNumber = this._seqNum++;
      //item.id = item.seqNumber;
      this.record.webTubeSeries.push(item);
    }
  }
}

Solution

  • In angular19 they have introduced resource and rxResource which can be used for data handling based on input signals. I am suggesting this approach using rxResource since we can leverage rxjs to perform the recursion.

    Recursively calling an API using RxJS expand operator

    First we define the construction of the class logic inside the class constructor.

    export class WebtubeSeries {
      videoTitle: any;
      urlThumbNailDefault: any;
      urlThumbNailMedium: any;
      videoId: any;
      urlThumbNailHigh: any;
      widthDefault: any;
      heightDefault: any;
      widthMedium: any;
      heightMedium: any;
      widthHigh: any;
      heightHigh: any;
      seqNumber: number;
    
      constructor(v: any, _seqNum = 0) {
        this.seqNumber = _seqNum;
        // this.videoTitle = v.snippet.title;
        // this.videoId = v.snippet.resourceId.videoId;
        // if (v.snippet.thumbnails) {
        //   this.urlThumbNailDefault = v.snippet.thumbnails.default.url;
        //   this.urlThumbNailMedium = v.snippet.thumbnails.medium.url;
        //   this.urlThumbNailHigh = v.snippet.thumbnails.high.url;
        //   this.widthDefault = v.snippet.thumbnails.default.width.toString();
        //   this.heightDefault = v.snippet.thumbnails.default.height.toString();
    
        //   this.widthMedium = v.snippet.thumbnails.medium.width.toString();
        //   this.heightMedium = v.snippet.thumbnails.medium.height.toString();
    
        //   this.widthHigh = v.snippet.thumbnails.high.width.toString();
        //   this.heightHigh = v.snippet.thumbnails.high.height.toString();
        //   this.seqNumber = _seqNum;
        // }
        // this.seqNumber = 0;
      }
    }
    

    Then, we can define the rxResource with a property called loader which makes the API call.

    Inside the loader, we use expand which is used for recursive API calls, we also use takeUntil to stop the sequence, when there is no next API token.

    Finally, we merge the results using reduce and construct the final result.

    youtube = rxResource({
      loader: () => {
        let _seqNum = 0;
        let records: any = [];
        return this.getUrlYouTube().pipe(
          expand((response: any) =>
            this.getNextVideoPage(response['nextPageToken'])
          ),
          takeWhile((response: any) => !!response['nextPageToken'], true),
          reduce((all: any, data: any) => all.concat(data.items), []),
          map((response: any) => {
            console.log(response);
            return this.parseVideoList(response, _seqNum);
          })
        );
      },
    });
    

    Full Code:

    import { Component } from '@angular/core';
    import { rxResource } from '@angular/core/rxjs-interop';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { of, first, EMPTY, expand, takeWhile, tap, reduce, map } from 'rxjs';
    import { CommonModule } from '@angular/common';
    
    export class WebtubeSeries {
      videoTitle: any;
      urlThumbNailDefault: any;
      urlThumbNailMedium: any;
      videoId: any;
      urlThumbNailHigh: any;
      widthDefault: any;
      heightDefault: any;
      widthMedium: any;
      heightMedium: any;
      widthHigh: any;
      heightHigh: any;
      seqNumber: number;
    
      constructor(v: any, _seqNum = 0) {
        this.seqNumber = _seqNum;
        // this.videoTitle = v.snippet.title;
        // this.videoId = v.snippet.resourceId.videoId;
        // if (v.snippet.thumbnails) {
        //   this.urlThumbNailDefault = v.snippet.thumbnails.default.url;
        //   this.urlThumbNailMedium = v.snippet.thumbnails.medium.url;
        //   this.urlThumbNailHigh = v.snippet.thumbnails.high.url;
        //   this.widthDefault = v.snippet.thumbnails.default.width.toString();
        //   this.heightDefault = v.snippet.thumbnails.default.height.toString();
    
        //   this.widthMedium = v.snippet.thumbnails.medium.width.toString();
        //   this.heightMedium = v.snippet.thumbnails.medium.height.toString();
    
        //   this.widthHigh = v.snippet.thumbnails.high.width.toString();
        //   this.heightHigh = v.snippet.thumbnails.high.height.toString();
        //   this.seqNumber = _seqNum;
        // }
        // this.seqNumber = 0;
      }
    }
    
    @Component({
      selector: 'app-root',
      imports: [CommonModule],
      template: `
        {{youtube.value() | json}}
      `,
    })
    export class App {
      a = 0;
      record: any = {
        webTubeSeries: [],
      };
      getUrlYouTube() {
        this.a++;
        const randon = Math.random();
        return of({
          items: [0, 0, 0, 0, 0],
          nextPageToken: randon,
        });
      }
      youtube = rxResource({
        loader: () => {
          let _seqNum = 0;
          let records: any = [];
          return this.getUrlYouTube().pipe(
            expand((response: any) =>
              this.getNextVideoPage(response['nextPageToken'])
            ),
            takeWhile((response: any) => !!response['nextPageToken'], true),
            reduce((all: any, data: any) => all.concat(data.items), []),
            map((response: any) => {
              console.log(response);
              return this.parseVideoList(response, _seqNum);
            })
          );
        },
      });
    
      private parseVideoList(response: any, _seqNum: number) {
        const result = [];
        for (let v of response) {
          console.log(v);
          _seqNum++;
          // if (v.snippet.thumbnails) {
          result.push(new WebtubeSeries(v, _seqNum));
          // }
        }
        return result;
      }
    
      private getNextVideoPage(_token: string) {
        this.a++;
        const randon = Math.random();
        return of({
          items: [1, 2, 3, 4, 5],
          nextPageToken: this.a > 10 ? null : randon,
        });
        // let url = this.urlYouTube + this.videoListId + '&pageToken=' + _token;
        // return this.http.get(url);
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo