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 *ngFor="let question of unanswered_questions$ | async">
<mat-card-header>
<div mat-card-avatar ></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!
CodePudding user response:
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 *ngIf="unanswered_questions">
<mat-card-header>
<div mat-card-avatar ></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.
CodePudding user response:
You may do that with a different workaround. Just define a variable currentQuestionNumber =1
;
and then check if this variable matches index 1 in HTML
<mat-card
*ngFor="let question of unanswered_questions$ | async; let i = index"
>
<ng-container *ngIf="currentQuestionNumber === i 1">
<mat-card >
<mat-card-header>
<div mat-card-avatar ></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>
</ng-container>
</mat-card>
and then add click handler on skip and next buttons to increment or decrement currentQuestionNumber
by one on each click.