Assumptions: The application already exists. You have two models article.rb
and comment.rb
. Articles have two attributes, title
and text
. Comments have two attributes, text
and article_id
. See these instructions if you need help getting started.
Assuming that you are nesting your :comments
resources inside of :articles
, mount ActionCable
and make sure you have a root.
config/routes.rb
Rails.application.routes.draw do
resources :articles do
resources :comments
end
mount ActionCable.server => '/cable'
root 'articles#index'
end
We don't want ActionCable
or anything else within our flow to raise an error before we have a chance to record the submitted comment. To protect against this, we're going to render our comment via a background job. First, update the model to use an after_create_commit. The commit part is important.
app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :article
after_create_commit { RenderCommentJob.perform_later self }
end
Rails 5 has made the ApplicationController and its methods more widely available. This makes rendering so much easier. We're going to utilize this to render our new comment.
app/jobs/render_comment_job.rb
class RenderCommentJob < ApplicationJob
queue_as :default
def perform(comment)
ActionCable.server.broadcast "article:#{comment.article_id}:comments", foo: render_comment(comment)
end
private
def render_comment(comment)
ApplicationController.renderer.render(partial: 'comments/comment', locals: { comment: comment })
end
end
Our controller is simple. We're going to create our comment and allow the js to render itself.
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
before_action :set_article
def create
@comment = Comment.create! text: params[:comment][:text], article: @article
end
private
def set_article
@article = Article.find(params[:article_id])
end
end
Okay, this is the bread and butter. We're getting into the loop that is ActionCable. Simply put, this is a two way street between client and server. It goes something like this:
- The client loads the url, creating a channel set to
App.foo
App.foo.connected()
is automatically called. This is where we can do necessary things like get resource ids or start/stop streams. Streams are subscriptions to a certain redis channel e.g.articles:1:comments
.App.foo
would correspond to a server side channel i/echannels/foo_channel.rb
and could call methods and pass arguments using@perform
example:@perform 'speak', message: "hello world"
=>FooChannel.speak message: "hello world"
.- Although, ActionCable is a two way street, we don't have to always use both directions. This means that the client can send back information and not expect a response, or as shown in our example, once a connection is established, the server can send messages prompted by other parts of the application such as a new database entry. It does this by using
ActionCable.server.broadcast()
e.g.ActionCable.server.broadcast "some_channel", message: "something happened"
.
Our server CommentsChannel
will do two primary things; control the current stream and stop all streams if comments are not applicable on the current page.
app/channels/comments_channel.rb
class CommentsChannel < ApplicationCable::Channel
def follow(params)
stop_all_streams
stream_from "article:#{params['article_id'].to_i}:comments"
end
def unfollow
stop_all_streams
end
end
The client CommentsChannel
is there to initiate the subscription and to alert the server of any changes in page status i/e the client navigates away from the current article.
app/assets/javascripts/channels/comments.coffee
App.comments = App.cable.subscriptions.create "CommentsChannel",
collection: -> $('#comments')
connected: ->
setTimeout =>
@followCurrentArticle()
, 1000
disconnected: ->
followCurrentArticle: ->
articleId = @collection().data('article-id')
if articleId
@perform 'follow', article_id: articleId
else
@perform 'unfollow'
received: (data) ->
@collection().append(data['comment'])
Head over to app/assets/javascripts/cable.coffee and uncomment the two lines at the bottom.
@App ||= {}
App.cable = ActionCable.createConsumer()
We are going to rely on Rails to render partials and handle cacheing of comments. Two important things to note. The form is set to remote: true
. This lets us use a view to render the new comment from CommentsController.create
with minimal code and without doing anything special outside of writing create.js.erb
app/views/comments/create.js.erb
$('#new_comment').replaceWith('<%=j render 'comments/new', article: @article %>');
_app/views/comments/comment.html.erb
<% cache comment do %>
<div class="comment">
<p>
<%= comment.text %>
</p>
</div>
<% end %>
_app/views/comments/comments.html.erb
<%= render 'comments/new', article: article %>
<section id="comments" data-article-id="<%= @article.id %>">
<%= render @article.comments %>
</section>
_app/views/comments/new.html.erb
<%= form_for [ @article, Comment.new ], remote: true do |f| %>
<%= f.text_area :text, size: '100x20' %><br>
<%= f.submit 'Add comment' %>
<% end %>
This article is based on the video tutorial by DHH, his actioncable-examples, and the public README available with ActionCable.