javascriptangularwordpress-rest-apiswitchmap

Angular - How to get posts, tags and comment count from Wordpress REST API using switchMap and then combine the resulting values using JavaScript?


I am working on an Angular app that needs to fetch posts, their respective tag names and comment count from Wordpress REST API. With the API three GET requests need to be sent to the server. The first request is being sent to /wp/v2/posts. The response from the server looks like this:

Excerpt of posts request:

[
    {
        "id": 9,
        "title": {
            "rendered": "Hello World! 3"
        },
        "tags": [
            2,
            3,
            4
        ]
    },
    {
        "id": 4,
        "title": {
            "rendered": "Hello World! 2"
        },
        "tags": [
            2,
            3
        ]
    }
    ...
]

The numbers in the tags arrays aren't the actual human-readable tag names, they're just the identifiers for the tags. After we have received the first response from the server, we need to send another GET request to the server to /wp/v2/tags?include=2,3,4 while supplying the tag identifiers with it. We don't need every single tag available in the database, just the ones that were referenced to in the response to the first request. In other words, the second request depends upon the results of the first request. The second request's response from the server looks like this:

Excerpt of tags request:

[
    {
        "id": 8,
        "name": "test"
    },
    {
        "id": 9,
        "name": "test2"
    },
    {
        "id": 30087,
        "name": "test3"
    }
]

The third and final request needs to be sent to /wp/v2/comments?post=3,4,9. The response from the server looks like this:

Excerpt of comments request:

[
   {
      "id":3,
      "post":4,
      "content":{
         "rendered":"<p>This is an example comment number 3.<\/p>\n"
      }
   },
   {
      "id":2,
      "post":9,
      "content":{
         "rendered":"<p>This is an example comment number 2.<\/p>\n"
      }
   }
]

After doing research, I discovered that in Angular RxJS' switchMap operator seems to be the way to go. My requirements are as follows:

then .subscribe() to the overall flow and get a combined object of posts, tags and comments which can then be iterated over in the Angular template file with NgForOf.

My code in Stackblitz so far looks like this. Forks to my code would be greatly appreciated. Upon coding the solution, I experienced a console error I don't fully understand: Cannot read properties of undefined (reading 'map') I need to fix this in order to continue the work.


Solution

  • I experienced a console error I don't fully understand: Cannot read properties of undefined (reading 'map') I need to fix this in order to continue the work.

    The reason for the error is that from within 2nd switchMap, you are only returning comments array.

    // here result is the response of /wp/v2/comments?post=3,4,9
    .pipe(map((result) => ({ result })));
    
    
    .subscribe(({ result }) => {
      // here result would be comments array and hence result.questions is undefined
      // result.questions.map results in trying to read 'map' of undefined
    }
    

    You can overcome the error by returning something as below from map:

    .pipe(map((result) => ({ ...object, comments: result })));
    

    and now within subscribe you can get the result:

    .subscribe(result => {
      // here result will be an object -> {questions: Array[3], tags: Array[3], comments: Array[3]}
      console.log(result);
      ...
    }
    


    Since the call to get tags and comments are independent of each other, you can make use of forkJoin RxJS operator, which basically waits for all the Observables to complete and then combines the result.

    Another approach:

    export class AppComponent {
      constructor(private questionService: QuestionService) {}
    
      // Instead of 'any' you can define your own type as per your use case
      mergedArrays$: Observable<any>;
    
      // This entire method logic can actually be moved within the service class
      getQuestions(): Observable<any> {
        return this.questionService.getQuestions().pipe(
          switchMap((questions: Question[]) => {
            const tagIDs = questions.map((question) => question.tags).join(',');
            const postIDs = questions.map((question) => question.id).join(',');
    
            return forkJoin({
              questions: of(questions),
              tags: this.questionService.getTags(tagIDs),
              comments: this.questionService.getAnswers(postIDs),
            });
          }),
          map(({ questions, tags, comments }) => {
            const mergedArrays = questions.map((question) => {
              return {
                ...question,
                tag_names: question.tags
                  .map((tagId) => tags.find((tag) => tag.id == tagId)?.name)
                  .filter((exists) => !!exists),
                comments: comments.filter((comment) => comment.post === question.id),
              };
            });
            return mergedArrays;
          })
        );
      }
    
      ngOnInit(): void {
        this.mergedArrays$ = this.getQuestions();
      }
    }
    
    <div class="container m-5">
      <h1>All Questions</h1>
      <hr />
      <ul class="list-group list-group-flush">
        <!-- Using async pipe in *ngFor -->
        <li class="list-group-item" *ngFor="let question of mergedArrays$ | async">
          <p>
            <b>{{ question.title.rendered }}</b>
          </p>
          <span>tags: </span>
          <span *ngFor="let tag_name of question.tag_names">
            <a href="#" class="badge bg-secondary">{{ tag_name }}</a>
          </span>
          <!--  Display comments -->
          <div>Comments ({{ question?.comments?.length }}):</div>
          <div *ngFor="let comment of question.comments">
            <span class="" [innerHTML]="comment?.content?.rendered"></span>
          </div>
        </li>
      </ul>
    </div>
    

    You can read more about the two concepts used in the above code: