Let's see our requirements.
- Create a Countdown timer
- start button which will start the timer from the current stage
- pause button which will pause the timer, so that, we can resume the timer on clicking start again
- stop button which will stop the timer, and reset the timer number.
Let's see how we can do with Vanilla JS.
<h3 id="result"></h3>
<button id="startBtn">Start</button>
<button id="pauseBtn">Pause</button>
<button id="stopBtn">Stop</button>
Let's first select the elements and add click listeners
const startBtn = document.querySelector('#startBtn');
const stopBtn = document.querySelector('#stopBtn');
const pauseBtn = document.querySelector('#pauseBtn');
const result = document.querySelector('#result');
startBtn.addEventListener('click', () => {
//start the interval
});
stopBtn.addEventListener('click', () => {
//stop the interval and reset value in HTML
});
pauseBtn.addEventListener('click', () => {
// pause the interval
});
We will have to create few variables.
- to store the current value
- to store the initial value
- to store the interval ( Since we want to do an action continuously on a specific interval, we will use setInterval )
let interval;
const initialValue = 10;
let currentValue = initialValue;
We will also set the current value to the HTML
result.innerHTML = `${currentValue}`;
Now, we will create the function to start the timer and call this function on click of start button
const startInterval = () => {
clearInterval(interval);
interval = setInterval(() => {
currentValue -= 1;
if (currentValue <= 0) {
currentValue = initialValue;
clearInterval(interval);
}
result.innerHTML = `${currentValue}`;
}, 1000);
};
startBtn.addEventListener('click', () => {
startInterval();
});
On click of stop button, we will clear the interval, and also reset the value.
stopBtn.addEventListener('click', () => {
currentValue = initialValue;
clearInterval(interval);
result.innerHTML = `${currentValue}`;
});
On click of Pause button, we are just clearing the interval, and not resetting the value.
pauseBtn.addEventListener('click', () => {
clearInterval(interval);
});
Here is the whole code.
Now, lets try the same with RxJS
First, same selectors again
const startBtn = document.querySelector('#startBtn');
const stopBtn = document.querySelector('#stopBtn');
const pauseBtn = document.querySelector('#pauseBtn');
const counterDisplayHeader = document.querySelector('h3');
Now, lets create event streams for the button clicks
const startClick$ = fromEvent(startBtn, 'click');
const stopClick$ = fromEvent(stopBtn, 'click');
const pauseBtn$ = fromEvent(pauseBtn, 'click');
Let's define a starting value, so that, countdown can start from any number defined.
const startValue = 10;
Now, the RxJS magic
merge(startClick$.pipe(mapTo(true)), pauseBtn$.pipe(mapTo(false)))
.pipe(
switchMap(shouldStart => (shouldStart ? interval(1000) : EMPTY)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue),
takeWhile(val => val >= 0),
startWith(startValue),
takeUntil(stopClick$),
repeat()
)
.subscribe(val => {
counterDisplayHeader.innerHTML = val.toString();
});
Let's try to breakdown
First, we will just try only the start. On click of start, we want to start an interval.
startClick$
.pipe(
switchMapTo(interval(1000)),
and, we want to decrement the value by 1 and start the value from the starting value. so, we will use two operators here
startClick$
.pipe(
switchMapTo(interval(1000)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue)
Now, we need to have option to stop the timer. We want to stop the timer on two scenarios.
- When the value reaches 0
- When user press stop button
startClick$
.pipe(
switchMapTo(interval(1000)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue),
takeWhile(val => val >= 0),
takeUntil(stopClick$)
We want to start with a value of startValue
startClick$
.pipe(
switchMapTo(interval(1000)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue),
takeWhile(val => val >= 0),
startWith(startValue),
takeUntil(stopClick$)
)
Now, in case of pause button click, we just want to emit an empty observable.
pauseBtn$
.pipe(
switchMapTo(EMPTY))
)
Finally, we want to combine both start and pause button clicks. We are not really interested in event details. instead, we just want to decide between interval or EMPTY observable based on click. So, we will just map the button clicks to true or false. if start button is clicked, map the value to true and if pause button is clicked, we map the value to false, so that, we can check on switchMap.
merge(startClick$.pipe(mapTo(true)), pauseBtn$.pipe(mapTo(false)))
.pipe(
switchMap(shouldStart => (shouldStart ? interval(1000) : EMPTY))
And, we want to start again once the timer is stopped. For that, we are using repeat() operator
And, you can see and play around with the whole code here,
So, with RxJS, I didn't have to create any extra external variables, intervals etc. Also, no need to add separate logic for start, stop, pause. Whole logic is added in a single chain of commands.
Isn't it neat? What do you think of this? Is there any better way to do this?
Let me know in comments.
Top comments (0)