Why is my RxJS countdown clock not displaying?

两盒软妹~` 提交于 2021-02-04 21:41:54

问题


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.


回答1:


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.




回答2:


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.




来源:https://stackoverflow.com/questions/61855688/why-is-my-rxjs-countdown-clock-not-displaying

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!