0
votes

I am working on an Angular 9 quiz app and I'm using RxJS for the countdown timer (in containers\scoreboard\time\time.component.ts) and the timer doesn't seem to be displaying. The stopTimer() function should stop the timer on the number of seconds on which it is stopped. The timer should stop after the correct answer(s) are selected and the timer should reset in between questions. The time elapsed per question should be saved into the elapsedTimes array. Please see my code for the timer in the TimeComponent on Stackblitz: https://stackblitz.com/edit/angular-9-quiz-app. Thank you.

The code below is my first start with building a countdown clock with RxJS. My most recent code is on Stackblitz.

  countdownClock() {
    this.timer = interval(1000)
      .pipe(
        takeUntil(this.isPause),
        takeUntil(this.isStop)
      );
    this.timerObserver = {
      next: (_: number) => {
        this.timePerQuestion -= 1;
          ...
        }
    };

    this.timer.subscribe(this.timerObserver);
  }

  goOn() {
    this.timer.subscribe(this.timerObserver);
  }

  pauseTimer() {
    this.isPause.next();
    // setTimeout(() => this.goOn(), 1000)
  }

  stopTimer() {
    this.timePerQuestion = 0;
    this.isStop.next();
  }

I use stopTimer() and pauseTimer() in my TimerService so I can call them from a different component.

2
Put a minimal reproducible example in the question. - jonrsharpe
can you write in words what your requirements are? - bryan60
Yes I would like the countdown clock to decrease from 20 seconds to 0 seconds. When the correct answer(s) is/are selected, the clock should stop on the amount of seconds left and the elapsed time per question should be saved in the elapsedTimes array for calculating total completion time later. For each following question the clock should reset to 20 seconds. The user's answers for each question and the time elapsed should be stored in its own array (using the Result interface) and passed to the ResultsComponent. - integral100x
Stuck getting the countdown clock to work. Can someone help? - integral100x
fixed your stackblitz by forking a new one from yours. find my answer below. - Aakash Garg

2 Answers

5
votes

As with every complex problem, you have to break it into smaller, digestible problems.

Therefore, I created a StackBlitz demo that recreates the basic functionalities:

  • start a timer
  • stop temporarily (mark timestamp); for example when all answers would be selected
  • continue from the last timestamp
  • reset timer
  • stop timer

Here's the code for this:

const $ = document.querySelector.bind(document);

const start$ = fromEvent($('#start'), 'click').pipe(shareReplay(1));
const reset$ = fromEvent($('#reset'), 'click');
const stop$ = fromEvent($('#stop'), 'click');
const markTimestamp$ = fromEvent($('#mark'), 'click');
const continueFromLastTimestamp$ = fromEvent($('#continue'), 'click');

const src$ = concat(
  start$.pipe(first()),
  reset$
).pipe(
  switchMapTo(
    timer(0, 1000)
      .pipe(
        takeUntil(markTimestamp$),
        repeatWhen(
          completeSbj => completeSbj.pipe(switchMapTo(
            continueFromLastTimestamp$.pipe(first())
          ))
        ),
        scan((acc, crt) => acc + 1000, 0)
      )
  ),
  takeUntil(stop$),
  repeatWhen(completeSbj => completeSbj.pipe(switchMapTo(start$.pipe(skip(1), first()))))
).subscribe(console.log)

Let's go through each relevant part.


concat(
  start$.pipe(first()),
  reset$
).pipe(switchMapTo(timer(...)))

The timer will start to count from 0 only if it hasn't started before(start$.pipe(first())) or the user wants to reset everything(reset$).

concat(a$, b$) makes sure that that b$ can't emit unless a$ completes.


timer(0, 1000)
  .pipe(
    takeUntil(markTimestamp$),
    repeatWhen(
      completeSbj => completeSbj.pipe(switchMapTo(
        continueFromLastTimestamp$.pipe(first())
      ))
    ),
    scan((acc, crt) => acc + 1000, 0)
  )

We want the timer to be active until markTimestamp$ emits. When this happens, the source(timer(0, 1000)) will be unsubscribed. With repeatWhen we can decide when should the timer be resubscribed. That is, when continueFromLastTimestamp$.pipe(first()) emits. It's important that we use first(), otherwise the source might be re-subscribed multiple times.

Placing scan((acc, crt) => acc + 1000, 0) after repeatWhen ensures that the last timestamp won't be lost. For example, the timer might start at X, at X+5 the user triggers markTimestamp$ and then at X + 100 the user triggers continueFromLastTimestamp$. What this happens, the timer will emit X+6.


takeUntil(stop$),
repeatWhen(completeSbj => completeSbj.pipe(switchMapTo(start$.pipe(skip(1), first()))))

The timer should be active until stop$ emits. Then, it can restart only if the user triggers start$ again. skip(1) is used because we don't the cached value by the ReplaySubject used by start$'s shareReplay and first() is used because the source should be re-subscribed only once.


2
votes

i have fixed your stackblitz. Based on your call of timerservice methods, your timer will behave

And no need to set elapsed time in teardown logic. it will automatically push whenver timer will stop.

Stackblitz Url :- https://stackblitz.com/edit/angular-9-quiz-app-er3pjn

Time countdown method :-

  countdownClock() {
    const $ = document.querySelector.bind(document);
    const start$ = this.timerService.isStart.asObservable().pipe(shareReplay(1));
    const reset$ = this.timerService.isReset.asObservable();
    const stop$ = this.timerService.isStop.asObservable();
    const markTimestamp$ = fromEvent($("#mark"), "click");
    const continueFromLastTimestamp$ = fromEvent($("#continue"), "click");
    start$.subscribe((data) => console.log(data));
    this.timeLeft$ = concat(start$.pipe(first()), reset$).pipe(
      switchMapTo(
        timer(0, 1000).pipe(
          scan((acc, crt) => acc > 0? acc - 1: acc, this.timePerQuestion),
        )
      ),
      takeUntil(stop$.pipe(skip(1))),
      repeatWhen(completeSbj =>
        completeSbj.pipe(
          switchMapTo(
            start$.pipe(
              skip(1),
              first()
            )
          )
        )
      )
    ).pipe(tap((value)=>this.timerService.setElapsed(this.timePerQuestion-value)));

In scoreboard component params subscribe i have reset the timer, so it will reset whenever question will change.