Exploring Child Records with Angular4 and RxJS Observables

I am currently in the process of developing a frontend for a blog. My main objective is to retrieve an array of posts from the API, then make additional API calls for each post to fetch the featured image objects and attach them to their respective parent Post objects. Finally, I aim to return the complete array of Post objects. I have encountered challenges in determining the most effective approach using Observables.

This is what I have accomplished thus far:

export class PostService {
private apiAddress: string = '/wp-json/wp/v2/posts';
private logger: Logger;

constructor (private httpService: HttpService, private loggerService: LoggerService) {
    this.logger = loggerService.newLogger('PostService');
}

getPosts(category: Category, page: number = 1, pageSize: number = 10): Observable<Post[]> {
    this.logger.log(`getPosts(id:${category.id}, ${page}, ${pageSize}) returned`);
    return this.httpService.get(`${this.apiAddress}?categories=${category.id}&page=${page}&per_page=${pageSize}&orderby=date&order=desc`).map<Response, Post[]>((res) => {
        let posts:Post[] = [];

        let body = res.json();
        for (var i = 0; i < body.length; i++) {
            posts.push(new Post(body[i]));
        }

        this.logger.log(`getPosts(id:${category.id}, ${page}, ${pageSize}) returned ${posts.length} posts`);
        return posts;
    });
}
}

This is my vision of what I would like to achieve:

export class PostService {
private apiAddress: string = '/wp-json/wp/v2/posts';
private logger: Logger;

constructor (private httpService: HttpService, private loggerService: LoggerService) {
    this.logger = loggerService.newLogger('PostService');
}

getPosts(category: Category, page: number = 1, pageSize: number = 10): Observable<Post[]> {
    this.logger.log(`getPosts(id:${category.id}, ${page}, ${pageSize}) returned`);
    return this.httpService.get(`${this.apiAddress}?categories=${category.id}&page=${page}&per_page=${pageSize}&orderby=date&order=desc`).map<Response, Post[]>((res) => {
        let posts:Post[] = [];

        let body = res.json();
        for (var i = 0; i < body.length; i++) {
            posts.push(new Post(body[i]));
        }

        this.logger.log(`getPosts(id:${category.id}, ${page}, ${pageSize}) returned ${posts.length} posts`);
        return posts;
    }).forEach((post, done) => {
        this.httpService.get(`/wp-json/wp/v2/media/${post.featured_media}`).map<Request, Media>((res) => {
            let body = res.json();
            post.featured_image = new Media(body);
            done(post);
        });
    });
}
}

I recognize that my understanding of Observables may be flawed. Any feedback would be greatly appreciated. Thank you!

Answer №1

To successfully execute the second request, you must first convert the response from the initial request into a stream.

It is not advisable to mix reactive programming with for loops; it is considered bad practice.

Your code should follow this structure:

url = `${this.apiAddress}?categories=${category.id}
        &page=${page}&per_page=${pageSize}
        &orderby=date&order=desc`
mediaUrl = `/wp-json/wp/v2/media/${post.featured_media}`;

getPosts(category: Category, 
         page: number = 1, 
         pageSize: number = 10): Observable<Post[]> {
    return this.httpService.get(url)
      .map(res => res.json())
      .flatMap(posts => Observable.from(posts))
      .map(post => new Post(post))
      .flatMap(post=> 
         this.httpService.get(mediaUrl)
             .map(res=>res.json())
             .map(image=> Object.assign(post, {
                    featured_image: new Media(body) 
                   }) 
              )
      ).toArray();

}    

Prior to explaining the code implementation, here's a debugging tip: if uncertain about the operations between two operators, utilize the do function.

...
.map(res => res.json())
.do(data=> console.log(data))
.flatMap(
...

The do function logs data in the console during execution and proceeds accordingly. The step-by-step breakdown of what I'm doing is as follows:

.map(res => res.json()) 

This line converts the stream containing the response into a stream of JSON data.

.flatMap(posts => Observable.from(posts))

As each stream contains an array, this conversion ensures that each item is processed individually rather than as part of an array.

.map(post => new Post(post))

This step involves converting each raw object into a post object.

.flatMap(post=> 

For each object, we make another HTTP call using flatMap, retrieve the response, modify it, then add the modified object back to the stream.

.map(image=> Object.assign(post, {
                featured_image: new Media(body) 
               }) 
          )

Here, we take the media response and update the post object by adding a new parameter, featured_image.

Answer №2

It appears that you are currently making two separate HTTP requests - one to retrieve posts and another to fetch feature media.

Instead of sending multiple requests, consider modifying your backend to return data in a structured format like this:

postdata : [
  {postid:1, postname:"something else", 
      children:[
            {childid :1, childname:first child item},
            {childid :2, childname:second child item}
          ]
    },  //this is a post with children items in it

  {postid :2, postname: "numbsertwo",
         children:[
                   ...return like in the above
                 ]

      }

If you follow this approach, you can easily iterate through the data using the following method:

 return this.httpService.get(`$urltoget`).map<Response, Post[]>((res) => {
    let posts:Post[] = [];

    let body = res.json();
    for (var i = 0; i < body.length; i++) {
        posts.push(new Post(body[i]));

       //here loop through the children

    }

    return posts;
})

Answer №3

I would go about it like this

fetchPosts(category: Category, pageNumber: number = 1, itemsPerPage: number = 10) {
    this.logger.log(`Fetching posts for category ID:${category.id}, page:${pageNumber}, pageSize:${itemsPerPage}`);
    return this.httpService.get(`${this.apiAddress}?categories=${category.id}&page=${pageNumber}&per_page=${itemsPerPage}&orderby=date&order=desc`).map((res) => {
        let posts:Post[] = [];

        let responseBody = res.json();
        for (var index = 0; index < body.length; index++) {
            posts.push(new Post(body[index]));
        }

        this.logger.log(`Fetched ${posts.length} posts for category ID:${category.id}, page:${pageNumber}, pageSize:${itemsPerPage}`);
        return posts;
    }).
    switchMap(posts => {
        const imageRequests = new Array<Observable>();
        forEach(post => {
            imageRequests.push(this.httpService.get(`/wp-json/wp/v2/media/${post.featured_media}`).map(res => {
                   let responseBody = res.json();
                   post.featured_image = new Media(responseBody);
                }
             ));
        })
        return Observable.combineLatest(imageRequests).map(() => posts);
    })
}

The approach involves initiating an initial http request to fetch the posts and then creating an array of additional http requests, one for each Post, in order to retrieve their respective images. Each http request is represented as an Observable, forming an Array of Observables.

You can merge an Array of Observables into a single Observable that emits only when all Observables have emitted using the combineLatest operator.

The final map operator ensures that the subscriber receives the array of Posts generated from the initial http call.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Attempting to bind an input parameter using NgStyle in Angular version 2 and above

Issue: I am in need of a single component (spacer) with a width of 100% and a customizable height that can be specified wherever it is used in the HTML (specifically in home.html for testing): number 1 <spacer height="'200px'"></spa ...

Adjust the color of the entire modal

I'm working with a react native modal and encountering an issue where the backgroundColor I apply is only showing at the top of the modal. How can I ensure that the color fills the entire modal view? Any suggestions on how to fix this problem and mak ...

The type 'Promise<any>' cannot be assigned to the type 'Contact[]'

After exhausting all my resources on the internet and StackOverflow, I still couldn't find the right answer. This is my first project in Angular with MongoDB, but I keep encountering this error: "Type 'Promise' is not assignable to type &apo ...

JavaScript - Imported function yields varied outcome from module

I have a utility function in my codebase that helps parse URL query parameters, and it is located within my `utils` package. Here is the code snippet for the function: export function urlQueryParamParser(params: URLSearchParams) { const output:any = {}; ...

What is the process for modifying href generation in UI-Router for Angular 2?

I am currently working on implementing subdomain support for UI-Router. Consider the following routes with a custom attribute 'domain': { name: 'mainhome', url: '/', domain: 'example.com', component: 'MainSiteC ...

Error in Typescript cloud function: response object is not callable

My cloud function is structured like this: // const {onSchedule} = require("firebase-functions/v2/scheduler"); // The Cloud Functions for Firebase SDK to set up triggers and logging. import * as logger from "firebase-functions/logger&quo ...

Error encountered when trying to access Proxy Object or Object[] in MobX with TypeScript

In my MobX Store, I have a straightforward structure: import { observable, action, makeObservable } from "mobx" import { BoxShadow, ShadowValues } from "./types" import { boxShadow } from "./constants" interface IStore { ...

Organizing JSON keys based on their values using Typescript

In the context of a main JSON structure represented below, I am interested in creating two separate JSONs based on the ID and Hobby values. x = [ {id: "1", hobby: "videogames"}, {id: "1", hobby: "chess"}, {id: "2", hobby: "chess ...

Oops! ExcelJs is throwing an error: "Unable to merge cells that are already

Currently, I'm attempting to create an Excel sheet using excelJs. Each order has an array of order items, which could be one or more. My goal is to avoid repeating certain information within the order while iterating through each item in the order. Ho ...

Using static typing in Visual Studio for Angular HTML

Is there a tool that can validate HTML and provide intellisense similar to TypeScript? I'm looking for something that can detect errors when using non-existent directives or undeclared scope properties, similar to how TypeScript handles compilation er ...

Attaching an event listener to elements with a specified Class name

Currently facing a challenge where I am trying to implement a function that captures click events on squares. The objective is to capture the click event on every button with the square class. import { Component, OnInit } from '@angular/core&apos ...

What steps can I take to avoid encountering the `@typescript-eslint/unbound-method` error while utilizing the `useFormikContent()` function?

Recently, I updated some of the @typescript-eslint modules to their latest versions: "@typescript-eslint/eslint-plugin": "3.4.0", "@typescript-eslint/parser": "3.4.0", After the update, I started encountering the fo ...

Troubleshooting spacing problems in Angular Material tables

I'm attempting to utilize an Angular Material table in the following way: <div class="flex flex-col pb-4 bg-card rounded-2xl shadow overflow-hidden"> <table mat-table class="" [dataSource]="$candidates ...

Maintaining Component State with ngIf in Angular 2

I have a webpage featuring a tab control that occupies a portion of the screen. The tabs are displayed or hidden using *ngIf and checked against an enum to determine which one is selected. As a result, the components are destroyed and created each time the ...

Uncaught Error: Unable to access 'visit' property of null

Sharing this in case it helps someone else who encounters the same error, as the stack trace doesn't offer any clues on what might have caused the problem. ...

Include a checkbox within a cell of a table while utilizing ngFor to iterate through a two-dimensional array

I am working with a two-dimensional array. var arr = [ { ID: 1, Name: "foo", Email: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f5939a9ab5939a9adb969a98">[email protected]</a>", isChecked: "true" ...

What is the best way to link a specific version of the @types package to its corresponding JavaScript package version?

Currently, I am in the process of working on a nodejs project with typescript 2.2 that relies on node version 6.3.1. My goal is to transition from using typings to utilizing @types. However, during this migration, several questions have arisen regarding th ...

Issue encountered in Angular 16: angular-slickgrid is reporting that property 'detail' is not found on type 'Event'

Currently, I am working on integrating angular-slickgrid v6 with Angular 16. In my code snippet, I am using (onClick)="onCellClicked($event.detail.eventData, $event.detail.args)" to retrieve the selected row. However, I encountered an error message stating ...

(Angular4 / MEAN) Making a Request to Local API Yields an Empty Response Body

I'm attempting to send data to an Items API, like this: "data": { "title": "stack", "notes": "asdsad", "time": "19:02", "meridian": "PM", "type": "Education", "_id": "5a2f02d3bba3640337bc92c9", ...

The constant value being brought in from an internal npm package cannot be determined

I have developed an internal npm package containing shared types and constants. My project is built using TypeScript with "target": "ESNext" and "module": "NodeNext". Within one of my files, I define: export type Su ...