Build a realtime table with Ruby on Rails and PostgreSQL
A basic understanding of Ruby and CoffeeScript will help you with this tutorial. You need to already have Ruby and Rails installed on your machine, as well as PostgreSQL.
The ability to update data in realtime is one of the top requirements for web applications in today’s world. Users prefer realtime update as opposed to constantly refreshing a page to see new information.
With that being said, implementing realtime update of information in any web application can be challenging and has to be done right. This post aims to show you how to achieve that with the help of Pusher in a Ruby application.
A quick look at what we’ll be building:
Prerequisites
A basic understanding of PostgreSQL, Ruby and CoffeeScript will help you with this tutorial. You should also have PostgreSQL installed. Kindly check the PostgreSQL, Ruby and Rails documentation for further installation steps.
Setting up the application
Before starting off, ensure you have both Ruby and Rails installed. Run the following code in your terminal:
$ ruby -v // 2.1 or above
$ rails -v // 4.2 or above
Now lets run the command to setup the app:
$ rails new pusher-tables -T --database=postgresql
The command above creates a new Rails application. The -T
flag excludes generation of test files as we won’t be writing any tests.
Go ahead and change directory into the newly created folder:
$ cd pusher-tables
In the root of your pusher-tables
directory, open your Gemfile
and add the following gems:
# Gemfile
gem 'bootstrap', '~> 4.1.0'
gem 'jquery-rails'
gem 'pusher'
gem 'figaro'
In your terminal, ensure you’re in the project directory and install the gems by running:
$ bundle install
Database setup
Next, we have to create a database called pusher-tables_development
for our app. Check out this article on how to create a Postgres database and an associated user and password.
In your database.yml
file, under the development
key, add the following code:
# config/database.yml
...
development:
<<: *default
database: pusher-tables_development // add this line if it isn't already there
username: database_user // add this line
password: user_password // add this line
...
The username and password in the code above should have access to the pusher-tables_development
database. After that, run the following code to setup the database:
# setup database
$ rails db:setup
See it working
After installing the gems, start the development server by running rails s
and visit http://localhost:3000 in your browser. You should see the following:
Pusher account setup
Head over to Pusher and sign up for a free account. A Pusher account is required for the realtime functionality in our app.
Create a new app by selecting Channels apps on the sidebar, then clicking the Create Channels app button on the bottom of the sidebar:
Configure an app by providing basic information requested in the form presented. You can choose the environment you intend to integrate Pusher with to be provided with boilerplate code for that framework:
You can retrieve your keys from the App Keys tab:
Now that you have your Pusher API keys, head over to config/initializers
directory. Create a pusher.rb
file and add the following code:
# config/initializers/pusher.rb
require 'pusher'
Pusher.app_id = ENV["PUSHER_APP_ID"]
Pusher.key = ENV["PUSHER_KEY"]
Pusher.secret = ENV["PUSHER_SECRET"]
Pusher.cluster = ENV["PUSHER_CLUSTER"]
Pusher.logger = Rails.logger
Pusher.encrypted = true
In your terminal, run figaro install
. It’ll generate an application.yml
file. In the application.yml
file add your Pusher keys:
# config/application.yml
PUSHER_APP_ID: 'xxxxxx'
PUSHER_KEY: 'xxxxxxxxxxxxxxxxx'
PUSHER_SECRET: 'xxxxxxxxxxxxxx'
PUSHER_CLUSTER: 'xx'
Setting up the model and controller
With our app up and running, we’ll use Rails’ scaffolding feature to quickly setup our employees model and controller:
// generate employee model and controllers
$ rails g scaffold employee name:string position:string office:string extension:string start_date:date
// run database migration
$ rails db:migrate
Setting up the landing page
After running those commands, restart your server and reload your browser page. You should still see the welcome page. We’re about to change all that.
Add the following code to your route file:
# config/routes.rb
...
root 'employees#index'
...
With that, your landing page should now contain a table with headings. Let’s style it up with Bootstrap.
In your application.js
file, add the following code just before the last line:
// app/assets/javascripts/application.js
.....
//= require jquery3 # add this line
//= require popper # add this line
//= require bootstrap # add this line
//= require_tree .
....
Rename your application.css
file to application.scss
, import Bootstrap and add some custom syles:
// app/assets/stylesheets/application.scss
@import "bootstrap";
@import url('https://fonts.googleapis.com/css?family=Josefin+Sans');
body {
font-family: 'Josefin Sans', sans-serif;
}
.navbar-brand {
color: #FFF !important;
&:hover {
background-color: unset;
}
}
.form-control {
outline: none !important;
&:focus {
border-color: transparent !important;
border: 1px !important;
}
}
If we restart our server and reload the app, we should see that the fonts have changed courtesy of Bootstrap.
If you encounter a RegExp error while trying to set up Bootstrap, In config/boot.rb
, change the ExecJS runtime from Duktape to Node.
# config/boot.rb
ENV['EXECJS_RUNTIME'] ='Node'
Setting up the table
To setup our table, we’ll be using Bootstrap’s table . Replace the code in your index.html.erb
file with the following:
<%# app/views/employees/index.html.erb %>
<div class='container-fluid'>
<p id="notice"><%= notice %></p>
<div class='row py-3 px-3'>
<div class='col-12 col-sm-8'>
<h3 class=''>Employees</h3>
<table id='employee-table' class="table table-hover">
<thead class="thead-light">
<tr>
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Extension</th>
<th>Start date</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @employees.each do |employee| %>
<tr>
<td><%= employee.name %></td>
<td><%= employee.position %></td>
<td><%= employee.office %></td>
<td><%= employee.extension %></td>
<td><%= employee.start_date %></td>
<td><%= link_to 'Show', employee %></td>
<td><%= link_to 'Edit', edit_employee_path(employee) %></td>
<td><%= link_to 'Delete', employee, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
</div>
<div class='col-12 col-sm-4'>
<h3>New Employee</h3>
<%= render 'form', employee: @employee %>
</div>
</div>
</div>
Update the index method of the employees controller, to include the following:
# app/controllers/employees_controller.rb
class EmployeesController < ApplicationController
...
def index
@employees = Employee.all
@employee = Employee.new # add this line
end
...
end
The code above adds Bootstrap styling to the table on our landing page. It also instantiates a new employee record in the index
method of the employees controller. If you reload the landing page, you should see the table and form in all its glory.
Adding new records to the table
Currently adding a new employee record redirects us a different page. We’d like to handle that with AJAX. To do that, we add remote: true
and format: :json
to our form. Replace the code in_form.html.erb
with the following:
// app/views/employees/_form.html.erb
<%= form_with(model: employee, remote: true, format: :json, id: 'add_employee') do |form| %>
<% if employee.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(employee.errors.count, "error") %> prohibited this employee from being saved:</h2>
<ul>
<% employee.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name, id: :employee_name, class: "form-control" %>
</div>
<div class="field">
<%= form.label :position %>
<%= form.text_field :position, id: :employee_position, class: "form-control" %>
</div>
<div class="field">
<%= form.label :office %>
<%= form.select :office, [['Lagos', 'Lagos'], ['New York', 'New York',], ['Lisbon', 'Lisbon']], {}, { class: "form-control", id: "employee_office" } %>
</div>
<div class="field">
<%= form.label :extension %>
<%= form.text_field :extension, id: :employee_extension, class: "form-control" %>
</div>
<div class="field">
<%= form.label :start_date %>
<%= form.date_field :start_date, id: :employee_start_date, class: "form-control" %>
</div>
<div class="actions">
<%= form.submit 'Create Employee', class: "btn btn-success" %>
</div>
<% end %>
Now that our form uses AJAX, we’ll handle updating the table with CoffeeScript.
# app/assets/javascripts/employees.coffee
updateTable = (data) ->
$('#employee-table tbody').append """
<tr>
<td>#{data.name}</td>
<td>#{data.position}</td>
<td>#{data.office}</td>
<td>#{data.extension}</td>
<td>#{data.start_date}</td>
<td><a href="/employees/#{data.id}">Show</a></td>
<td><a href="/employees/#{data.id}/edit">Edit</a></td>
<td><a data-confirm="Are you sure?" rel="nofollow" data-method="delete" href="/employees/#{data.id}">Delete</a></td>
</tr>
"""
return
$ ->
$('#add_employee').on 'ajax:success', (data) ->
$('#add_employee')[0].reset()
updateTable data.detail[0]
return
return
In the above code, we add an ajax:success
event listener to our form courtesy of jQuery-ujs. Whenever we add new employee records, we get our newly added employee as a response and update our table with it.
Lets reload our page and add a few employees, they should pop up on the page as soon as we hit submit.
Realtime service with Pusher
To update our table realtime, we publish the new employee record to a channel and subscribe to it on the frontend of our app. In the employee model, we add an after_commit
callback which calls a method that publishes the new record. We use after_commit
so data is published whenever we add or update a record.
Add the following code to the employee model:
# app/models/employee.rb
class Employee < ApplicationRecord
after_commit :notify_pusher, on: [:create, :update]
def notify_pusher
Pusher.trigger('employee', 'new', self.as_json)
end
end
Here, our initialized Pusher client triggers an event called new
through a channel named employee.
Updating the UI
So the server is pushing data out on each change, now it’s up to the client to listen for those changes and do something with that data. To do this, we rename our employees.coffee
file to employees.coffee.erb
so we can sprinkle some Ruby code in it.
Update the file with the following code:
# app/assets/javascripts/employees.coffee.erb
updateTable = (data) ->
$('#employee-table tbody').append """
<tr>
<td>#{data.name}</td>
<td>#{data.position}</td>
<td>#{data.office}</td>
<td>#{data.extension}</td>
<td>#{data.start_date}</td>
<td><a href="/employees/#{data.id}">Show</a></td>
<td><a href="/employees/#{data.id}/edit">Edit</a></td>
<td><a data-confirm="Are you sure?" rel="nofollow" data-method="delete" href="/employees/#{data.id}">Delete</a></td>
</tr>
"""
return
$ ->
$('#add_employee').on 'ajax:success', (data) ->
$('#add_employee')[0].reset()
return
pusher = new Pusher('<%= ENV["PUSHER_KEY"] %>',
cluster: '<%= ENV["PUSHER_CLUSTER"] %>'
encrypted: true)
channel = pusher.subscribe('employee')
channel.bind 'new', (data) ->
updateTable data
return
return
Lastly, add the Pusher library inside the head tag in the application.html.erb
file just before the javascript_include_tag
:
<%# app/views/layouts/application.html.erb %>
<head>
<title>RealtimeTable</title>
<%= csrf_meta_tags %>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<script src="https://js.pusher.com/4.1/pusher.min.js"></script> # add this line
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
Bringing it all together
Restart the development server if it is currently running. Check your page on http://localhost:3000
Conclusion
So far, we have learnt how to leverage the realtime functionality provided by Pusher to display updates in realtime. Feel free to explore more by visiting Pusher’s documentation. Lastly, the complete source code of this demo application is on Github.
19 May 2018
by Christian Nwamba