It is a common requirement in modern web applications to have the client view responding to server events. By doing so, end users can be notified in real-time of any relevant state or data update. Supporting push notifications in your web applications is a way of making them more interactive, respond better to server events, and ultimately deliver an improved user experience.
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 the ability of AngularJS to react to events, you get a very simple way of handling Server Sent Events in your web application.
Let’s 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 in this scenario will keep the user informed as soon as market prices are updated.
In this example, we will simulate a data update by generating new market values every 5 seconds, storing them in a database and streaming the new data to the client.
- Generate a Rails controller including ActionController:Live and returning event-stream content type
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 |
- Use the method response.stream.write to push data to the client
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 |
- Generate new market prices and a price variation indicator (up or down) in JSON formatFor reference, the model looks like
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
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 |
- On the client side we now use AngularJS to open a stream with the server and process events.HTML Markup:AngularJS Controller
- HTML Markup:
… <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> | |
… |
- AngularJS Controller
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 will further reduce the amount of effort needed to stream events.
This article Server Sent Events with Rails 4 and AngularJS was originally published on my Blog here.