20
votes

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.

7
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

7 Answers

26
votes

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));
8
votes

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

5
votes

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))
2
votes

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);
  } );
1
votes

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
}
0
votes

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.

0
votes

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

// * startPoint => Value of timer continuing to go down
countDown(startPoint: number) {
  // * 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) => {
    this.countDownOnRetry = startPoint - second;
  });
}