Techioz Blog

ネストされたコメントの場合、親コメントの ID をコメント フォームに渡すにはどうすればよいですか?

概要

コメントが記事を直接参照している場合、次の形式でコメントを作成することに成功しました。

<%= form_with(model: [@article, @article.comments.build]) do |f| %>
 
 <p>Comment:</p>
 <%= f.text_area :body, class:'' %>
 <%= image_submit_tag("Post.jpg", height: "25", width: "25", class: "mt-5 ms-3") %>
<% end %>

しかし、今、ネストされたコメントを作成しようとしていますが、以前に作成したコメントへの返信としてフォームにアクセスするとき、親コメントのIDをフォームに渡す方法、またはnil親を渡す方法がわからずに立ち往生しています。 -コメント ID。記事自体からフォームに直接アクセスし、その記事を参照している場合。

app/views/articles/show.html.erb では、記事に直接コメントを作成するためにコメント フォームが呼び出されます。これは私が試したことです。

<%= render '/comments/comment_form', locals: {article: @article, parent: nil} %>

app/views/comments/show.html.erb では、以前に作成したコメントへの返信としてフォームが呼び出されますが、これは私が試したことです:

 <%= render '/comments/comment_form', locals: {article: comment.article, parent: comment} %>

ネストされたコメントを作成する前に、何も渡さず (ローカル: {etc.} なし)、articles/show.html.erb からフォームをレンダリングしただけで、見事に機能しました。

この件に関して何かご指導をいただければ幸いです。以下は、この問題に関連すると思われるコードの残りの部分です。

app/models/comments.rb 内

class Comment < ApplicationRecord
  belongs_to :article
  belongs_to :commenter, class_name: 'User', foreign_key: :user_id
  belongs_to :parent, class_name: 'Comment', optional: true

  has_many :comments, foreign_key: :parent_id

  validates_presence_of :body, length: { minimum: 3, maximum: 500 }
end

app/controllers/comments_controller.rb 内

class CommentsController < ApplicationController
  before_action :authenticate_user!

  

  def create
    @comment = Comment.new(comment_params.merge(commenter: current_user))
    @comment.article_id = params[:article_id]
    @comment.save
    redirect_to article_path(params[:article_id])
  end

  private

  def comment_params
    params.require(:comment).permit(:body, :article_id, :user_id, :parent_id)
  end
end

解決策

インスタンス変数とローカル変数を混同していますが、これは非常に簡単です。

@ 記号が付いたものはすべて、Ruby のインスタンス変数です。 Rails での動作方法は、コントローラー内のすべてのインスタンス変数が、テンプレート エンジンに渡されるコンテキストであるビュー コンテキストを介してビューで利用できるようにすることです。それは基本的にあなたのビューの中の自己であると考えてください。

一方、ローカル割り当ては、render を呼び出すときに渡され、通常のメソッドとして、または local_assigns メソッドを通じて呼び出されます。 Rails はメタプログラミングを通じてこれらの呼び出しをビュー コンテキストに委任します。これらの呼び出しには @ 記号を含めないでください。また、ビューローカルを言語機能であるローカル変数と混同しないように注意してください。

それでは、実際にローカルを使用するように部分を変更することから始めましょう。

<%= form_with(model: [local_assigns(:parent), comment]) do |f| %>
  # ..
<% end %>

local_assigns(:parent) は、ローカルが渡されない場合に NoMethodError を回避する気の利いたちょっとしたトリックで、浅いネストで同じフォームを再利用可能にするために使用できます。

また、フォーム内で @article.comments.build を実行しないでください。モデルのインスタンス化は、ビューではなくコントローラーの仕事です。また、フォームは常に新しいインスタンスにバインドされます。つまり、フォームが無効で編集のためにフォームを壊した場合、ユーザー入力はすべて失われます。

コントローラー側では、複数の異なるリソースにネストできるリソースを処理する 2 つの異なる方法があります。 1 つ目の方法は、すべてを 1 つのコントローラーにルーティングし、「パラメーター スニッフィング アプローチ」を使用することです。

class CommentsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_parent
  
  # Creates articles nested in a Article or in another Comment
  # POST /articles/1/comments
  # POST /comments/2/comments
  def create
    @comment = @parent.comments.new(comment_params) do |c|
      c.user = current_user
    end
    # don't assume that saving will always be successful
    if @comment.save
      redirect_to @parent
    else
      render :new
    end
  end

  private

  def set_parent
    if params[:article_id].present?
      @parent = Article.find(params[:article_id])
    elsif params[:comment_id].present?
      @parent = Comment.find(params[:comment_id])
    end
  end

  def comment_params
    # you should not be permitting any of those params!
    params.require(:comment).permit(:body)
  end
end

これは人気がありますが、複雑さが増すにつれて非常に醜くなるため、私はこのアプローチの大ファンではありません。また、単一責任原則も完全に無視しています。たとえば、親のタイプに応じて異なる方法でリダイレクトすることが必要な場合があります。

2 番目のアプローチは、合成を使用して別のコントローラーを作成することです。

resouces :articles do 
  resouces :comments, only: :create, module: :articles
end

resouces :comments, only: [] do 
  resouces :comments, only: :create, module: :comments
end
# Creates Comments on an Article 
class CommentsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_parent
  
  # Creates comments nested in a parent record. 
  # This parent record should be set by subclasses. 
  def create
    @comment = @parent.comments.new(comment_params) do |c|
      c.user = current_user
    end
    # don't assume that saving will always be successful
    if @comment.save
      redirect_to @parent
    else
      render :new
    end
  end

  private

  def set_parent
    raise "This method must be implemented by subclasses!"
  end

  def comment_params
    # you should not be permitting any of those params!
    params.require(:comment).permit(:body)
  end
end
module Articles 
  # Creates Comments on an Article 
  class CommentsController < ::CommentsController 
    private

    def set_parent
      @parent = Article.find(params[:article_id])
    end
  end
end
module Comments 
  # Creates Comments nested in another Comment 
  class CommentsController < ::CommentsController 
    private

    def set_parent
      @parent = Comment.find(params[:comment_id])
    end
  end
end