Search code examples
ruby-on-railsrubyactioncable

How to first render a partial in broadcasts_to and then redirect (AbstractController::DoubleRenderError)


I'm new to Rails and was trying to use Action Cable to update clients when a new record is saved in database.

To do so, I have this posts_controller.rb:

  # POST /posts
  # POST /posts.json
  def create
    @post = Post.new(post_params)
    picture = params[:post][:picture]
    respond_to do |format|
      if @post.save
        @post.picture.attach(picture) if picture.present?
        PostsChannel.broadcast_to @post, html: render(partial: 'post', locals: { post: @post }) #<--- render
        format.html { redirect_to posts_url, notice: 'Post was successfully created.' } #<--- redirect
        format.json { render :show, status: :created, location: @post }
      else
        format.html { render :new }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

And _post.html.erb:

<div class="cd-timeline__block js-cd-block">
  <div class="cd-timeline__img cd-timeline__img--picture js-cd-img">
    <p>😃</p>
  </div>

  <div class="cd-timeline__content js-cd-content">
    <%= image_tag post.picture.variant(resize: '400x400') %>
    <p><%= post.text %></p>
    <span class="cd-timeline__date"><%= post.created_at.strftime('%d/%m/%Y %T') %></span>
  </div>
</div>

This ends up with the error AbstractController::DoubleRenderError because of the partial render and the redirect. The thing is that I have no idea how to do this otherwise. I don't want to generate the HTML in JS file as saw is some code examples(I would have a code duplication for my post template). I've been looking for other examples but nothing really helped me out.

The same problem was discussed here and here but I don't see how to use flash to get what I want.

Any advice?


Solution

  • Ok, so I got it working! :)

    There is a rails function that does exactly what I want: render_to_string which doesn't send http response. I had some issues with websockets so I installed Redis + changed the way I was broadcasting and now everything is working fine!

    For the records, here is all of it:

    # config/routes.rb
    Rails.application.routes.draw do
      resources :posts
      mount ActionCable.server => '/cable'
    end
    

    -

    # config/cable.yml
    development:
      adapter: redis
      url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
    

    -

    # app/channels/posts_channel.rb
    class PostsChannel < ApplicationCable::Channel
      def subscribed
        stream_for 'post'
      end
    end
    

    -

    // app/assets/javascripts/channels/posts.js
    App.cable.subscriptions.create('PostsChannel', {
      received: function({ html }) {
        $("#timeline-container").prepend(html);
      }
    });
    

    -

    <!-- app/views/posts/_post.html.erb -->
    <div class="cd-timeline__block js-cd-block">
      <div class="cd-timeline__img cd-timeline__img--picture js-cd-img">
        <p>😃</p>
      </div>
    
      <div class="cd-timeline__content js-cd-content">
        <%= image_tag post.picture.variant(resize: '400x400') %>
        <p><%= post.text %></p>
        <span class="cd-timeline__date"><%= post.created_at.strftime('%d/%m/%Y %T') %></span>
      </div>
    </div>
    

    -

    # POST /posts
    # POST /posts.json
    def create
      @post = Post.new(post_params)
      picture = params[:post][:picture]
      respond_to do |format|
        if @post.save
          @post.picture.attach(picture) if picture.present?
          ActionCable.server.broadcast 'posts:post', html: render_to_string(partial: 'post', locals: { post: @post })
          format.html { redirect_to posts_url, notice: 'Post was successfully created.' }
          format.json { render :show, status: :created, location: @post }
        else
          format.html { render :new }
          format.json { render json: @post.errors, status: :unprocessable_entity }
        end
      end
    end