対話型コメント欄の非同期通信


はじめに

現在、TECHCAMPというプログラミングスクールに通っています。フリマのクローンアプリを開発中ですが、コメント欄を実装することにしました。フリマアプリのコメント欄は「出品者」と「それ以外の方」の対話風の見た目にしたので、これを非同期通信化するにあたり、工夫した点を中心に書いていきます。

達成したいこと

コメント欄を非同期通信で更新し、出品者とそれ以外の方(お客様)でビューの見た目が分岐するようにしたいです。完成見本は以下のとおりです。

★お客様が投稿した場合

★出品者が投稿した場合

手順

今回は、通常のコメント投稿は完了している前提で、その後の非同期化に絞って手順を書いていきます。

  • コントローラーのcreateアクションの編集
  • jbuilderファイルの新規作成
  • jsファイルの作成

コントローラーのcreateアクションの編集

ポイントは、2点です。

1点目は、createアクション時にインスタンス変数「@item」と「@comment」を作っているという点です。@itemは、コメントした商品のレコードを抽出し、@commentは、保存したコメントのレコードを格納しています。これらのインスタンス変数は、この後、jbuilderでjson化するために使用します。

2点目は、respond_to内に「format.html」を追加し、万が一jQueryが読み込まれなかった場合にHTML形式のフォーマットでもレスポンスできるようにしています。後述しますが、turbolinksを併用しているとjsファイルがうまく読み込まれない場面があります。もちろんきちんと管理できればベストですが、初めてのチーム開発であるという方も多いと思うので、こういった保険も必要ではないかと思い、記述しました。

comments_controller.rb
class CommentsController < ApplicationController
  def create
    @item = Item.find(params[:item_id])
    @comment = Comment.create(comment_params)
    respond_to do |format|
      format.html { redirect_to item_path(params[:item_id]) }
      format.json
    end
  end

destroyアクションは今回は省略〜

  private
  def comment_params
    params.require(:comment).permit(:comment).merge(user_id: current_user.id, item_id: params[:item_id])
  end
end

jbuilderファイルの新規作成

jbuilderファイルを作成します。こちらもポイントが2点あります。
1点目は、3行目の「json.comment_user_id」と7行目の「json.item_user_id」の箇所です。この後、jsファイル内で出品者かどうかで追加するビューを条件分岐させます。itemテーブルのuser_idとcommentテーブルのuser_idが一致すれば、出品者となります。

2点目は、1行目の「json.comment_id」と5行目の「json.comment_item_id」です。こちらはコメントを削除する時のリンク<aタグ>で使用します。削除するURLのid部分にこれらを代入して正しいリンクにします。

views/comments/create.json.jbuilder
json.comment_id      @comment.id
json.comment         @comment.comment
json.comment_user_id @comment.user_id
json.name            @comment.user.nickname
json.comment_item_id @comment.item_id
json.item_id         @item.id
json.item_user_id    @item.user_id

jsファイルの作成

長くなりますが、完成コードを全文掲載します。

comment.js
$(function(){
  function buildHTML(comment){
    var html = `<div class="mainShow__box__content__top__commentBox__comments__comment">
                  <div class="profile">
                    <div class="profile__name">
                      ${comment.name}
                    </div>
                    <div class="profile__right">
                      <div class="image">
                        <i class="fas fa-user-circle"></i>
                      </div>
                      <div class="seller_or_buyer">
                        出品者
                      </div>
                    </div>
                  </div>
                  <div class="comment">
                    <div class="comment__text">
                      ${comment.comment}
                    </div>
                    <div class="comment__bottom">
                      <div class="comment__date">
                        <i class="far fa-clock"></i>
                        本日
                      </div>
                      <div class="comment__trash">
                        <a item_id="@item" rel="nofollow" data-method="delete" href="/items/${comment.item_id}/comments/${comment.comment_id}"><i class="fa fa-trash"></i>
                        </a>
                      </div>
                    </div>
                  </div>
                </div>`
    return html;
  }

  function buildHTMLother(comment){
    var html = `<div class="mainShow__box__content__top__commentBox__comments__comment--other">
                  <div class="profile">
                    <div class="profile__right">
                      <div class="image">
                        <i class="far fa-user-circle"></i>
                      </div>
                      <div class="seller_or_buyer">
                        お客様
                      </div>
                    </div>
                    <div class="profile__name">
                      ${comment.name}
                    </div>
                  </div>
                  <div class="comment">
                    <div class="comment__text">
                      ${comment.comment}
                    </div>
                    <div class="comment__bottom">
                      <div class="date">
                        <i class="far fa-clock"></i>
                        本日
                      </div>
                      <div class="comment__icon">
                        <div class="flag">
                          <i class="far fa-flag"></i>
                        </div>
                        <div class="trash">
                          <a item_id="@item" rel="nofollow" data-method="delete" href="/items/${comment.item_id}/comments/${comment.comment_id}"><i class="fa fa-trash"></i></a>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>`
    return html;
  }

  $('#comment_form').on ('submit',function(e){
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action');
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){
      if (data.comment_user_id == data.item_user_id) {
        var html = buildHTML(data);
        $('#comments').append(html);
        $('.text_area').val('');
        $('.comment_btn').prop('disabled', false);
      } else {
        var html = buildHTMLother(data);
        $('#comments').append(html);
        $('.text_area').val('');
        $('.comment_btn').prop('disabled', false);
      }
    })
    .fail(function(){
      alert('error');
    })
  })
});

前半は、追加するビューのHTMLが記述されています。今回は出品者とそれ以外の方でビューが変わり、CSSも若干違うので、2種類用意しています。出品者のコメントであれば、buildHTMLメソッドで、それ以外の方であれば、buildHTMLotherメソッドで追加されます。HTML内には、コメント本文やニックネームの他、deleteメソッド用のリンク内にidも入っています。これで非同期通信で追加されたコメントをすぐに削除することもできます。

後半は、jsファイルの具体的な実行内容が書かれています。処理結果がdoneとなれば、出品者かどうかをif文で条件分岐させています。

turbolinks対応

turbolinksを併用しているとjsファイルが正常に読み込まれないことがあります。これについての解決策は他の方が書かれている記事が参考になりました。この記事を見つけるまで、そもそもturbolinksが原因とも考えていなかったので、目からウロコでした。

Railsでページ読み込みしないとjsが効かないのを解決する方法 @ryico

今日の積み上げ

これまで、ajaxによる非同期通信は投稿したコメントデータがjsonで返ってくるという認識でした。しかしそうではなく、createアクション時に作成したインスタンス変数をjbuilderで加工してjson化して返すということが理解できました。今回のようにitemテーブルのレコードを使用したい場合は、@itemを作れば良いということですね。