Building a Simple Application with RxJS

687 VIEWS

· ·

RxJS is a JavaScript library for reactive programming. At its core, reactive programming is programming asynchronously utilizing observable sequences. An observable sequence is used to define an asynchronous data stream. Basically, as data is emitted from the data stream, that data can be observed reacted to asynchronously. The best way to understand these concepts is most likely through the use of a sample program. Below I will take you through a sample application that simply utilizes HTML, jQuery, jQuery UI, and of course, RxJS to show the basic concepts of reactive programming using RxJS in an effort to help you get started with the library.

Displaying baseball scores using RxJS

In choosing a test application to build using RxJS, I decided to build a simple UI for displaying sample scores for baseball games. I created four files to get the job done—an HTML file for display, a JS file where most of our work will be done, and two JSON files that will hold 10 sample baseball scores for two different days. The goal will be to provide the user with a datepicker for selecting a date for which they would like to display scores. We will then show the user one score at a time and they will have a “Next” button to move from displaying one score to the next. When our code is completed, it will look like the screenshot below:

The Submit button will allow us to look at sample scores from another date while the Next button will allow us to cycle through the sample scores from that date.

Using observable sequences

After including all the necessary libraries in my HTML (jQuery, jQuery UI, RxJS), I was able to get started with the JavaScript side of the application. The first step was to set up a simple datepicker using the Datepicker widget from jQuery UI. By setting up the datepicker element in the HTM,L along with the Submit button that can be seen in the screenshot above, that brings us to our first opportunity to utilize the RxJS library by creating an observable sequence to monitor the click event for the Submit button:

 $('#datepicker').datepicker();
var showIndex = 1;

var showScoresClickStream = Rx.Observable.fromEvent($('#showScoresButton'), 'click');

The final line of code in this snippet creates a stream that emits a response each time the HTML element with the ID showScoresButton (our Submit button) is clicked. We can then use this stream to build the URL to our JSON file that holds the score data for the date selected in the datepicker. We do so through the creation of another stream which we call showScoresResponseStream. Right now, we only have knowledge that the button has been clicked. By using the RxJS map function on the showScoresClickStream, we can take the emitted value that there has been a click event and create the URL. See below:

 var showScoresResponseStream = showScoresClickStream.map(event => {
       var dateAsString = $('#datepicker').val().replace(/\//g, '');
       var url = 'http://localhost:8080/data/scores' + dateAsString + '.json';
       return url;
   });

With the addition of the above lines of code, we have created another observable that will emit a value with each click of the Submit button—except this time, instead of emitting that there has been a click event, the showScoresResponseStream will emit a URL containing the sample scores for the date provided to the datepicker (since this is just a sample project I simply created two sample files that have their filename formatted as follows: scoresMMDDYYYY.json). Think of this code by looking at the following diagram of the two streams.

This essentially depicts what the map function does. The showScoresClickStream will emit a response with each click that simply denotes that the click event has happened while the showScoresResponseStream has now been defined to emit a response with each click event that will contain the URL of the JSON file to be retrieved for processing.

Now that we know the URL of the JSON file where we will find our data, we can utilize something called flatMap to help us retrieve an observable stream from the response of the URL. The code will look like this:

 var getJsonResponseStream = showScoresResponseStream
   .flatMap(requestUrl => {
       return Rx.Observable.fromPromise($.getJSON(requestUrl))
           .catch(response => {
               cleanUpUI();
               return Rx.Observable.empty()
           });
   });

function cleanUpUI() {
   $('#next').hide();
   $('.result').each(function(index) {
       $('#' + this.id +' .away-team').empty();
       $('#' + this.id +' .away-score').empty();
       $('#' + this.id +' .home-team').empty();
       $('#' + this.id +' .home-score').empty();
       $(this).hide();
   });
} 

What’s happening here is as follows:

  1. We take the request URL that is emitted from showScoresResponseStream.
  2. We utilize the fromPromise function to create an observable stream of promises.
  3. The flatMap function then allows us to create an observable stream of the objects emitted from the promises stream.
  4. The catch function that follows the fromPromise is there to catch an invalid URL. In our case, there will be many, as I have only created two JSON files with sample score data. By catching an error such as a ‘404’, we will ensure that our application does not crash if a date with no sample scores is selected. I created a simple JavaScript function called cleanUpUI to remove all score data from a previous submission, should there be any.
  5. So now we are in a very useful spot. We have a stream holding the JSON response data (getJsonResponseStream), and can leverage additional RxJS functionality to pull the individual scores from the JSON response and display them on the page. The first step will be to call map on getJsonResponseStream to produce an observable sequence of the parsed JSON objects. We will then chain a call to concatAll which will ensure that each Observable created by the prior call to map will be concatenated into the stream in the order it is emitted. Another way to do this same thing would be to use the concatMap function provided in the RxJS library. But for now, we’ll stick with the two-step approach.

    Following this, we will make the call to subscribe where we can utilize the Observables in the JSON object stream to display the scores in the HTML. See below for the code implementation of what was just described:

     getJsonResponseStream.map(data => JSON.parse(JSON.stringify(data)))
       .concatAll()
       .subscribe(obj => {
           $('#next').show();
           displayValues(obj);
       });
    
    function displayValues(object) {
       var elementId = 'result' + object.id;
    
       var awayTeam = JSON.stringify(object.away_team);
       var awayScore = JSON.stringify(object.away_score);
       var homeTeam = JSON.stringify(object.home_team);
       var homeScore = JSON.stringify(object.home_score);
    
       $('#' + elementId +' .away-team').text(awayTeam.replace(/['"]+/g, ''));
       $('#' + elementId +' .away-score').text(awayScore.replace(/['"]+/g, ''));
       $('#' + elementId +' .home-team').text(homeTeam.replace(/['"]+/g, ''));
       $('#' + elementId +' .home-score').text(homeScore.replace(/['"]+/g, ''));
    
       if (showIndex == object.id) {
           $('#' + elementId).show();
       } else {
           $('#' + elementId).hide();
       }
    }

    The displayValues function will add the text from the JSON objects to the UI, while only showing the score that we want to show at that particular moment.

    Finally, we have to add functionality for the Next button to cycle through the scores. Once again, we will create an observable sequence for the click event of the Next button. From there, we will utilize the scan function in the RxJS library to emit the click count as a value between 1 and 10. In our particular application, our JSON files only contain 10 scores per file. The stream we are creating for the click event will increment from 1 to 10 with each click and then restart at 1 with the next click following 10. We can then use this index in our call to subscribe where we can choose to show the score at the current index.

     var nextButton = $('#next').hide();
    Rx.Observable.fromEvent(nextButton, 'click')
       .scan(count => (count + 1)%10, 1)
       .subscribe(count => {
           setShowIndex(count);
           $('.result').each(function(index) {
               if (index == count) {
                   $(this).show();
               }
               else {
                   $(this).hide();
               }
           });
       });
    
    function setShowIndex(count) {
       if (count == 0) {
           showIndex = 1;
       } else {
           showIndex = count;
       }
    }

    Conclusion

    This example took you through only a small portion of what RxJS has to offer, but serves to show some of the common functionality provided by RxJS. Through the use of observable sequences, we are able to build a standalone application that never requires the page to reload in order to function—that is to say that through the use of the RxJS library in conjunction with jQuery and jQuery UI, we can set the content of a single HTML file dynamically as conditions change.

    Below I have posted the full HTML and JavaScript files for your reference:

    showScoresPage.html

     
    
    
    
    Scores
    
    
    
    
    
    
    
    
      

    Scores

    Date:

     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     

    showScores.js

     $(document).ready(function() {
    
       $('#datepicker').datepicker();
       var showIndex = 1;
    
       var showScoresClickStream = Rx.Observable.fromEvent($('#showScoresButton'), 'click');
    
       var showScoresResponseStream = showScoresClickStream.map(event => {
               var dateAsString = $('#datepicker').val().replace(/\//g, '');
               var url = 'http://localhost:8080/data/scores' + dateAsString + '.json';
               return url;
           });
    
       var getJsonResponseStream = showScoresResponseStream
           .flatMap(requestUrl => {
               return Rx.Observable.fromPromise($.getJSON(requestUrl))
                   .catch(response => {
                       cleanUpUI();
                       return Rx.Observable.empty()
                   });
           });
    
       getJsonResponseStream.map(data => JSON.parse(JSON.stringify(data)))
           .concatAll()
           .subscribe(obj => {
               $('#next').show();
               displayValues(obj);
           });
    
       function displayValues(object) {
           var elementId = 'result' + object.id;
    
           var awayTeam = JSON.stringify(object.away_team);
           var awayScore = JSON.stringify(object.away_score);
           var homeTeam = JSON.stringify(object.home_team);
           var homeScore = JSON.stringify(object.home_score);
    
           $('#' + elementId +' .away-team').text(awayTeam.replace(/['"]+/g, ''));
           $('#' + elementId +' .away-score').text(awayScore.replace(/['"]+/g, ''));
           $('#' + elementId +' .home-team').text(homeTeam.replace(/['"]+/g, ''));
           $('#' + elementId +' .home-score').text(homeScore.replace(/['"]+/g, ''));
    
           if (showIndex == object.id) {
               $('#' + elementId).show();
           } else {
               $('#' + elementId).hide();
           }
       }
    
       function cleanUpUI() {
           $('#next').hide();
           $('.result').each(function(index) {
               $('#' + this.id +' .away-team').empty();
               $('#' + this.id +' .away-score').empty();
               $('#' + this.id +' .home-team').empty();
               $('#' + this.id +' .home-score').empty();
               $(this).hide();
           });
       }
    
       var nextButton = $('#next').hide();
       Rx.Observable.fromEvent(nextButton, 'click')
           .scan(count => (count + 1)%10, 1)
           .subscribe(count => {
               setShowIndex(count);
               $('.result').each(function(index) {
                   if (index == count) {
                       $(this).show();
                   }
                   else {
                       $(this).hide();
                   }
               });
           });
    
       function setShowIndex(count) {
           if (count == 0) {
               showIndex = 1;
           } else {
               showIndex = count;
           }
       }
    
    });

Scott Fitzpatrick has over 5 years of experience as a software developer. He has worked with many languages, including Java, ColdFusion, HTML/CSS, JavaScript and SQL.


Discussion

Click on a tab to select how you'd like to leave your comment

Leave a Comment

Your email address will not be published. Required fields are marked *

Menu