銀行員からのRailsエンジニア

銀行員からのRailsエンジニア

銀行員から転身したサービス作りが大好きなRailsエンジニアのブログです。個人で開発したサービスをいくつか運営しており、今も新しいサービスを開発しています。転職して日々感じていること、個人開発サービス運営のことなどを等身大で書いていきます。

Ajaxを用いた動的なコメント投稿・削除機能の実装で学ぶRuby on Rails

1. はじめに

ブログなどの投稿ページにマストなコメント投稿、削除機能をAjaxを用いて動的に作ってみました。コメント投稿、削除でいちいちページ遷移するより圧倒的に使いやすいです。

なんとか実装できましたが、めちゃめちゃ時間かかって苦労しました。。。
ネット上に自分の求めていたジャストの記事が無かったので、今回の実装をまとめてみました!

決意のツイート:


ツイッター始めてまだ一週間くらいですが、今までで一番いいねをいただいたのでこの記事頑張ろうと思いました。ありがとうございます。)

実装の内容自体は基本的なことであり現役プログラマーの方からすれば当たり前のことばかりだと思いますが、色々と調べながら実装したことを書いてみます。
細かい部分まで極力書いているので一つでも勉強になることがあれば嬉しいです。

デザインはかなりダサくて恥ずかしいですが、完成のイメージはこんな感じです。(もちろんこれからかっこよくするところです)
現在Mrサンプルさんでログイン中の為、下のコメントのみ削除ボタンが出ており、コメント・削除ボタンを押すと、ページ遷移することなくコメント投稿・削除ができ、スムーズにコメント投稿・削除ができます。

f:id:ysk_pro:20180207211057p:plain

投稿ページの実装、JQueryの読み込みはできている前提です。
さていってみましょう!

※バージョンは、Ruby:2.3.0、Rails:5.1.4です。

まずは、コメントモデル・コメントコントローラーを作成します。
私はまとめてscaffoldで作成しました。(不要なものもできてしまうのですが問題ありません)

2. スキーマ(コメントテーブル作成)

config/db/schema.rb

  create_table "comments", force: :cascade do |t|
    t.text "content"
    t.integer "post_id"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

どの投稿に対するコメントであるかを格納するために「post_id」、誰の投稿であるかを格納するために「user_id」カラムの作成が必要です。


3. モデル(アソシエーション設定)

app/models/comment.rb

  belongs_to :user
  belongs_to :post
  validates :content, presence: true

ユーザーはたくさんのコメントを持てる、投稿はたくさんのコメントは持てると、一対他(ユーザー・投稿:一、コメント:他)の関係になっているので、コメントのモデルは「berongs_to」でのアソシエーションになります。

コメントの内容が無いとコメントの意味が無いので、コメント内容必須のバリデーションを設定しています。

app/models/post.rb

  belongs_to :user
  has_many :comments, dependent: :destroy

ユーザーはたくさんの投稿を持てる、投稿はたくさんのコメントを持てる、の関係なので投稿のアソシエーションはこのようになります。

「dependent: :destroy」は、この場合「投稿が削除された時に、同時にコメントも消去する」という意味です。
コメントだけ残ってしまっても意味が無いので記載が必要です。


app/models/user.rb

  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy

同様の考え方です。


4. ルーティング

config/routes.rb

  resources :posts do
    resources :comments
  end

コメントがどの投稿へのものであるかを識別するために、ルーティングのURLに投稿のIDを含める必要があります。
具体的には「/post/12/comment/」といったURLになります。(ネストする、と言います。)
12がpost_idです。

参考
Rails のルーティング | Rails ガイド


5. コメントコントローラー

app/controllers/comment.rb

  def create
    @post = Post.find(params[:post_id]) #①
    @comment = @post.comments.build(comment_params) #②
    @comment.user_id = current_user.id #③
    if @comment.save
      render :index #④
    end
  end

  def destroy
    @comment = Comment.find(params[:id]) #⑤
    if @comment.destroy
      render :index #⑥
    end
  end

  private
    def comment_params
      params.require(:comment).permit(:comment_content, :post_id, :user_id)
    end

createアクション:
#①:コメントをする対象の投稿(post)のインスタンスを作成します。

#②:「.build」を使うことで、@postのidをpost_idに含んだ形でcommentインスタンスを作成します。
「.new」で普通にインスタンスを作成して、次の行でpost_idを入れても同じです。

参考
build - リファレンス - - Railsドキュメント


#③:現在のuserのidを入れます。

#④:保存がされると、render :indexによって「app/views/comments/index.js.erb」を探しにいきます。
「form_with」でフォームを送信した時は、デフォルトでjsファイルを探しにいく設定になっています。
htmlファイルを探しにいってほしい場合には、form_withの後に「local: true」と記載する必要があります。

参考:form_withについて
rails-ujs と form_with の使い方 - ボクココ

参考:renderについて
レイアウトとレンダリング | Rails ガイド


destroyアクション:
#⑤:削除する対象のコメントインスタンスを探します。

#⑥:削除がされると、「index.js.erb」を探しにいきます。
削除のリンクを記載している「link_to」の中に「remote: true」を記載していることでjsファイルを探しにいってくれます。
7. app/views/comments/_index.html.erb に記載しています。
「remote: true」を記載していなかった場合は、htmlファイルを探しにいきます。


6. 投稿のコントローラー

app/controllers/posts_controller.rb

  def show
    @post = Post.find(params[:id])
    @comment = Comment.new #①
    @comments = @post.comments #②
  end

どちらも、7. 投稿のビュー「app/views/posts/show.html.erb」でパーシャルに渡す変数として使用します。
#①:入力フォームで使用するインスタンスを作成しています。

#②:コメント一覧表示で使用するためのコメントデータを入れています。


7. 投稿のビュー

app/views/posts/show.html.erb

  <div>
    <h4>コメント</h4>
    <div id="comments_area"><!-- #① -->
      <!-- 投稿されたコメント一覧をブログの詳細ページに表示するためのrender -->
      <%= render partial: 'comments/index', locals: { comments: @comments } %>
    </div>  
    <% if user_signed_in? %>
      <!-- コメント入力欄をブログの詳細ページに表示するためのrender -->
      <%= render partial: 'comments/form', locals: { comment: @comment, post: @post } %>
    <% end %>
  </div>

#①:「id="comments_area"」がポイントです。
このidをターゲットにして、このdiv内をAjaxで書き換えます。
このdivの内側に、renderを使ってパーシャルを表示します。
@commentをパーシャル内で使うローカル変数commentとして渡しています。

参考:パーシャルを利用するときのrenderの使い方について
render - リファレンス - - Railsドキュメント

参考:ローカル変数について
Railsの部分テンプレートからインスタンス変数を参照するのはやめよう。 | CreativeStyle

どちらもとても勉強になりました。


8. パーシャル部分のビュー

app/views/comments/_index.html.erb

<% comments.each do |comment| %>
  <% unless comment.id.nil? %>
    <p><%= link_to "#{comment.user.name}さん", user_path(comment.user.id) %></p>
    <p>コメント:<%= comment.content %></p>
    <% if comment.user == current_user %>
      <p><%= link_to 'コメントを削除する', post_comment_path(comment.post_id, comment.id), method: :delete, remote: true %></p>
    <% end %>
  <% end %>
<% end %>

パーシャルはファイル名の先頭に「_」を入れます。
投稿のビューから渡したローカル変数(comments)を comment に入れて一つずつ表示しています。

ポイントは、コメントの削除のところで「(comment.post_id, comment.id)」と投稿のidとコメントのidを渡す必要があることと、5. コメントコントローラーのところでも触れましたが「remote: true」をつけることによって、コントローラーでjsファイルを探しにいってもらうことです。
idをcomment.post_idとcomment.idの2つ渡す必要があるのは、削除したいコメントを指定するには「post/12/comment/31」のようにpost_idとcomment_idを指定する必要があるためです。


app/views/comments/_form.html.erb

<%= form_with(model: [post, comment] ) do |form| %>
  <div>
    <%= form.text_area :comment_content %>
  </div>
  <div class="actions">
    <%= form.submit "コメントをする" %>
  </div>
<% end %>

ポイントは、「model: [post, comment]」とすることです。
post, commentはそれぞれ、7. 投稿のビューで渡しているインスタンスのローカル変数です。
投稿に紐づいたコメントを生成するため、ここでpost、commentのインスタンスを渡すことが必要になります。


9. jsファイル

app/views/comments/index.js.erb

$("#comments_area").html("<%= j(render 'index', { comments: @comment.post.comments }) %>")
$("textarea").val('')

とてもシンプルです。
このファイルに、7. 投稿のビューの中で id = "comments_area"とした箇所を書き換える処理を記載しています。

「$("#comments_area")」が id = "comments_area"をターゲットとする記載です。
ターゲットとした箇所を、「render 'index'」で指定している8. パーシャル部分のビューの内容で書き換えています。

{ comments: @comment.post.comments }で、@comment.post.comments をローカル変数 comments に入れて渡しています。
@comment.post.comments は、コメント一覧表示するのに必要なコメント全件です。

「$("textarea").val('')」によって、コメント入力後のコメント入力欄を空にしています。


10. 最後に

以上で、Ajaxを用いた動的なコメント投稿・削除機能が実装できたはずです!
※分かりやすくするために、デザイン面のbootstrapに関するコードは消しています。

間違っている箇所、分かりにくい箇所等あれば是非教えてください!

ブログを書いてみた感想:


Twitterは自分と同じような境遇、目指すべき人たちと気軽に繋がれるのがすごいいいですよね!始めてみて良かったです。)

次は、いいね機能をAjaxを用いて動的することと、コメントに対しての返信機能をつけようと思っています。
ジャストな記事がなかったらまたブログ書こうと思います!!
(リクエスト等あればお答えしたいです。もし、もし感想、ご意見等いただければ次回も頑張ろうと思えます、、、笑)

11. 追記

Ajaxを用いた動的ないいね機能・コメントへの返信機能の実装が完了しました!コメント返信機能・またその動的な実装はジャストな記事がなかったので、次の記事にしようと思います!
f:id:ysk_pro:20180212170706p:plain