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