Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Rails 5 and ActionCable

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.

Routes

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

Models

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

Jobs

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

Controllers

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

Channels

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:

  1. The client loads the url, creating a channel set to App.foo
  2. 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.
  3. App.foo would correspond to a server side channel i/e channels/foo_channel.rb and could call methods and pass arguments using @perform example: @perform 'speak', message: "hello world" => FooChannel.speak message: "hello world".
  4. 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".

Server/Channels

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

Client/Channels

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'])

Assets

Head over to app/assets/javascripts/cable.coffee and uncomment the two lines at the bottom.

@App ||= {}
App.cable = ActionCable.createConsumer()

Views

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 %>

Acknowledgements

This article is based on the video tutorial by DHH, his actioncable-examples, and the public README available with ActionCable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment