Pushing notifications to the browser can easily be achieved by streaming JSON data to the client. An HTTP connection is kept open between the server and the browser and on reception of events, the web page is updated to reflect the change.
Streaming JSON data has become easier than ever using Rails 4 and its ActionController::Live. Coupling that with AngularJS ability to react to events, you get a very simple way of handling Server Sent Events in your web application.
Lets say we want to display a subset of the share market prices updated periodically where new values will be "pushed" by the server to the browser in a JSON format. Using a push mechanism is this scenario will keep the user informed as soon as market prices are updated.
In this example, we will simulate data update by generating new market values every 5 seconds, storing them in a database and streaming the new data to the client.
1. Generate a Rails controller including ActionController:Live and returning event-stream content type
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SharesController < ApplicationController | |
include ActionController::Live | |
Mime::Type.register "text/event-stream", :stream | |
def index | |
response.headers['Content-Type'] = 'text/event-stream' | |
# Stream content here | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def index | |
response.headers['Content-Type'] = 'text/event-stream' | |
begin | |
loop do | |
response.stream.write "data: #{generate_new_values}\n\n" # Add 2 line breaks to delimitate events | |
sleep 5.second | |
end | |
rescue IOError # Raised when browser interrupts the connection | |
ensure | |
response.stream.close # Prevents stream from being open forever | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def generate_new_values | |
now = Time.now | |
current_values = [] | |
companies = Company.all(order: :code) | |
companies.each do |company| | |
previous_share = Share.where(company: company).order('timestamp DESC').first | |
new_value = previous_share.value + (rand().round(2) - 0.5) | |
share = Share.create(company: company, value: new_value, timestamp: Time.now) | |
variation = share.value > previous_share.value ? 'up' : 'down' | |
current_values << {company: company, share: share, variation: variation} | |
end | |
current_values.to_json | |
end |
For reference, the model looks like
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create_table "companies", force: true do |t| | |
t.string "code" | |
t.string "name" | |
t.datetime "created_at" | |
t.datetime "updated_at" | |
end | |
create_table "shares", force: true do |t| | |
t.integer "company_id" | |
t.decimal "value" | |
t.datetime "timestamp" | |
t.datetime "created_at" | |
t.datetime "updated_at" | |
end |
HTML Markup:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
... | |
<div class="container shares" id="shares" ng-controller="SharesCtrl" ng-init="init()"> | |
<div class="share-container" ng-repeat="entry in entries"> | |
<div class="share-code">{{entry.company.code}}</div> | |
<div class="share-name">{{entry.company.name}}</div> | |
<div class="share-value">${{entry.share.value}}</div> | |
<div class="share-variation"><img ng-src="{{entry.variation == 'up' && 'up.png' || 'down.png'}}"></div> | |
</div> | |
</div> | |
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var shareModule = angular.module('shares', []); | |
shareModule.factory('Shares', function() { | |
return {}; | |
}); | |
shareModule.controller('SharesCtrl', function($scope, Shares) { | |
$scope.init = function() { | |
var source = new EventSource('/shares'); | |
source.onmessage = function(event) { | |
$scope.$apply(function () { | |
$scope.entries = JSON.parse(event.data) | |
}); | |
}; | |
}; | |
}); |
On page load, the EventSource object opens a stream with the server and on each message received, AngularJS is notified to apply those changes to the model and view.
The next version of Rails (4.1) will provide a ActionController::Live::SSE which would reduce even more the amount of effort needed to stream events.
Example source code on Github