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.

Jon Miles
  • 9,605
  • 11
  • 46
  • 66
  • 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 . – Simon_Weaver Jan 31 '20 at 20:25

12 Answers12

30

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));
Niklas Fasching
  • 1,326
  • 11
  • 15
  • 1
    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. – Jon Miles Jan 21 '16 at 12:17
  • 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](https://github.com/Nupf/cull/blob/master/src/Observable.js))? – Niklas Fasching Jan 21 '16 at 16:37
  • There is an operator that does this, `generate` it just hasn't been added to the new project yet. – paulpdaniels Jan 21 '16 at 17:11
  • I saw the `generate` function in v4, I'm surprised it's not in v5. It would offer me the functionality I require. – Jon Miles Jan 22 '16 at 10:35
15

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

Matt Bogenhagen
  • 176
  • 1
  • 5
  • 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. – Itay Jan 03 '21 at 11:05
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))
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
Zafer Qadi
  • 69
  • 1
  • 1
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);
  } );
Bayu
  • 2,340
  • 1
  • 26
  • 31
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.

Itay
  • 398
  • 2
  • 14
  • Hi just wanted to understand this. in first code snippet why you are using `numberOfSeconds + 1` inside the scan and take – NicoleZ Oct 11 '22 at 16:16
  • 1
    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. – Itay Oct 12 '22 at 12:13
  • 1
    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, 0```` – Itay Oct 12 '22 at 12:16
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
}
anlijudavid
  • 509
  • 6
  • 12
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')}`);
})
Emmanuel
  • 4,933
  • 5
  • 46
  • 71
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!`)
});
Alexey Grinko
  • 2,773
  • 22
  • 21
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>
ezhupa99
  • 1,838
  • 13
  • 18
0

Or this way :)

 interval(1000)
      .take(10)
      .map((t) => Math.abs(t - 10))
      .subscribe((t) => {
        console.log(t);
      });
Andrea Scarafoni
  • 937
  • 2
  • 13
  • 26
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;
    });
}
Md omer arafat
  • 398
  • 7
  • 10
-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));
  • 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](/help/how-to-answer). – Community Feb 06 '23 at 09:21