Skip to content

Instantly share code, notes, and snippets.

Revisions

  1. @davidpaulhunt davidpaulhunt revised this gist Dec 28, 2015. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion stream-comments-with-rails-5-and-action-cable.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    # 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`.
    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](https://gist.github.com/davidpaulhunt/a185d1753e438c505227) 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.
  2. @davidpaulhunt davidpaulhunt revised this gist Dec 28, 2015. 1 changed file with 0 additions and 2 deletions.
    2 changes: 0 additions & 2 deletions stream-comments-with-rails-5-and-action-cable.md
    Original file line number Diff line number Diff line change
    @@ -153,8 +153,6 @@ $('#new_comment').replaceWith('<%=j render 'comments/new', article: @article %>'
    <%= f.submit 'Add comment' %>
    <% end %>
    ```
    ## Conclusion
    ActionCable is a really useful tool. WebSockets have grown in popularity and use cases. It makes sense for Rails to establish a convention for generating and using them within the framework.

    #### Acknowledgements
    This article is based on the [video tutorial](https://www.youtube.com/watch?v=n0WUjGkDFS0) by DHH, his [actioncable-examples](https://github.com/rails/actioncable-examples), and the [public README](https://github.com/rails/rails/tree/master/actioncable) available with ActionCable.
  3. @davidpaulhunt davidpaulhunt revised this gist Dec 28, 2015. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion stream-comments-with-rails-5-and-action-cable.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    # ActionCable
    # 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`.

  4. @davidpaulhunt davidpaulhunt revised this gist Dec 28, 2015. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions stream-comments-with-rails-5-and-action-cable.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    # 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`.

    #### Routes
  5. @davidpaulhunt davidpaulhunt revised this gist Dec 28, 2015. 1 changed file with 0 additions and 3 deletions.
    3 changes: 0 additions & 3 deletions stream-comments-with-rails-5-and-action-cable.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,3 @@
    # Stream Comments with Rails 5 and ActionCable
    Scenario: You have a personal blog application that allows readers to leave anonymous comments. You want the comments to appear dynamically. Rather than bringing in a third party library, you want to use Rails 5 and it's ActionCable feature.

    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`.

    #### Routes
  6. @davidpaulhunt davidpaulhunt created this gist Dec 28, 2015.
    161 changes: 161 additions & 0 deletions stream-comments-with-rails-5-and-action-cable.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,161 @@
    # Stream Comments with Rails 5 and ActionCable
    Scenario: You have a personal blog application that allows readers to leave anonymous comments. You want the comments to appear dynamically. Rather than bringing in a third party library, you want to use Rails 5 and it's ActionCable feature.

    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`.

    #### Routes
    Assuming that you are nesting your `:comments` resources inside of `:articles`, mount `ActionCable` and make sure you have a root.

    <small>_config/routes.rb_</small>
    ```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.

    <small>_app/models/comment.rb_</small>
    ```ruby
    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.

    <small>_app/jobs/render_comment_job.rb_</small>
    ```ruby
    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.

    <small>_app/controllers/comments_controller.rb_</small>
    ```ruby
    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.

    <small>_app/channels/comments_channel.rb_</small>
    ```ruby
    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.

    <small>_app/assets/javascripts/channels/comments.coffee_</small>
    ```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 <small>_app/assets/javascripts/cable.coffee_</small> and uncomment the two lines at the bottom.
    ```coffee
    @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_

    <small>_app/views/comments/create.js.erb_</small>
    ```html
    $('#new_comment').replaceWith('<%=j render 'comments/new', article: @article %>');
    ```
    <small>_app/views/comments/_comment.html.erb_</small>
    ```html
    <% cache comment do %>
    <div class="comment">
    <p>
    <%= comment.text %>
    </p>
    </div>
    <% end %>
    ```
    <small>_app/views/comments/_comments.html.erb_</small>
    ```html
    <%= render 'comments/new', article: article %>

    <section id="comments" data-article-id="<%= @article.id %>">
    <%= render @article.comments %>
    </section>
    ```
    <small>_app/views/comments/_new.html.erb_</small>
    ```html
    <%= form_for [ @article, Comment.new ], remote: true do |f| %>
    <%= f.text_area :text, size: '100x20' %><br>
    <%= f.submit 'Add comment' %>
    <% end %>
    ```
    ## Conclusion
    ActionCable is a really useful tool. WebSockets have grown in popularity and use cases. It makes sense for Rails to establish a convention for generating and using them within the framework.

    #### Acknowledgements
    This article is based on the [video tutorial](https://www.youtube.com/watch?v=n0WUjGkDFS0) by DHH, his [actioncable-examples](https://github.com/rails/actioncable-examples), and the [public README](https://github.com/rails/rails/tree/master/actioncable) available with ActionCable.