Exploring Riot.js – Event-driven app (Step2)

Welcome to Step 2 of the “Exploring Riot.js – Part 3”!, modestly entitled:

Step #2 – an even more amazing app!

In the Step 1 of our “Exploring Riot.js – Event-driven app”, we saw an amazing app that presented the data from the Stockmarket API in different forms: a table and a chart.

Do you remember one thing about the Stockmarket API? Every time, you call it, some prices may change.  It can be quite frustrating being forced to refresh our browser page with a “F5” or by clicking on the “refresh” icon of the browser. You know what? Let’s add another feature to our already amazing app. A feature that will bring the user to the next level of UX (not to say to the heaven because we are modest): let’s add a “Reload stocks” button that fetches the new stocks when it is clicked and thus, refreshes both the table and the chart. Brilliant! Instead of forcing the user to refresh the page through the browser, we provide him a “Reload stocks” button that refreshes the UI. A UX conceptual revolution, I tell you so! (Does ever one noticed a bit of irony?). Ok, for those who had some doubt about the legitimity of the “Reload stocks” button as an innovating UX experience, let’s pretend it’s for a good cause: illustrating the event feature of Riot.js

riot.js stockmarket application (step2) screenshot

As suggested earlier, the events feature of Riot.js is served by components called Observables. Observables are components that can send and receive events. These enable us  to get a loose-coupled architecture and promote the modularity of an app. Let’s have a look at a simple example.

We declare an observable like this:

var myObs = riot.observable();

You can also attach the observable flavor to an object / function too:

function Duck() {
  // Make Duck instances observable
  riot.observable(this)
}

An observable can send events (trigger):

myObs.trigger('myEvent');

and it can send events with data:

var data = {price: 11};
myObs.trigger('myEvent', data);

And it can receive events too:

myObs.on('myEvent', function(data) {
  // your code here
});

Now, you get the basis, let’s modify our app.

First, we create an observable. I call it “bus” because it reminds me the event bus in the Java world such as the Guava one. And I use this observable as a bus of events. So, in the index.html, we have:

var bus = riot.observable();

Then, we will start refactoring a bit the code. Let’s do something we should have done a while ago: encapsulating the code that fetches the stocks from the Stockmarket API into a service.

We build our service upon the bus. Once we fetch the stocks, we send an event (‘newStocksEvent’) with the stocks through the bus: any component interested in this event will subscribe to it and thus be able to receive the stocks. The code of our service (stockmarket-service.js) now looks like this:

'use strict';

class StockMarketService {
  constructor(bus) {
    this.bus = bus;
    this.url = "http://stockmarket.streamdata.io/prices";
  }

  fetchJson() {
    var self = this;
    var xhr = new XMLHttpRequest();

    xhr.onload = function() {
      var status = xhr.status;
      if (xhr.readyState == 4 && xhr.status == 200) {
        var stocks = JSON.parse(xhr.responseText);
        self.bus.trigger('newStocksEvent', stocks);

      } else {
        console.error(xhr.status);
        self.bus.trigger('errorStocksEvent', xhr.status);

      }
    }

    xhr.open("GET", self.url, true);
    xhr.send();
  }
}

Hint: As you can see our service is written in ECMAScript 2015 (ES6). If you’re using a recent version of Node.js, it natively supports it (see here and here for details). Otherwise, you have to tell Babel to transpile this code too.

As we already use babel-core for transpiling code in our Riot components, we can use the require hook to compile code in the service.

First of all we need to create a main.js file to call babel-register and give it our service’s filename:

require('babel-core/register');
require('./app/js/stockmarket-service.js');

Then we are just going to add a new task in our serve task to compile it:

{
   ...
   "scripts": {
    ...
    "serve": "parallelshell 'node main.js' 'npm run watch' 'npm run browsersync'"
  },
  ...
}

Now, let’s add our awesome UX feature: the “Reload stocks” button. This is a fairly simple Riot.js component (stockmarket-button.tag) with a button and an UI event handler:

<stockmarket-button>
  <h3>{opts.title}</h3>
  <button type="button" class="btn btn-success" onclick="{loadStocks}">{opts.title}</button>

  <script type='es6'>
    this.loadStocks = (e) => {
      opts.stockMarketService.fetchJson();
    };
  </script>
</stockmarket-button>

Note the use of the StockMarketService: we inject it as a service in the Riot.js opts object of the tag:

 <body>
  <!-- mount point -->
  <stockmarket-button></stockmarket-button>
  ...
  
  <!-- include riot.js -->
  ...
  <script src="./dist/js/stockmarket-button.js"></script>
  <script src="./dist/js/stockmarket-service.js"></script>

  <!-- mount normally -->
  <script>
    var bus = riot.observable();
    var stockMarketService = new StockMarketService(bus);

    riot.mount('stockmarket-button', {title: 'Load Stocks', stockMarketService: stockMarketService});
    ...
  </script>
</body>

Once again, the injection of the service is done in the riot.mount(.) instruction. By some aspects, it reminds me of  injection in AngularJS.

The next step is modifying our two components (stockmarket-table and stockmarket-barchart) to handle the stocks. These two components will subscribe to the ‘newStocksEvent’. When the component receives the event, it will get the stocks encapsulated with the event and be able to refresh its UI.

For the stockmarket-table, the code becomes now:

<stockmarket-table>
  <h3>{opts.title}</h3>
  <table class="table table-striped table-bordered table-hover table-condensed" style="width: 25%;">
    <thead>
      <tr>
        <th>Title</th>
        <th>Price</th>
      </tr>
    </thead>

    <tbody>
      <tr each={stocks}>
        <td>{title}</td>
        <td>{price}</td>
      </tr>
    </tbody>
  </table>

  <script type="es6">
    var self = this;
    self.stocks = opts.stocks;

    opts.bus.on('newStocksEvent', function (param) {
        self.stocks = param.stocks;
        self.update();
    });
  </script>
</stockmarket-table>

We start by binding the stocks received when the component is mounted to an internal stocks property on which the each directive is going to iterate through. Then at the reception of the event, we update the value of this field with the new value received and call the update() function. This function is a Riot.js component function that tells the component to update its UI now (like $scope.digest() or $scope.apply() in AngularJS).

The code of the stockmarket-barchart is a bit longer but in essence, it’s the same principle: we subscribe to the ‘newStocksEvent’ with a callback to update the chart. Hence an updateChart(.) function:

<stockmarket-barchart>
  <h3>{opts.title}</h3>
  <svg class="chart" id="{chartId}"></svg>

  <script type='es6'>
    var self = this;
    this.chartId = opts.chart_id || 'chart-1';

    ...

    opts.bus.on('newStocksEvent', function (param) {
      var data = param.stocks.map(s => {
        return {'title': s.title, 'price': s.price};
      });

      updateChart(data, self.chartId, opts.height);

      self.update();
    });

    function updateChart(data, id = 'chart-1', aHeight = 500) {
      var margin = {top: 20, right: 30, bottom: 30, left: 40};
      var height = aHeight - margin.top - margin.bottom;
      var y = d3.scaleLinear()
             .range([height, 0]);
      y.domain([0, d3.max(data, function (d) {
        return d.price;
      })]);

      var chart = d3.select("#" + id);
      chart.selectAll("rect")
           .data(data)
           .transition()
           .duration(1000)
           .attr("y", function (d) {
             return y(d.price);
           })
           .attr("height", function (d) {
             return height - y(d.price);
           });
    }
  </script>
</stockmarket-barchart>

In all the cases, we need to call the update() function of the component to refresh its UI.

A small note about D3.js: to update a barchart, we select all the rectangles of the chart and set the new data via the data(.) D3.js directive. Some few links help to understand this: here and here.

The index.html changes a little bit too. We don’t want to mount the two components until we receive the first set of stocks. So, we mount them at the reception of the first ‘newStocksEvent’:


<body>
  ...

  <!-- mount normally -->
  <script>
  var bus = riot.observable();
  var stockMarketService = new StockMarketService(bus);
  var areMounted = false;

  riot.mount('stockmarket-button', {title: 'Load Stocks', stockMarketService: stockMarketService});

  bus.on('newStocksEvent', function(param) {
    if (!areMounted) {
      riot.mount('stockmarket-table', {title: 'Stocks', bus: bus, stocks: param.stocks});
      riot.mount('stockmarket-barchart', {title: 'Graph', bus: bus, stocks: param.stocks});
      areMounted = true;
    }
  })
  </script>
</body>

Conclusion

In this section, we’ve seen the use of an observable. We use it as an event bus but it’s not the only pattern usage for the observables. As you can see, using observables helps to keep the code loosely-coupled and is really easy to use.
So, what’s next? Remember, I promise a surprise… Discover it in the next post!

Oh, by the way, the code can be found on our GitHub (eventDrivenApp branch, tag step2).

 

Want more about riot.js?

Exploring Riot.js – Introduction

Exploring Riot.js – Get your hands dirty

Exploring Riot.js – Event-driven app (Step1)

Exploring Riot.js – Event-driven app (Step2) … you are here

Exploring Riot.js – Event-driven app (Step3)

Exploring Riot.js – En route to…

Exploring Riot.js – Route 66 (Takeaway)

 

Share it :
0002

Give it a try!

Try streaming any JSON REST API within 30 sec
curl -v "https://proxy.streamdata.io/http://mysite.com/myJsonRestService?param1=[]&param2=[]"

Leave a Reply

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