Learn Angular & RxJS: Countdown Timer

How to create a simple countdown app with RxJS

Photo by NeONBRAND on Unsplash

I have built a sample countdown application in Angular. Our application will be able to maintain state, start, pause, and reset using RxJS. This will help us learn RxJS concepts that we can reuse across other applications.

Before you start, download the source code.

The master branch is the completed solution. In the follow-along branch, the labeled code snippets in the article have been removed. I have placed each code snippet label in the source code, so they can be located easily. See README.md for instructions.

Application

Application Overview

App Component Overview

app-countdown/countdown.component.ts will represent our clock, as well as the start, pause, and reset buttons.

app-time-input/time-input.component.ts will represent our three inputs for entering seconds, minutes, and hours.

appInputToCountdown/input-to-countdown.directive.ts will be responsible for communication between countdown.component and time-input.component . This is how we will receive the total time in seconds from the input and display it in the countdown.

time-format.pipe.ts will convert our total number of seconds to a time formatted string.

Managing State with BehaviorSubject

Our directive, input-to-countdown.directive.ts , will manage our state utilizing an RxJS BehaviorSubject. Managing state simply means keeping track of our seconds, minutes, hours, and total seconds.

Key Concept #1: Protect subjects from extensive access/modification

A BehaviorSubject is a subject with an initial value [1]. We keep our BehaviorSubject state private and expose its observable obs$ as public. This protects state from extensive modification while allowing other components to listen for emissions from state via obs$ . [2]

1.1 | inputToCountdown.directive.ts | follow-along

updateState(value, command) will be called when one of our input values change in time-input.component . Updating the state will also trigger an emission from obs$ . This is our first key concept in action. We lock down our subject to a minimum and give open access to our observable for emissions.

Key Concept #2: Keep subject data clean

The parameter command notifies us which input changed between seconds, minutes, and hours. We will convert the input value to an integer and make sure it is a positive number to keep data clean, then get the current value of state as update . Note that our subject can only be updated via updateState() . We validate the input data at its single modification point to guarantee clean data.

1.2 | inputToCountdown.directive.ts | follow-along

update will be modified to contain the latest changes from the input, and recalculate the total seconds in calculateSeconds(update) . Then we update state via this.state.next(update) .

1.3 | inputToCountdown.directive.ts | follow-along

Update State

Binding Events and State to Inputs

We will inject our directive into time-input.component.ts . All of our logic is invoked in time-input.component.html .

Component Injects Directive

The key detail here is our change event. When one of our inputs fires a change event, our directive will invoke updateState(value,command) . In other words, we have bound each input to update our state on a change event.

Tip: By labeling our input, we can access its value via name.value instead of $event.target.value .

2.1 | time-input.component.html | follow-along

Start, Pause, and Reset Observables

The snippet below contains a lot of information. We will break it down by RxJS operator usage.

3.1 | countdown.component.ts | follow-along

Map To/Merge

mapTo takes our emitted value and converts it to whatever value we pass in as an argument. In our application, we are converting change events and click events into true/false/null values. [3]

Key Concept #3: Utilize a single observable to promote easy subscription management strategies

merge will combine our four observables into a single observable [4]. Merge will only emit one value at a time. intervalObs$ will now emit either true, false, or null depending on which event occurred. Note: all of our event observables will be managed by a single subscription and a single Async pipe.

break-down pt 1 | countdown.component.ts

Switch Map

switchMap allows us to start emitting from a new observable based on input from our original observable. Our original observable is the merge of all possible events that could trigger a change (start, pause, reset, state change). Our new observable is either an observable of null, an interval, or an observable of empty. Note, our original observable will still emit when an event occurs. [5]

Scan

scan has an accumulator and a current value. The accumulator keeps track of all emitted values. In our case, we are simply using the accumulator to remember the previous value and subtract from it. We only use the current value to signal if the value is null (reset) or not null (continue). [6]

Key Concept #4: Use scan to manage internal state and avoid side-effects

d is our input-to-component.directive . If currentValue is null, or accumulatedValue is false, we will return the total seconds from d . Once we return total seconds, accumulatedValue will be set back to total seconds. If accumulatedValue is zero, return zero. In other words, scan allows us to keep track of an internal state inside our observable without modifying outside data.

Note that when we switchMap to of() , scan will not decrement accumulatedValue because of() does not emit a value.

break-down pt 2 | countdown.component.ts

An Analogy Between Our Countdown and a Computer

Passing in true is like starting our computer. If the computer is off, we will begin at our initial starting point. If the computer is asleep, we will start where we left off. Passing in false is like putting our computer to sleep. Passing in null is like shutting our computer off.

So when our inputs change (seconds, minutes, or hours) or we click reset, we want the computer to shut off until the next start. If we click pause, we want the computer to sleep until we start again from where we left off.

Format Input with Pipe

To wrap up our countdown, we will use an Angular pipe to format our total seconds into a display value. The padding function will pad the number if the number is below 10. For example, the number nine becomes 09 .

4.1 | time-format.pipe.ts | follow-along

Key Concept #5: Utilize async pipe to simplify subscription management

Notice the else default syntax. If our observable has not emitted a truthy value yet, instead of showing nothing, we will default to 00:00:00

4.2 | countdown.component.html | follow-along end

Counting Down

Conclusion

Implementing a countdown in JavaScript can lead to bug-prone code and unexpected side effects. With RxJS we were able to create an efficient countdown, without a large amount of code. Thanks for reading!