2

I have an Observable-Array which contains a list of questions. I would like to display my questions one by one but can't figure out a way to do that.

So far I only managed to display all of them with a *ngFor in my html.

This is my component:

import { Component, OnInit } from '@angular/core';
import { mergeMap, Observable, of, concatAll, Subject, startWith, zip, Subscription } from "rxjs";
import { Question } from "../models/Question";
import { QuestionService } from "../services/question.service";
import { AuthService } from "../services/auth.service";
import { User } from "../models/User";

@Component({
  selector: 'app-play',
  templateUrl: './play.component.html',
  styleUrls: ['./play.component.css']
})
export class PlayComponent implements OnInit {

  user_id: Pick<User, "id"> | undefined
  unanswered_questions$: Observable<Question[]> | undefined
  question$: Observable<Question> | undefined

  constructor(
    private questionService: QuestionService,
    private authService: AuthService
  ) { }

  ngOnInit(): void {
    this.user_id = this.authService.userId
    this.unanswered_questions$ = this.getUnansweredQuestions(this.user_id)
  }

  getUnansweredQuestions(user_id: Pick<User, "id"> | undefined):    Observable<Question[]> {
    return this.questionService.fetchAllUnansweredQuestions(user_id);
  }
}

This is my html:

<mat-card class="question-card" *ngFor="let question of unanswered_questions$ | async">
  <mat-card-header>
    <div mat-card-avatar class="example-header-image"></div>
    <mat-card-title>{{question.title}}</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <h3>{{question.body}}</h3>
  </mat-card-content>
  <mat-card-actions>
    <button mat-button>{{question.answer1}}</button>
    <button mat-button>{{question.answer2}}</button>
    <button mat-raised-button color="accent">Skip<mat-icon>skip_next</mat-icon></button>
  </mat-card-actions>
</mat-card>

I found this Post where someone is trying to do basically the same thing. Unfortunately both answers on that post don't work for me. I figured that they don't work due to the post being 4 years old and me using a newer version of rxjs and angular.

Any help is much appreciated. Thank you!

2
  • 1
    When you say "display one by one" what does this mean? What have you tried so far to achieve the desired functionality? Commented Jun 1, 2022 at 14:10
  • My observable-Array unanswered_questions$ contains multiple questions that I want to show to my user one by one, so the user can press the skip button and the next question will be displayed. So far I have tried both solutions from the post I linked in my question. I've tried to subscribe to my observable and somehow get the data from the observable-array to be saved in a normal observable (question$: Observable<Question> | undefined). Unfortunately this does not work. Commented Jun 1, 2022 at 14:15

1 Answer 1

1

Currently you code "works" because you are piping your observable to the async pipe handler which holds executing the ngFor until the observable is resolved.

I would alter your code so that you subscribe to the observable and handle the ensuing result. Actually, I would first convert to a promise and await it since, IMO, that style is more readable and predictable, especially for a "one and done" event.

So alter your component like so (NOTE: I've left off the imports and decorator for sake of brevity):

export class PlayComponent implements OnInit {

  user_id: Pick<User, "id"> | undefined;
  unanswered_questions: Question[];
  question$: Observable<Question> | undefined;
  questionIndex = 0;

  constructor(
    private questionService: QuestionService,
    private authService: AuthService
  ) { }

  async ngOnInit(): Promise<void> {
    this.user_id = this.authService.userId;
    await this.unanswered_questions = this.getUnansweredQuestions(this.user_id).toPromise();
  }

  getUnansweredQuestions(user_id: Pick<User, "id"> | undefined):    Observable<Question[]> {
    return this.questionService.fetchAllUnansweredQuestions(user_id);
  }

  skipQuestion(): void {
    if (this.questionIndex !== this.getUnansweredQuestions.length) {
     this.questionIndex++;
     }
  }
}

And then your HTML:

<mat-card class="question-card" *ngIf="unanswered_questions">
  <mat-card-header>
    <div mat-card-avatar class="example-header-image"></div>
    <mat-card-title>{{unanswered_questions[questionIndex].title}}</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <h3>{{unanswered_questions[questionIndex].body}}</h3>
  </mat-card-content>
  <mat-card-actions>
    <button mat-button>{{unanswered_questions[questionIndex].answer1}}</button>
    <button mat-button>{{unanswered_questions[questionIndex].answer2}}</button>
    <button mat-raised-button color="accent" (click)="skipQuestion()">Skip<mat-icon>skip_next</mat-icon></button>
  </mat-card-actions>
</mat-card>

By adding ngIf to your matCard you prevent rendering until the observable is resolved.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for your response! This worked perfectly for what I'm trying to do. I just had to use this.unanswered_questions = await lastValueFrom(this.getUnansweredQuestions(this.user_id)); since toPromise() is deprecated, but other than that, this seems like a perfect solution, thank you!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.