Build a live comments app with Ruby on Rails and PostgreSQL
You will need Ruby, Rails and PostgreSQL installed on your machine.
Introduction
In this post, we will explore and build a basic realtime commenting application. We will create a fake post and assume that users read it and decided to leave a comment.
This is similar to what is obtainable in a real-world application, where you can have live commenting features implemented in your project and comments added by readers will update in realtime without the need to refresh the page.
This basically increases the level of engagement and allows for a proper interaction and participation from users. By the time we are done, our application will look like this:
This application will be built with Ruby on Rails and the realtime functionality fully powered by Pusher.
Prerequisites
A basic understanding of Ruby, CoffeeScript and PostgresSQL will help you with this tutorial. Ensure that you have PostgreSQL installed on your machine. Kindly check the documentation of PostgreSQL, Ruby and Rails for further installations procedures.
Creating the live comment application
It is important that you have both Ruby and Rails installed. Run the following code in your terminal to confirm the successful installation of both Ruby and Rails:
$ ruby -v // 2.1 or above
$ rails -v // 4.2 or above
Secondly, we will run a command that will create a new Rails application in a folder called live-comments-pusher
. Run the command below in your terminal:
$ rails new live-comments-pusher -T --database=postgresql
The -T
flag was added to prevent generation of the default testing suite as we won’t be writing any tests.
Go ahead and change directory into the newly created folder:
$ cd live-comments-pusher
In the root directory of this application, open the Gemfile
and add the following gems:
# Gemfile
gem 'bootstrap', '~> 4.1.0'
gem 'jquery-rails'
gem 'pusher'
gem 'figaro'
and install them:
$ bundle install
Database setup
To set up the database for our application, create a database called live-comments-pusher_development
. 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: live-comments-pusher_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 live-comments-pusher_development
database. Next, run the following code to setup the database:
# setup database
$ rails db:setup
Starting the web server
Start the development server by running rails s
from the terminal and visit http://localhost:3000 in your browser. Your page should look like this:
Signing up with Pusher
Head over to Pusher and sign up for a free account.
Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:
Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher with, to be provided with boilerplate code:
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
The command above will generate an application.yml
file. Locate the newly generate file in config/application.yml
and add the Pusher credentials obtained from your dashboard:
# config/application.yml
PUSHER_APP_ID: 'YOUR_APP_ID'
PUSHER_KEY: 'YOUR_APP_KEY'
PUSHER_SECRET: 'YOUR_APP_SECRET'
PUSHER_CLUSTER: 'CLUSTER'
Setting up the model and controller
With our application up and running, we’ll use Rails’ scaffolding feature to quickly set up our comment model and controller:
# generate comment model and controller
$ rails g scaffold comment message:text
# run database migration
$ rails db:migrate
Setting up the homepage
Restart the development 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 'comments#index'
...
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 styles:
// 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;
}
}
#wrapper {
padding-top: 15px;
}
p {
line-height: 1.3125rem;
}
.comments {
margin: 2.5rem auto 0;
max-width: 60.75rem;
padding: 0 1.25rem;
}
.comment-wrap {
margin-bottom: 1.25rem;
display: table;
width: 100%;
min-height: 5.3125rem;
}
.photo {
padding-top: 0.625rem;
display: table-cell;
width: 3.5rem;
}
.photo .avatar {
height: 2.25rem;
width: 2.25rem;
border-radius: 50%;
background-size: contain;
}
.comment-block {
padding: 1rem;
background-color: #f5f5f5;
display: table-cell;
vertical-align: top;
border-radius: 0.1875rem;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
}
.comment-block textarea {
width: 100%;
resize: none;
}
.comment-text {
margin-bottom: 1.25rem;
}
.bottom-comment {
color: #acb4c2;
font-size: 0.875rem;
}
.comment-actions {
float: right;
}
.comment-actions li {
display: inline;
margin: -2px;
cursor: pointer;
}
.comment-actions li.show {
padding-right: 0.75rem;
border-right: 1px solid #e1e5eb;
}
.comment-actions li.destroy {
padding-left: 0.75rem;
padding-right: 0.125rem;
}
At the moment, if we restart the development server and reload the application, we should notice changes in the fonts of our page, this is because we have successfully included Bootstrap.
If you encounter any errors relating to application.html.erb
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'
Set up the view
To set up the view, replace the code in your index.html.erb
file with the following:
<%# app/views/comments/index.html.erb %>
<div id="wrapper" class="container">
<div class="text-center">
<h1>Sample post</h1>
<p>This is an interesting sample post for Pusher </p>
</div>
<div id="comments" class="comments">
<% @comments.each do |comment| %>
<div class="col-6 offset-3">
<div class="comment-wrap">
<div class="photo">
<div class="avatar" style="background-image: url('http://res.cloudinary.com/yemiwebby-com-ng/image/upload/v1525202285/avatar_xcah9z.svg')"></div>
</div>
<div class="comment-block">
<p class="comment-text"><%= comment.message %></p>
<div class="bottom-comment">
<ul class="comment-actions">
<li class="show"><%= link_to 'Show', comment %></li>
</ul>
</div>
</div>
</div>
</div>
<% end %>
</div>
<div class='col-6 offset-4 col-sm-4'>
<%= render 'form', comment: @comment %>
</div>
</div>
Update the index method of the comments controller, to include the following:
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
...
def index
@comments = Comment.all
@comment = Comment.new # add this line
end
...
end
The code above adds Bootstrap styling to the table on our landing page. It also instantiates a new comment record in the index
method of the comments controller. If you reload the landing page, you should see the form.
Post a comment
Currently adding a new comment record redirects us to a different page. We would like to handle that with AJAX instead. To achieve this, add remote: true
and format: :json
to your form. Replace the code in _form.html.erb
with the following:
// app/views/comments/_form.html.erb
<%= form_with(model: comment, remote: true, format: :json, id: 'add_comment') do |form| %>
<% if comment.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(comment.errors.count, "error") %> prohibited this comment from being saved:</h2>
<ul>
<% comment.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :message %>
<%= form.text_area :message, id: :comment_message, class: "form-control" %>
</div>
<div class="actions">
<%= form.submit 'Add comment', class: "btn btn-success"%>
</div>
<% end %>
Now that our form uses AJAX, we’ll handle updating the page with CoffeeScript. Open app/assets/javascripts/comments.coffee
and paste in the code below:
# app/assets/javascripts/comments.coffee
addComment = (data) ->
$('#comments').append """
<div class="col-6 offset-3">
<div class="comment-wrap">
<div class="photo">
<div class="avatar" style="background-image: url('http://res.cloudinary.com/yemiwebby-com-ng/image/upload/v1525202285/avatar_xcah9z.svg')"></div>
</div>
<div class="comment-block">
<p class="comment-text">#{data.message}</p>
<div class="bottom-comment">
<ul class="comment-actions">
<li class="show"><a href="/comments/#{data.id}">Show</a></li>
</ul>
</div>
</div>
</div>
</div>
"""
return
$ ->
$('#add_comment').on 'ajax:success', (data) ->
$('#add_comment')[0].reset()
addComment data.detail[0]
return
return
In the above code, we added an ajax:success
event listener to our form courtesy of jQuery-ujs. Whenever we add a new comment, we get our newly added comment as a response and update our page with it.
Let’s reload our page and add a few comments, they should pop up on the page as soon as we hit submit.
Realtime service with Pusher
To update our page in realtime, we will publish the new comment record to a channel and subscribe to it on the frontend of our app. In the comment model, we’ll add an after_commit
callback, which will call a method that will publish the new comment. We’ll use after_commit
so data is published whenever we add or update a comment.
Add the following code to the comment model:
# app/models/comment.rb
class Comment < ApplicationRecord
after_commit :notify_pusher, on: [:create, :update]
def notify_pusher
Pusher.trigger('comment', 'new', self.as_json)
end
end
Here, our initialized Pusher client triggers an event called new
through a channel named comment
.
Updating the frontend of the application
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’ll rename our comments.coffee
file to comments.coffee.erb
so we can sprinkle some Ruby code in it.
Update the file with the following code:
# app/assets/javascripts/comments.coffee.erb
addComment = (data) ->
$('#comments').append """
<div class="col-6 offset-3">
<div class="comment-wrap">
<div class="photo">
<div class="avatar" style="background-image: url('http://res.cloudinary.com/yemiwebby-com-ng/image/upload/v1525202285/avatar_xcah9z.svg')"></div>
</div>
<div class="comment-block">
<p class="comment-text">#{data.message}</p>
<div class="bottom-comment">
<ul class="comment-actions">
<li class="show"><a href="/comments/#{data.id}">Show</a></li>
</ul>
</div>
</div>
</div>
</div>
"""
return
$ ->
$('#add_comment').on 'ajax:success', (data) ->
$('#add_comment')[0].reset()
return
pusher = new Pusher('<%= ENV["PUSHER_KEY"] %>',
cluster: '<%= ENV["PUSHER_CLUSTER"] %>'
encrypted: true)
channel = pusher.subscribe('comment')
channel.bind 'new', (data) ->
addComment 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>NewNestcomments</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= 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>
Testing the application
Restart the development server if it is currently running. Check your page on http://localhost:3000:
Conclusion
In this tutorial, we have been able to tap into one of the realtime services offered by Pusher to build a live comments application. This can be improved on by adding extra features as you deem fit. The complete source code for this application can be found here on GitHub.
27 May 2018
by Christian Nwamba