1

Goal: I have a quiz component and I want to be able to show one question at a time in my template. The user can show the next question when they click the Next button.

Problem: I have a FirebaseListObservable that contains the entire list of questions. How can I render only one at a time to my template. I pasted below what I have for code so far. It renders the entire list. I don't know where to go from here, partly due to my beginner's level of RxJS knowledge.

import { Component, OnInit } from '@angular/core';
import { FirebaseService } from '../../firebase.service';
import { Observable } from 'rxjs/Observable';
import { Question } from '../../model/question';

@Component({
  selector: 'app-quiz',
  template: `
    <md-grid-list cols="1" rowHeight="2:1">
      <md-grid-tile>
        <md-card *ngFor="let question of questions$ | async">
          <md-card-header>
            <md-card-title>{{question?.course}}</md-card-title>
            <md-card-subtitle>{{question?.chapter}}</md-card-subtitle>
          </md-card-header>
          <md-card-content>
            <p>{{question?.question}}</p>
          </md-card-content>
          <md-card-actions>
            <button md-button>See Answer</button>
            <button (click)="nextQuestion(question)" md-button>Next 
Question</button>            
          </md-card-actions>
        </md-card>
      </md-grid-tile>
    </md-grid-list> 
  `,
  styles: [`.example-card { width: 400px;}`]
})
export class QuizComponent implements OnInit {

  questions$: Observable<Question[]>;

  constructor(private fbDatabase: FirebaseService) { }

  ngOnInit() {
    this.questions$ = this.fbDatabase.getFirebaseList('/questions');
  }

  nextQuestion() {
  }

}
1
  • just updated my answer. Let me know if that works Commented Sep 4, 2017 at 3:41

2 Answers 2

1

First of all, I'd make use of the component pattern by creating a component to display one question:

import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';

export interface IQuestion {
  label: string;
  answers: string[];
}

@Component({
  selector: 'app-question',
  template: `
    <div>
      <b>Question:</b>
      <p>{{ question.label }}</p>

      <b>Possible answers</b>
      <p *ngFor="let answer of question.answers">{{ answer }}</p>      
    </div>
  `,
  styles: [``],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppQuestionComponent {
  @Input() question: IQuestion;
}

Then, the AppComponent's code and its comments are enough to understand I think:
TS code

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  question$: Observable<IQuestion>;
  questions$: Observable<IQuestion[]>;

  _click$ = new Subject<void>();
  click$ = this._click$.startWith(null);

  constructor(private firebaseService: FirebaseService) { }

  ngOnInit() {
    // get the questions from firebase
    this.questions$ = this
      .firebaseService
      .getFirebaseList('your-list')
      // add a false statement so we know when to ends
      .map(questions => [...questions, false]);

    const questionsOneByOne$ = this
      .questions$
      .mergeMap(
        questions =>
          questions.map(
            // explode each question to a different stream value...
            question => Observable.of(question)
          )
      )
      // ...so we can get them one by one
      .concatAll();

    this.question$ = Observable
      .zip(questionsOneByOne$, this.click$)
      .map(([question, _]) => question);
  }

  nextQuestion() {
    this._click$.next();
  }
}

HTML code

<div *ngIf="question$ | async as question; else noMoreQuestions">
  <app-question [question]="question"></app-question>

  <button (click)="nextQuestion()">Go to next question</button>
</div>

<ng-template #noMoreQuestions>
  No more questions
</ng-template>

Here's a live demo on Stackblitz (with a mocked firebase list)
https://stackblitz.com/edit/angular-mbnscx

Let me know if you have further question(s) ;)

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

Comments

0

You should just render each item (question object) of your question array at a time if you don't want to display previous questions. I would add a new observable variable type Question for rendering to the view. An index variable to keep track of the item in array. Something like:

@Component({
      selector: 'app-quiz',
      template: `
        <md-grid-list cols="1" rowHeight="2:1">
          <md-grid-tile>
            <md-card *ngIf="question | async">
              <md-card-header>
                <md-card-title>{{(question | async)?.course}}</md-card-title>
                <md-card-subtitle>{{(question | async)?.chapter}}</md-card-subtitle>
              </md-card-header>
              <md-card-content>
                <p>{{(question | async)?.question}}</p>
              </md-card-content>
              <md-card-actions>
                <button md-button>See Answer</button>
                <button (click)="nextQuestion(index + 1)" md-button>Next 
    Question</button>            
              </md-card-actions>
            </md-card>
          </md-grid-tile>
        </md-grid-list> 
      `,
      styles: [`.example-card { width: 400px;}`]
    })

    export class QuizComponent implements OnInit {

      questions: Array<Question>;

      question: Observable<Question>;
      index: number = 0;

      constructor(private fbDatabase: FirebaseService) { }

      ngOnInit() {
          this.fbDatabase.getFirebaseList('/questions').subscribe((res) =>{
              if(res) {
                console.log(res);
                this.questions = res;
                this.question = this.nextQuestion(index);
              }
          });

      }

      nextQuestion(i: number): Observable<Question> {
          if i >= this.questions.length {
            i = 0;
          }

          return Observable.of(this.questions[i]);
      }

    }

Let me know if that works for you?

6 Comments

My template shows up blank but there is no errors in console. I added console.log(this.questions, this.question) to the top of nextQuestion function and found out that the array is initialized but Observable is undefined until I first click Next. After clicking, my md-card is still blank. The console, however, now says Scalar Observable. If I keep clicking next, I keep getting the same observable back. The first one in my list.
@LandonWaldner I just updated answer. I added ngIf to make sure that we have data back from firebase before rendering it, and also correcting async in the template (If it works, you can remove either *ngIf or async from question object. Also, I added a console.log to see data coming back from firebase. If this solution not working, please send me the console.log output as I don't know about your data structure.
ngOnInit() { this.index = 0; this.fbDatabase.getFirebaseList('/questions').subscribe( (res) => { if (res) { this.questions = res; this.nextQuestion(this.index); } } ); } nextQuestion(i: number) { if (i >= this.questions.length) { this.index = i = 0; } this.question = Observable.of(this.questions[i]); this.index++; }
Oops. I was able to get it to working by tweaking the above code a little. I appreciate all of your help. Especially the idea of casting to a regular Observable. I tried to upvote but I have less than 15 reputation so it didn't count.
Great to hear that it works. Even better when you know how to tweak it. If you think my answer helps, then tick it as correct answer. If you are proud of your tweaking, just post your own answer so that others can learn.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.