How to make countdown timer with RxJS Observables?
Asked Answered
H

12

26

I'm struggling to create a countdown timer using Observables, the examples at http://reactivex.io/documentation/operators/timer.html do not seem to work. In this specific example the error related to timerInterval not being a function of the Observable returned from timer.

I have also been experimenting with other approaches and the best I've come up with is:

Observable.interval(1000).take(10).subscribe(x => console.log(x));

The problem here is it counts up from 0 to 10 and I want a countdown timer e.g. 10,9,8...0.

I've also tried this but the timer does not exist for type Observable

Observable.range(10, 0).timer(1000).subscribe(x => console.log(x));

As well as, which produces no output at all.

Observable.range(10, 0).debounceTime(1000).subscribe(x => console.log(x));

To clarify I need help with ReactiveX's RxJS implementation, not the MircoSoft version.

Hilleary answered 21/1, 2016 at 10:45 Comment(1)
remember any timer that just keeps calling timer(1000) will drift over time. Fine for short periods of time, but not if you're programming a clock! If you need accuracy you need to use your system clock for calculating the time offset .Placate
D
31

You were on the right track - your problem was that timer does not exist on the prototype (and thereby on Observable.range())but on Observable (see the RxJS docs). I.e. jsbin

const start = 10;
Rx.Observable
  .timer(100, 100) // timer(firstValueDelay, intervalBetweenValues)
  .map(i => start - i)
  .take(start + 1)
  .subscribe(i => console.log(i));

// or
Rx.Observable
  .range(0, start + 1)
  .map(i => start - i)
  .subscribe(i => console.log(i));
Diacid answered 21/1, 2016 at 11:15 Comment(4)
Thanks for your suggestion. It does work, it just feels like there should be simpler ways to do this with Observables. Ideally an iterator operator that allows a count down rather than range(start, count) which only increments.Hilleary
Hopefully someone else can provide a way. Until then: Have you considered extending the prototype of Observable to hide the implementation (e.g. like this)?Diacid
There is an operator that does this, generate it just hasn't been added to the new project yet.Cutin
I saw the generate function in v4, I'm surprised it's not in v5. It would offer me the functionality I require.Hilleary
M
17

Using timer, scan and takeWhile if you don't want to depend on a variable for your starting time, the 3rd argument in scan is the starting number

timer$ = timer(0, 1000).pipe(
  scan(acc => --acc, 120),
  takeWhile(x => x >= 0)
);

Check it out on Stackblitz

Mohenjodaro answered 28/11, 2019 at 19:24 Comment(1)
This timer will end one second late because it checks -1 and only then completes. Using take(120) instead of takeWhile() will work like a charm.Carry
S
6

With interval, allows you to specify how long a second is

const time = 5 // 5 seconds
var timer$ = Rx.Observable.interval(1000) // 1000 = 1 second
timer$
  .take(time)
  .map((v)=>(time-1)-v) // to reach zero
  .subscribe((v)=>console.log('Countdown', v))
Sender answered 2/2, 2018 at 21:55 Comment(0)
M
2

I am the take...() lover, so I am using takeWhile() as follow ( RxJS 6.x.x, in ES6 way )

import {timer} from 'rxjs';
import {takeWhile, tap} from 'rxjs/operators';


let counter = 10;
timer(1000, 1000) //Initial delay 1 seconds and interval countdown also 1 second
  .pipe(
    takeWhile( () => counter > 0 ),
    tap(() => counter--)
  )
  .subscribe( () => {
    console.log(counter);
  } );
Magpie answered 28/8, 2019 at 13:48 Comment(0)
C
2

This example worked for me :)

By the way, using takeWhile(val => val >= 0) instead of take(someNumber) might make sense but it will check -1 and only then complete.. 1 second too late.

The example below will emit 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0. Starting at 10 and ending at 0 immediately seems trivial but it was rather tricky for me.

const counter$ = interval(1000); // rxjs creation operator - will fire every second
const numberOfSeconds = 10;
counter$.pipe(
    scan((accumulator, _current) =>  accumulator - 1, numberOfSeconds + 1),
    take(numberOfSeconds + 1),

    // optional
    finalize(() => console.log(
      'something to do, if you want to, when 0 is emitted and the observable completes'
    ))
)

This will do the same:


counter$.pipe(
    scan((accumulator, current) => accumulator - 1, numberOfSeconds),
    startWith(numberOfSeconds), // scan will not run the first time!
    take(numberOfSeconds + 1),

    // optional
    finalize(() => console.log(
      'something to do, if you want to, when 0 is emitted and the observable completes'
    ))
  )

You could, of course, make many changes.. you can mapTo(-1) before the scan for example, and then write accumulator + current and the current will be -1.

Carry answered 3/1, 2021 at 16:42 Comment(3)
Hi just wanted to understand this. in first code snippet why you are using numberOfSeconds + 1 inside the scan and takeTetracycline
The entire code takes 10 seconds but it has 11 rounds. In the written examples it's always take(11) because it fires 11 times - I want the latest value to be zero. 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0. In my case I printed all the values in my UI, the user clicked the timer and saw 10 . . . . all the way till zero and including zero.Carry
In the second example the startWith() fires the number 10, so I didn't have to write numberOfSeconds + 1 in the accumulator when it starts. And then the rest of the code fires 9, 8, 7, 6, 5, 4, 3, 2, 1, 0Carry
M
1

My counterdown function with display time:

import { Observable, timer, of, interval } from "rxjs";
import { map, takeWhile, take } from "rxjs/operators";

function countdown(minutes: number, delay: number = 0) {
   return new Observable<{ display: string; minutes: number; seconds: number }>(
      subscriber => {
        timer(delay, 1000)
          .pipe(take(minutes * 60))
          .pipe(map(v => minutes * 60 - 1 - v))
          .pipe(takeWhile(x => x >= 0))
          .subscribe(countdown => { // countdown => seconds
            const minutes = Math.floor(countdown / 60);
            const seconds = countdown - minutes * 60;

            subscriber.next({
              display: `${("0" + minutes.toString()).slice(-2)}:${("0" + seconds.toString()).slice(-2)}`,
              minutes,
              seconds
            });

            if (seconds <= 0 && minutes <= 0) {
              subscriber.complete();
            }
       });
   });
}

countdown(2).subscribe(next => {
  document.body.innerHTML = `<pre><code>${JSON.stringify(next, null, 4)}</code></pre>`;
});

Output i.e:

{
   "display": "01:56",
   "minutes": 1,
   "seconds": 56
}
Mede answered 20/12, 2019 at 19:5 Comment(1)
D
1

This is the simplest approach imho:

import { interval } from 'rxjs'
import { map, take } from 'rxjs/operators'

const durationInSeconds = 1 * 60 // 1 minute

interval(1000).pipe(take(durationInSeconds), map(count => durationInSeconds - count)).subscribe(countdown => {
  const hrs  = (~~(countdown / 3600)).toString()
  const mins = (~~((countdown % 3600) / 60)).toString()
  const secs = (~~countdown % 60).toString()
  console.log(`${hrs.padStart(2, '0')}:${mins.padStart(2, '0')}:${secs.padStart(2, '0')}`);
})
Deserved answered 21/12, 2021 at 23:5 Comment(0)
S
1

As if there were not enough proposed solutions, I will add one more. It is a re-usable function with parameters to configure your countdown options, and it's written in TypeScript:

import { filter, interval, map, Observable, startWith, take } from "rxjs";

interface CountdownOptions {
    tickDuration?: number;
    emitZero?: boolean;
    emitInitial?: boolean;
}

/**
 * Creates an observable which emits a decremented value on every tick until it reaches zero.
 * @param ticks - the amount of ticks to count down
 * @param options (object):
 *          tickDuration - the duration of one tick in milliseconds (1000 by default)
 *          emitZero - whether to emit 0 at the end of the countdown, immediately before the observable completes (true by default)
 *          emitInitial - whether to emit initial countdown value immediately after the observable is created (true by default)
 */
function countdown(ticks: number, options: CountdownOptions = {}): Observable<number> {
    const tickDuration = options.tickDuration ?? 1000;
    const emitZero = options.emitZero ?? true;
    const emitInitial = options.emitInitial ?? true;

    const countdown$ = interval(tickDuration).pipe(
        take(ticks),
        map(n => ticks - 1 - n),
        filter(value => emitZero || value !== 0)
    );

    if (emitInitial)
        return countdown$.pipe(startWith(ticks));
    else
        return countdown$;
}

// SAMPLE USAGE

// starts immediately, logs values from 10 to 0, and completes immediately on 0
countdown(10).subscribe({
    next: x => console.log(`Launching in ${x} seconds...`),
    complete: () => console.log(`Launched!`)
});

// starts after 1 second, logs values from 9 to 1, and completes on 0 without emitting it
countdown(10, { emitZero: false, emitInitial: false}).subscribe({
    next: x => console.log(`Please wait ${x} seconds...`),
    complete: () => console.log(`Ready!`)
});
Subacid answered 17/8, 2022 at 18:24 Comment(0)
S
0

I also needed an interval that counts backward, so I tried this solution:

const { interval, take } = rxjs

const countDownEl = document.querySelector("#countdown");

/**
 * Coundown with RxJs
 * @param startPoint {number} Value of timer continuing to go down
 */
function countDown(startPoint) {
  // * Fire Every Second
  const intervalObs = interval(1000);

  // * Shrink intervalObs subscription
  const disposeInterval = intervalObs.pipe(take(startPoint));

  // * Fire incremental number on every second

  disposeInterval.subscribe((second) => {
    console.log("Second: ", second);
    countDownEl.innerHTML = startPoint - second;
  })
}

countDown(10);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.3.0/rxjs.umd.min.js"></script>

<label>Countdown: <span id="countdown"></span></label>
Shelby answered 6/3, 2021 at 2:8 Comment(0)
K
0

Or this way :)

 interval(1000)
      .take(10)
      .map((t) => Math.abs(t - 10))
      .subscribe((t) => {
        console.log(t);
      });
Kalagher answered 4/2, 2022 at 9:49 Comment(0)
F
0

Easy Appraoch

private countDown: Subscription = new Subscription();
public counter = 300; // seconds
private tick = 1000;

resetTimer() {
    this.canResendOTP = false;
    this.counter = 300;
    this.countDown.unsubscribe();
}

timer() {
    this.resetTimer();
    this.countDown = timer(0, this.tick)
    .subscribe(() => {
        --this.counter;
    });
}
Fic answered 6/6, 2022 at 4:12 Comment(0)
R
-1
const rxjs = require("rxjs");
function customTimer(seconds){
return rxjs
.range(0, seconds + 1)
.pipe(
  rxjs.concatMap((t) => rxjs.of(t).pipe(rxjs.delay(1000))),
  rxjs.map((sec) => ({
    hours: Math.trunc(sec / (60 * 60)),
    minutes: Math.trunc(sec / 60) - Math.trunc(sec / (60 * 60)) * 60,
    seconds: sec - Math.trunc(sec / 60) * 60,
  })),
  rxjs.map(time=>{
    return {
        hours:time.hours>9?`${time.hours}`:`0${time.hours}`,
        minutes:time.minutes>9?`${time.minutes}`:`0${time.minutes}`,
        seconds:time.seconds>9?`${time.seconds}`:`0${time.seconds}`
    }
  })
 );
}

function countDownTimer(startDate, endDate) {
 const seconds = (endDate - startDate) / 1000;
 return rxjs.range(0, seconds + 1).pipe(
        rxjs.concatMap((t) => rxjs.of(t).pipe(rxjs.delay(1000))),
        rxjs.map((sec) => seconds - sec),
        rxjs.takeWhile((sec)=>sec>=0),
        rxjs.map((sec) => ({
          hours: Math.trunc(sec / (60 * 60)),
          minutes: Math.trunc(sec / 60) - Math.trunc(sec / (60 * 60)) * 60,
          seconds: sec - Math.trunc(sec / 60) * 60,
    })),
    rxjs.map((time) => {
      return {
        hours: time.hours > 9 ? `${time.hours}` : `0${time.hours}`,
        minutes: time.minutes > 9 ? `${time.minutes}` : `0${time.minutes}`,
        seconds: time.seconds > 9 ? `${time.seconds}` : `0${time.seconds}`,
     };
   })
  );
 }

// Normal Countdown timer
customTimer(3600).subscribe(time=>console.log(time));

// Reverse  Countdown timer
const startDate = new Date();
const endDate = new Date();
endDate.setMinutes(startDate.getMinutes() + 1);
countDownTimer(startDate, endDate).subscribe((time) => console.log(time));
Rearm answered 1/2, 2023 at 5:21 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Velasquez

© 2022 - 2024 — McMap. All rights reserved.