ruby-on-railshotwire-railsturbo

Rails 7 turbo frames and multiple forms issue with the form value


I'm very new to Rails. I've found this strange issue related to turbo_frame and multiple new forms.

I have a model Post with comments and when I try to load multiple forms the value from one form appears on the others even if I specify to use Comment.new

###posts/index.html.erb

<div id="posts">
  <% @posts.each do |post| %>
    <div class="post">
    <%= turbo_frame_tag dom_id(post) do %>
        <%= render 'posts/show', post: post)%>
    <% end %>
    </div>
    <hr>
  <% end %>
</div>


### posts/_show.html.erb
  <% post.comments.each do |comment| %>
    <%= render "comments/comment", post: post, comment: comment %>
  <% end %>
  <%= turbo_frame_tag "comments_#{post.id}" do %>
  <% end %>

  <%= render "comments/add_new_comment", post: post, comment: Comment.new %>
### _add_new_comment.html.erb
<%= turbo_frame_tag "comments_new#{post.id}" do %>
   <%= link_to "Add comment", open_create_new_comment_path(post) %>
<% end %>
### comments/_comment.html.erb
<%= turbo_frame_tag dom_id(comment) do %>
  <div>
    <%= comment.body %>
    <div>
      <%= link_to 'Edit', edit_post_comment_path(comment.post, comment) %>
      <%= link_to 'Delete', post_comment_path(comment.post, comment),
                method: :delete,
                data: { confirm: 'Are you sure?', turbo_method: :delete } %>
    </div>
  </div>
<% end %>
### comments/_form.html.erb
<%= turbo_frame_tag "new_comment_form_#{post.id}" do %>
  <%= form_with(model: [post, Comment.new]) do |form| %>
    <%= form.rich_text_area :body %>
    <%= form.hidden_field :post_id, value: post.id %>
    <%= form.submit "Submit", data: { disable_with: "Submitting..." } %>
  <% end %>
<% end %>

And finally the controller

class CommentsController < ApplicationController
  before_action :set_post
  before_action :set_comment, only: [:edit, :update, :destroy]

  def edit
    render turbo_stream: turbo_stream.replace("comment_#{params[:id]}", partial: "comments/edit_form", locals: {post: @post, comment: @comment})
  end

  def open_create_new
    render turbo_stream: turbo_stream.replace("comments_new#{@post.id}", partial: "comments/form", locals: {post: @post, comment: Comment.new})
  end

  def create
    @comment = Comment.new(comment_params)

    if @comment.save
      respond_to do |format|
        format.turbo_stream do
          render turbo_stream: [
            turbo_stream.prepend("comments_#{@post.id}", partial: "comments/comment", locals: {comment: @comment}),
            turbo_stream.replace("new_comment_form_#{@post.id}", partial: "comments/add_new_comment", locals: {post: @post})
          ]
        end
      end

    else
      respond_to do |format|
        format.turbo_stream { render turbo_stream:turbo_stream.replace("new_comment_form_#{@post.id}", partial: "comments/add_new_comment", locals: {post: @post})}
      end
    end
  end

  # PATCH/PUT /comments/1 or /comments/1.json
  def update
    if @comment.update(comment_params)
      render turbo_stream: turbo_stream.replace("edit_comment_form_#{@comment.id}", partial: "comments/comment", locals: {comment: @comment})
    else
      render turbo_stream: turbo_stream.replace("edit_comment_form_#{@comment.id}", partial: "comments/comment", locals: {comment: @comment})
    end
  end

  # DELETE /comments/1 or /comments/1.json
  def destroy
    @comment.destroy
    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: turbo_stream.remove("comment_#{params[:id]}")
      end
    end
  end

  private

  # Use callbacks to share common setup or constraints between actions.
  def set_comment
    @comment = Comment.find(params[:id])
  end

  def set_post
    @post = Post.friendly.find_by(slug: params[:post_id])
  end

  # Only allow a list of trusted parameters through.
  def comment_params
    params.require(:comment).permit(:body, :post_id, :parent_id)
  end
end

The strange is each form works perfectly individually, but if I load the form from post 1 input some text, and then open the form to add comments to the other's posts the text is the same as I put in form 1, and when I try to submit the form the logs say the body is empty.

Ideas ? why the content from one is added to the others when I put all in locals and use Comment.new


Solution

  • This is a javascript issue with rich text field, you need to have unique ids for every rich text field on the page, you can use :namespace option of form_with helper:

    <%= form_with model: [post, Comment.new], namespace: dom_id(post) do |form| %>
      <%= form.rich_text_area :body %>
    <% end %>
    
    <input type="hidden" name="comment[body]" id="post_2_comment_body_trix_input_comment" autocomplete="off">
    <!--                               namespace: ^^^^^^  -->
    

    Drop turbo_frame_tags, you're not actually using them:

    <!-- app/views/posts/index.html.erb -->
    
    <div id="posts">
      <%= render @posts %>
    </div>
    
    <!-- app/views/posts/_post.html.erb -->
    
    <div id="<%= dom_id post %>">
    
      <!-- will append post's comments here -->
      <div id="<%= dom_id post, :comments %>">
        <%= render post.comments %>
      </div>
    
      <!-- will update with new comment form then back to link -->
      <div id="<%= dom_id post, :new_comment %>">
        <%= link_to "Add comment", new_post_comment_path(post), data: {turbo_stream: true} %>
      </div>
    
    </div>
    
    <!-- app/views/comments/_form.html.erb -->
    
    <%= form_with model: [post, Comment.new], namespace: dom_id(post) do |form| %>
      <%= form.rich_text_area :body %>
      <%= form.submit "Submit", data: {turbo_submits_with: "Submitting..."} %>
    <% end %>
    
    <!-- app/views/comments/_comment.html.erb -->
    
    <div id="<%= dom_id comment %>">
      <%= comment.body %>
      <div>
        <%= link_to "Edit", edit_post_comment_path(comment.post, comment), data: {turbo_stream: true} %>
        <%= link_to "Delete", post_comment_path(comment.post, comment), data: {turbo_confirm: "Are you sure?", turbo_method: :delete} %>
      </div>
    </div>
    
    # app/controllers/comments_controller.rb
    
    def new
      respond_to do |format|
        format.turbo_stream do
          render turbo_stream: turbo_stream.update(
            [@post, :new_comment],
            partial: "comments/form",
            locals: {post: @post, comment: Comment.new}
          )
        end
      end
    end
    
    def create
      @comment = @post.comments.new(comment_params)
      if @comment.save
        respond_to do |format|
          format.turbo_stream do
            render turbo_stream: [
              turbo_stream.append(
                [@post, :comments],
                partial: "comments/comment",
                locals: {comment: @comment}
              ),
              turbo_stream.update(
                [@post, :new_comment],
                helpers.link_to("Add comment", new_post_comment_path(@post), data: {turbo_stream: true})
              )
            ]
          end
        end
      else
        respond_to do |format|
          format.turbo_stream do
            render turbo_stream: turbo_stream.update(
              [@post, :new_comments],
              partial: "comments/form",
              locals: {comment: @comment, post: @post}
            )
          end
        end
      end
    end
    
    def edit
      respond_to do |format|
        format.turbo_stream do
          render turbo_stream: turbo_stream.update(
            @comment,
            partial: "comments/form",
            locals: {comment: @comment, post: @post}
          )
        end
      end
    end
    
    def update
      respond_to do |format|
        if @comment.update(comment_params)
          format.turbo_stream do
            render turbo_stream: turbo_stream.update(@comment)
          end
        else
          format.turbo_stream do
            render turbo_stream: turbo_stream.update(
              @comment,
              partial: "comments/form",
              locals: {comment: @comment, post: @post}
            )
          end
        end
      end
    end
    
    def destroy
      @comment.destroy
      respond_to do |format|
        format.turbo_stream { render turbo_stream: turbo_stream.remove(@comment) }
      end
    end