Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save davidpaulhunt/9bc21bbf792cb3565315 to your computer and use it in GitHub Desktop.
Save davidpaulhunt/9bc21bbf792cb3565315 to your computer and use it in GitHub Desktop.

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.

@diogowernik
Copy link

Hi, thanks for the tutorial, is great, i could not make it work yest
I would like to know how is the view at the end for:

app/views/articles/show.html.erb

I think i am missing something ;)

@lethunder
Copy link

hello,

this is a great tuto. I'm trying now to manage rights so that the author of the comments can edit or delete his comment. Any ideas?

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