[Rails]コメント機能を非同期通信(Ajax)、ついでにエラーメッセージとフラッシュメッセージも非同期化

#はじめに

コメント機能を非同期にしていく中で、投稿、削除は非同期にできたけど、バリデーションをかけた場合のエラーメッセージ、フラッシュメッセージの表示まで解説した記事がなかなか見つからなくて、時間がかかったので１つの記事にまとめて書いておきます。
初心者の方の参考になれば幸いです。

#この記事の対象となる人

- コメント投稿、削除機能を非同期にしたい人
- 他の記事を参考にして実装したが、エラーメッセージ、フラッシュメッセージが動かなくて困っている人
- １つの記事だけを参考にしてコメント機能を非同期にしたい人

#開発環境

> ruby 2.6.3
> Rails 5.2.6

#前提

同期通信でのコメント機能は実装済みの状態から解説していきます。

挙動としては、下記を想定
コメントを投稿　→　コメントが保存され、フラッシュメッセージが表示
コメントを削除　→　コメントが削除され、フラッシュメッセージが表示
空でコメントを投稿　→　バリデーションがかかって、エラーメッセージが表示

モデル名、変数名等は適宜、ご自身の開発環境に変換してお考えください。

user  ユーザー
post  投稿
comment  コメント
で進めていきます。

#現状のコードの確認

解説は省略しますが、現状のコードの確認をしていきます。
まずはモデルから

```user.rb
:
  has_many :post, dependent: :destroy
  has_many :comments, dependent: :destroy
:
```

```post.rb
:
  belongs_to :user
  has_many :comments, dependent: :destroy
:
```

```comment.rb
class Comment < ApplicationRecord

  belongs_to :user
  belongs_to :post

  validates :comment, presence: true
end
```

次にコントローラー

```comments_controller.rb
class CommentsController < ApplicationController

  def create
    @post = Post.find(params[:post_id])
    @comment = current_user.comments.new(comment_params)
    @comment.post_id = @post.id
    if @comment.save
      redirect_to post_path(@post), notice: 'コメントを投稿しました'
    else
      render 'posts/show'
    end
  end

  def destroy
    Comment.find_by(id: params[:id], post_id: params[:post_id]).destroy
    redirect_to post_path(params[:post_id]), alert: 'コメントを削除しました'
  end

  private

  def comment_params
    params.require(:comment).permit(:comment)
  end

end
```

```posts_controller.rb
:
def show
  @post = Post.find(params[:id])
  @comment = Comment.new
end
:
```

最後にビュー

```erb:posts/show.html.erb
<div class="post-body">
  <%= attachment_image_tag @post, :image, :fill, 200, 200 %>
  <p>説明：<%= @post.content %></p>
  <p>ユーザーネーム：<%= @post.user.name %></p>
  <p>投稿日時：<%= @post.created_at.strftime('%Y/%m/%d') %></p>
  <% if @post.user == current_user %>
    <%= link_to "削除", post_path(@post), method: :delete %>
  <% end %>
  <!--いいね機能-->
  <div class="likes_buttons_<%= @post.id %>">
    <%= render 'likes/like', post: @post %>
  </div>
</div>
<!--コメントフォーム-->
<div class="new-comment">
  <%= form_with(model:[@post, @comment], local: true) do |f| %>
    <!--エラーメッセージ-->
    <%= render 'layouts/error_messages', model: f.object %>
    <%= f.text_area :comment, rows:'5',placeholder: "コメントをここに" %>
    <%= f.submit "送信する" %>
  <% end %>
</div>
<!--コメント一覧部分-->
<div class="comments">
  <% if @post.comments.present? %>
  <p>コメント件数：<%= @post.comments.count %></p>
  <% @post.comments.each do |comment| %>
    <%= comment.user.name %>
    <%= comment.created_at.strftime('%Y/%m/%d') %><%= comment.comment %>
    <% if comment.user == current_user %>
      <div class="comment-delete">
        <%= link_to "削除", post_comment_path(comment.post, comment), method: :delete, data: {confirm: "本当に削除しますか？"} %>
      </div>
    <% end %>
  <% end %>
  <% else %>
  <p>コメントはまだありません</p>
  <% end %>
</div>
```

```erb:layouts/_error_messages.html.erb
<!--エラーメッセージ-->
<% if model.errors.any? %>
  <div class="alert alert-danger">
    <ul class="list-unstyled">
      <h5><%= model.errors.count %>件のエラー</h5>
      <% model.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>
```

```erb:layouts/application.html.erb
:
<main>
  <!--フラッシュメッセージ-->
  <p class="notice"><%= notice %></p>
  <p class="alert"><%= alert %></p>
  <%= yield %>
</main>
:
```

これが同期通信のコメント機能を実装済みのコードです。（レイアウトはかなり適当なので適宜、変更ください）
ではコメント機能を非同期にしていきましょう！

#ポイントの整理
ポイントは全部で４つ

- 部分テンプレートにする
- ｊｓ対応にする
- 対応するjsファイルを作成
- コントローラーのリダイレクトを変更

上記ポイント押さえておきましょう！

#部分テンプレートにする

まずは、コメント機能のフォーム、コメント一覧の部分を部分テンプレートにして、divタグで囲います。
このdivタグで囲ったコメント一覧の部分を非同期で、更新していきます。

```erb:comments/_form.html.erb
<%= form_with(model:[post, comment], local: true) do |f| %>
  <!--エラーメッセージ-->
  <%= render 'layouts/error_messages', model: f.object %>
  <%= f.text_area :comment, rows:'5',placeholder: "コメントをここに" %>
  <%= f.submit "送信する" %>
<% end %>
```

```erb:comments/_post_comments.html.erb
<% if post.comments.present? %>
  <p>コメント件数：<%= post.comments.count %></p>
  <% post.comments.each do |comment| %>
    <%= comment.user.name %>
    <%= comment.created_at.strftime('%Y/%m/%d') %><%= comment.comment %>
    <% if comment.user == current_user %>
      <div class="comment-delete">
        <%= link_to "削除", post_comment_path(comment.post, comment), method: :delete, data: {confirm: "本当に削除しますか？"} %>
      </div>
    <% end %>
  <% end %>
<% else %>
  <p>コメントはまだありません</p>
<% end %>
```

```erb:posts/show.html.erb
:
<!--部分テンプレート化-->
<!--コメントフォーム-->
<div class="new-comment">
  <%= render 'comments/form', post: @post, comment: @comment %>
</div>
<!--コメント一覧部分-->
<!--divタグのclassをid="comments_area"に変更-->
<div id="comments_area"> 
  <%= render 'comments/post_comments', post: @post %>
</div>
:
```

#js対応にする

form_withとlink_toのデフォルトがhtml形式なので、jsファイルを探しに行ってくれるように、変更していきます。

form_withは`local: trueをつけるとhtmlファイル`を、`local: trueをつけないとjsファイルを探しに行ってくれる`ので、`form_withのlocal: trueを外します。`

```erb:comments/_form.html.erb
<!--local: trueを削除-->
<%= form_with(model:[post, comment]) do |f| %>
  <!--エラーメッセージ-->
  <div id="comments-error">
    <%= render 'layouts/error_messages', model: f.object %>
  </div>
  <%= f.text_area :comment, rows:'5',placeholder: "コメントをここに" %>
  <%= f.submit "送信する" %>
<% end %>
```

削除ボタンのlink_toに`remote: true`を追加

```erb:comments/_post_comments.html.erb
:
<!--remote: trueを追加-->
<%= link_to "削除", post_comment_path(comment.post, comment), method: :delete, remote: true, data: {confirm: "本当に削除しますか？"} %>
:
```

#対応するjsファイルをつくる

ここまでで、非同期で更新したい範囲、非同期で更新するためjsファイルを探しに行くようにすることができました。
まだ探しに行くjsファイルが無いので作成していきます。
`views/comments直下`にpost_comments.js.erbを作成します。

```erb:views/comments/post_comments.js.erb
<!--id="comments_areaにをrenderで指定した部分テンプレートに更新-->
$("#comments_area").html("<%= j(render 'comments/post_comments', post: @post, comment: @comment) %>");
<!--フォームを送信したあとにフォームの値を空にします-->
$("textarea").val('');
```

#コントローラーのリダイレクトを変更

最後にコントローラーのリダイレクトの部分をrenderでjsファイルを指定します。

```controllers/comments_controller.rb
def create
    @post = Post.find(params[:post_id])
    @comment = current_user.comments.new(comment_params)
    @comment.post_id = @post.id
    if @comment.save
      flash.now[:notice] = 'コメントを投稿しました'
      render :post_comments  #render先にjsファイルを指定
    else
      render 'posts/show'
    end
  end

  def destroy
    Comment.find_by(id: params[:id], post_id: params[:post_id]).destroy
    flash.now[:alert] = '投稿を削除しました'
    #renderしたときに@postのデータがないので@postを定義
    @post = Post.find(params[:post_id])  
    render :post_comments  #render先にjsファイルを指定
  end
```

これで、非同期でのコメントの投稿、削除ができました！
終了です。ありがとうございました。

ここまでの内容なら他の記事でもよくありましたが、肝心の空で送信したときのエラーメッセージ、投稿に成功したとき、削除したときのフラッシュメッセージが待てど暮らせど出てきません。。

ではさっそく非同期に対応させていきましょう！

#エラーメッセージの非同期化

エラーメッセージは投稿がバリデーションチェックに引っかかったとき（今回は空のコメント）に、表示させたいので、コメント機能を作ったときとは別のjsファイルを作って、コメントが保存されなかったときにだけ更新させるようにします。

手順は3つです。
- エラーメッセージの部分テンプレートをdivタグで囲う
- 囲ったdivタグを対象に非同期で更新させる
- コントローラーのrender先をｊｓファイルを指定

```erb:comments/_form.html.erb
<%= form_with(model:[post, comment]) do |f| %>
  <!--エラーメッセージをdivタグで囲む-->
  <div id="comments-error">
    <%= render 'layouts/error_messages', model: f.object %>
  </div>
  <%= f.text_area :comment, rows:'5',placeholder: "コメントをここに" %>
  <%= f.submit "送信する" %>
<% end %>
```

次に`views/comments直下`にerror.js.erbを作成します。

```erb:views/comments/error.js.erb
$("#comments-error").html("<%= j(render 'layouts/error_messages', model: @comment) %>");
```

最後にコントローラーのrender先を変更します。

```controllers/comments_controller.rb
:
def create
  @post = Post.find(params[:post_id])
  @comment = current_user.comments.new(comment_params)
  @comment.post_id = @post.id
  if @comment.save
    flash.now[:notice] = 'コメントを投稿しました'
    render :post_comments
  else
    render :error  #render先にjsファイルを指定
  end
end
:
```

これでバリデーションに引っかかったときにエラーメッセージが非同期で実装できました。

#フラッシュメッセージの非同期化

最後にフラッシュメッセージも非同期で実装していきます。
フラッシュメッセージはコメントの投稿に成功したとき、削除したときに表示したいので、`post_comments.js.erb`にフラッシュメッセージの記述を追加していきます。

手順は２つです。
- フラッシュメッセージを部分テンプレート化してdivタグで囲う
- 囲ったdivタグを対象に非同期で更新させる

```erb:views/layouts/application.html.erb
:
<main>
  <!--フラッシュメッセージの部分をdivタグで囲む-->
  <div id="flash-message">
    <%= render 'laouts/flash' %>
  </div>
  <%= yield %>
</main>
:
```

divタグで囲めたらid名を対象にｊｓファイルに追加します

```erb:comments/post_comments.js.erb
$("#comments_area").html("<%= j(render 'comments/post_comments', post: @post, comment: @comment) %>");
$("textarea").val('');
<!--下記を追加-->
$('#flash-message').html("<%= j(render 'layouts/flash') %>");
```

これでコメントを投稿、削除したときにフラッシュメッセージが表示されるはずです。

ただ１つ注意点として、flash[:notice]のままだと投稿したり削除したりを何度もしたときに、前のフラッシュメッセージが残り続けてしまうので、`flash.now`にしておいてください。

#最後に

自分が実装する際に、エラーメッセージ、フラッシュメッセージまで同じ記事で解説している記事が見つからず、かなり時間がかかってしまいましたので、今回書いて見ようと思いました。
エラーメッセージ、フラッシュメッセージが表示されなくて苦労したので、同じように苦労している方の参考になれたら幸いです。
かなり長くなってしまいましたが、最後まで見ていただきありがとうございます。