Building a Stopwatch

Enough theory—you’re probably itching to start building something. The first project you’ll take on in this book is a stopwatch that contains three observables. The stopwatch will have two buttons, one for starting and one for stopping, with an observable monitoring each. Behind the scenes will be a third observable, ticking away the seconds since the start button was pressed in increments of 1/10th of a second. This observable will be hooked up to a counter on the page. You’ll learn how to create observables that take input from the user, as well as observables that interact with the DOM to display the latest state of your app.

images/stopwatch.png

Before we get to the code, take a second to think about how you’d implement this without Rx. There’d be a couple of click handlers for the start and stop buttons. At some point, the program would create an interval to count the seconds. Sketch out the program structure—what order do these events happen in? Did you remember to clear the interval after the stop button was pressed? Is business logic clearly separated from the view? Typically, these aren’t concerns for an app of this size; I’m specifically calling them out now, so you can see how Rx handles them in a simple stopwatch. Later on, you’ll use the same techniques on much larger projects, without losing clarity.

This project has two different categories of observables. The interval timer has its own internal state and outputs to the document object model (DOM). The two-click streams will be attached to the buttons and won’t have any kind of internal state. Let’s tackle the hardest part first—the interval timer behind the scenes that needs to maintain state.

Running a Timer

This timer will need to track the total number of seconds elapsed and emit the latest value every 1/10th of a second. When the stop button is pressed, the interval should be cancelled. We’ll need an observable for this, leading to the question: “How on earth do I build an observable?”

Good question—read through this example (don’t worry about knowing everything that’s going on, but take a few guesses as you go through it).

 import​ { Observable } ​from​ ​'rxjs'​;
 
 let​ tenthSecond$ = ​new​ Observable(observer => {
 let​ counter = 0;
  observer.next(counter);
 let​ interv = setInterval(() => {
  counter++;
  observer.next(counter);
  }, 100);
 
 return​ ​function​ unsubscribe() { clearInterval(interv); };
 });

Let’s walk through that line-by-line. As you read through each snippet of code, add it to the stopwatch.ts file in vanilla/creatingObservables.

 import​ { Observable } ​from​ ​'rxjs'​;

The first thing is to use import to bring in Observable from the RxJS library. All of the projects in this book start off by bringing in the components needed to run the project. If your editor is TypeScript-aware (I recommend Visual Studio Code[3]), you probably have the option to automatically import things as you type. Most examples in this book skip the import statement for brevity’s sake.

 let​ tenthSecond$ = ​new​ Observable(observer => {

There’s that dollar sign again, indicating the variable contains an observable. On the other side of the equals sign is the standard Rx constructor for observables, which takes a single argument: a function with a single parameter, an observer. Technically, an observer is any object that has the following methods: next(someItem) (called to pass the latest value to the observable stream), error(someError) (called when something goes wrong), and complete() (called once the data source has no more information to pass on). In the case of the observable constructor function, Rx creates the observer for you and passes it to the inner function. Later on, we’ll see some other places you can use observers and even create new ones.

 let​ counter = 0;
 observer.next(counter);
 let​ interv = setInterval(() => {
  counter++;
  observer.next(counter);
 }, 100);
images/aside-icons/note.png

While setInterval isn’t perfect at keeping exact time, it suffices for this example. You’ll learn about more detailed methods of tracking time in Chapter 8, Advanced Angular.

Inside the constructor function, things get interesting. There’s an internal state in the counter variable that tracks the number of tenths-of-a-second since the start. Immediately, observer.next is called with the initial value of 0. Then there’s an interval that fires every 100 ms, incrementing the counter and calling observer.next(counter). This .next method on the observer is how an observable announces to the subscriber that it has a new value available for consumption. The practical upshot is that this observable emits an integer every 100 ms representing how many deciseconds have elapsed since…

…well, when exactly does this function run? Throw some console.log statements in and run the above snippet. What happens?

Nothing appears in the console—the constructor appears to never actually run. This is the lazy observable at work. In Rx land, this constructor function will only run when someone subscribes to it. Not only that, but if there’s a second subscriber, all of this will run a second time, creating an entirely separate stream (this means that each subscriber gets its own timer)! You can learn more about how all of this works in Chapter 5, Multiplexing Observables, but for now, just remember that each subscription creates a new stream.

Finally, the inner function returns yet another function (called an unsubscribe function):

 return​ ​function​ unsubscribe() { clearInterval(interv); };

If the constructor function returns another function, this inner function runs whenever a listener unsubscribes from the source observable. In this case, the interval is no longer needed, so we clear it. This saves CPU cycles, which keeps fans from spinning up on the desktop, and mobile users will thank us for sparing their batteries. Remember, each subscriber gets their own instance of the constructor, and so, has their own cleanup function. All of the setup and teardown logic is located in the same place, so it requires less mental overhead to remember to clean up all the objects that get created.

Speaking of mental overhead, that was a lot of information in just a few lines of code. There are a lot of new concepts here, and it might get tedious writing this every time we want an interval. Fortunately, all of this work has already been implemented in the Rx library in the form of a creation operator:

 import​ { interval } ​from​ ​'rxjs'​;
 
 let​ tenthSecond$ = interval(100);

Rx ships with a whole bunch of these creation operators for common tasks. You can find the complete list under the “Static Method Summary” heading at the official RxJS site.[4] interval(100) is similar to the big constructor function we had above. Now, to actually run this code, subscribe:

 import​ { interval } ​from​ ​'rxjs'​;
 
 let​ tenthSecond$ = interval(100);
 tenthSecond$.subscribe(console.log);

When there’s a subscribe call, numbers start being logged to the console. The numbers that are logged are slightly off from what we want. The current implementation counts the number of tenths-of-a-second since the subscription, not the number of seconds. One way to fix that is to modify the constructor function, but stuffing all the logic into the constructor function gets unwieldy. Instead, an observable stream modifies data after a root observable emits it using a tool called an operator.

Piping Data Through Operators

An operator is a tool provided by RxJS that allows you to manipulate the data in the observable as it streams through. You can import operators from ’rxjs/operators’. To use an operator, pass it into the .pipe method of an observable. Here, the fictional exampleOperator is used for illustration:

 import​ { interval } ​from​ ​'rxjs'​;
 import​ { exampleOperator } ​from​ ​'rxjs/operators'​;
 
 interval(100)
 .pipe(
  exampleOperator()
 );
images/aside-icons/note.png In previous versions of RxJS, the operators were methods attached directly to the observable. This made it difficult for bundling tools like Webpack to determine which operators weren’t needed in the production bundle. With RxJS v6, only operators that are needed are imported, allowing a bundler to ignore the rest, resulting in a smaller bundle.

Next, you’ll learn how to use the most popular operator: map.

Manipulating Data in Flight with map

Right now, you have a collection of almost-right data that needs just one little tweak for it to be correct. Enter the map operator.

Generally speaking, a map function takes two parameters (a collection and another function), applies the function to each item, and returns a new collection containing the results. A simple implementation looks something like this:

 function​ map(oldArr, someFunc) {
 let​ newArr = [];
 for​ (​let​ i = 0; i < oldArr.length; i++) {
  newArr.push(someFunc(oldArr[i]));
  }
 return​ newArr;
 }

JavaScript provides a built-in map for arrays that only takes a single parameter (the function). The array in question is the one the map method is called on:

 let​ newArr = oldArr.map(someFunc);

This example only works with synchronous arrays—conceptually, map works on any type of collection. Observables are just such a collection and Rx provides a map operator of its own. It’s piped through a source observable, takes a function, and returns a new observable that emits the result of the passed-in function. Importantly, the modification in map is synchronous. Even though new data arrives over time, this map immediately modifies the data and passes it on. Syntax-wise, the only major difference is that the RxJS example uses pipe to pass in map:

 let​ newObservable$ = oldObservable$.pipe(
  map(someFunc)
 );

We can apply this mapping concept to tenthSecond$. The source observable will be the interval created earlier. The modification is to divide the incoming number by 10. The result looks something like:

 import​ { map } ​from​ ​'rxjs/operators'​;
 
 tenthSecond$
 .pipe(
  map(num => num / 10)
 )
 .subscribe(console.log);

With these few lines, the first observable is ready. Two more to go.

Handling User Input

The next step is to manage clicks on the start and stop buttons. First, grab the elements off the page with querySelector:

 let​ startButton = document.querySelector(​'#start-button'​);
 let​ stopButton = document.querySelector(​'#stop-button'​);

Now that we have buttons, we need to figure out when the user clicks them. You could use the constructor covered in the last section to build an observable that streams click events from an arbitrary element:

 function​ trackClickEvents(element) {
 return​ ​new​ Observable(observer => {
 let​ emitClickEvent = event => observer.next(event);
 
  element.addEventListener(​'click'​, emitClickEvent);
 return​ () => element.removeEventListener(emitClickEvent);
  });
 }

Much like interval, we can let the library do all the work for us. Rx provides a fromEvent creation operator for exactly this case. It takes a DOM element (or other event-emitting object) and an event name as parameters and returns a stream that fires whenever the event fires on the element. Using the buttons from above:

 import​ { fromEvent } ​from​ ​'rxjs'​;
 
 let​ startButton = document.querySelector(​'#start-button'​);
 let​ stopButton = document.querySelector(​'#stop-button'​)
 
 let​ startClick$ = fromEvent(startButton, ​'click'​);
 let​ stopClick$ = fromEvent(stopButton, ​'click'​);

Add a pair of subscribes to the above example to make sure everything’s working. Every time you click, you should see a click event object logged to the console. If you don’t, make sure the code subscribes to the correct observable and that you’re clicking on the correct button.

Assembling the Stopwatch

All three observables have been created. Now it’s time to assemble everything into an actual program.

 // Imports
 import​ { interval, fromEvent } ​from​ ​'rxjs'​;
 import​ { map, takeUntil } ​from​ ​'rxjs/operators'​;
 
 // Elements
 let​ startButton = document.querySelector(​'#start-button'​);
 let​ stopButton = document.querySelector(​'#stop-button'​);
 let​ resultsArea = document.querySelector<HTMLElement>(​'.output'​);
 // Observables
 let​ tenthSecond$ = interval(100);
 let​ startClick$ = fromEvent(startButton, ​'click'​);
 let​ stopClick$ = fromEvent(stopButton, ​'click'​);
 
 startClick$.subscribe(() => {
  tenthSecond$
  .pipe(
  map(item => (item / 10)),
  takeUntil(stopClick$)
  )
  .subscribe(num => resultsArea.innerText = num + ​'s'​);
 });
Joe asks:
Joe asks:
What’s that <HTMLElement> mean?

This book uses TypeScript for all the examples. The <angle bracket notation> denotes the specific return type for querySelector. TypeScript knows that querySelector will return some kind of Element. In this case, we know specifically that we’re querying for an element of the HTML variety, so we use this syntax to override the generic element. With that override, TypeScript now knows that resultsArea has properties specific to an HTMLElement, such as .innerText. We don’t need to use the angle brackets when we query for button elements, because we’re not doing anything button specific with those variables, so the generic Element type suffices.

There’s a few new concepts in the stopwatch example, so let’s take it blow-by-blow. To start, there are six variables, three elements from the page and three observables (you can tell which ones are observables, because we annotated the variable name with a dollar sign). The first line of business logic is a subscription to startClick$, which creates a click event handler on that element. At this point, no one’s clicked the Start button, so Rx hasn’t created the interval or an event listener for the stop button (saving CPU cycles without extra work on your part).

When the Start button is clicked, the subscribe function is triggered (there’s a new event). The actual click event is ignored, as this implementation doesn’t care about the specifics of the click, just that it happened. Immediately, tenthSecond$ runs its constructor (creating an interval behind the scenes), because there’s a subscribe call at the end of the inner chain. Every event fired by $tenthSecond runs through the map function, dividing each number by 10. Suddenly, an unexpected operator appears in the form of takeUntil.

takeUntil is an operator that attaches itself to an observable stream and takes values from that stream until the observable that’s passed in as an argument emits a value. At that point, takeUntil unsubscribes from both. In this case, we want to continue listening to new events from the timer observable until the user clicks the Stop button. When the Stop button is pressed, Rx cleans up both the interval and the Stop button click handler. This means that both the subscribe and unsubscribe calls for stopClick$ happen at the library level. This helps keep the implementation simple, but it’s important to remember that the (un)subscribing is still happening.

Finally, we put the latest value from tenthSecond$ on the page in the subscribe call. Putting the business logic in Rx and updating the view in the subscribe call is a common pattern you’ll see in both this book and in any observable-heavy frontend application.

You’ll notice that repeated clicks on the Start button cause multiple streams to start. The inner stream should listen for clicks on either button and pass that to takeUntil. This involves combining two streams into one, a technique you’ll learn in Chapter 2, Manipulating Streams.

How Does This Apply Externally?

I’m sure you’re just jaw-droppingly stunned at the amazing wonder of a stopwatch. Even in this basic example, you should start to see how Rx can simplify our complicated frontend codebases. Each call is cleanly separated, and the view update has a single location. While it provides a neat demo, modern websites aren’t made entirely of stopwatches, so why build one at all?

The patterns in this example didn’t just solve the mystery of the stopwatch. Everything you’ve built so far in this chapter sums into a pattern that solves any problem of the shape, “Upon an initiating event, watch a stream of data until a concluding event occurs.” This means that this code also solves one of the biggest frontend frustrations:

Drag and Drop

Another example of RxJS’s power is drag-and-drop. Anyone who’s tried to implement drag-and-drop without a library understands just how hair-pullingly frustrating it is. The concept is simple: On a mousedown event, track movement and update the page with the new position of the dragged item until the user lets go of the mouse button. The difficult part of dealing with a dragged element comes in tracking all of the events that fire, maintaining state and order without devolving into a horrible garbled mess of code.

Adding to the confusion, a flick of the user’s wrist can generate thousands of mousemove events—so the code must be performant. Rx’s lazy subscription model means that we aren’t tracking any mousemove events until the user actually drags the element. Additionally, mousemove events are fired synchronously, so Rx will guarantee that they arrive in order to the next step in the stream.

Write out the following snippet in dragdrop.ts, in the same directory as the previous stopwatch example. The following example reuses the stopwatch patterns to create a draggable tool:

 import​ { fromEvent } ​from​ ​'rxjs'​;
 import​ { map, takeUntil } ​from​ ​'rxjs/operators'​;
 
 let​ draggable = <HTMLElement>document.querySelector(​'#draggable'​);
 
 let​ mouseDown$ = fromEvent<MouseEvent>(draggable, ​'mousedown'​);
 let​ mouseMove$ = fromEvent<MouseEvent>(document, ​'mousemove'​);
 let​ mouseUp$ = fromEvent<MouseEvent>(document, ​'mouseup'​);
 
 mouseDown$.subscribe(() => {
  mouseMove$
  .pipe(
  map(event => {
  event.preventDefault();
 return​ {
  x: event.clientX,
  y: event.clientY
  };
  }),
  takeUntil(mouseUp$)
  )
  .subscribe(pos => {
 // Draggable is absolutely positioned
  draggable.style.left = pos.x + ​'px'​;
  draggable.style.top = pos.y + ​'px'​;
  });
 });

At the start are the same bunch of variable declarations that you saw in the stopwatch example. In this case, the code tracks a few events on the entire HTML document, though if only one element is a valid area for dragging, that could be passed in. The initiating observable, mouseDown$ is subscribed. In the subscription, each mouseMove$ event is mapped, so that the only data passed on are the current coordinates of the mouse. takeUntil is used so that once the mouse button is released, everything’s cleaned up. Finally, the inner subscribe updates the position of the dragged element across the page.

Plenty of other conceptual models lend themselves to this pattern.

Loading Bars

Instead of trying to track lots of global state, let Rx do the heavy lifting. You’ll find out in Chapter 3, Managing Asynchronous Events how to add a single function here to handle cases when a bit of your app didn’t load.

 startLoad$.subscribe(() => {
  assetPipeline$
  .pipe(
  takeUntil(stopLoad$)
  )
  .subscribe(item => updateLoader(item));
 });

Chat Rooms

We both know just how much programmers love chat rooms. Here, we use the power of Rx to track only the rooms the user has joined. You’ll use some of these techniques to build an entire chat application in Chapter 5, Multiplexing Observables.

 loadRoom$.subscribe(() => {
  chatStream$
  .pipe(
  takeUntil(roomLeave$)
  )
  .subscribe(msg => addMsgToRoom(msg));
 });
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.162.49