1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

コメント機能実装 - 非同期通信でのコメント表示を操作する - Ajax,Ruby, Rails, jQuery, jbuilder, JSON

Last updated at Posted at 2020-08-19

はじめに

ECサイトやブログサイトでコメント機能実装を行うことがありますよね?

今回はコメント機能実装を事前準備をはじめとして、コメント機能実装にまつわるテーブル、モデル、コントローラーの作成、jbuilder、JSON、jQueryの組み立て方を一つ一つ詳しく解説しながら、非同期通信(Ajax)について深堀し、コメント機能を実装する際に便利なチップを共有します!!

Twitter: https://twitter.com/f_t_b_future

目次

1開発環境

Rails 5.2.3
Ruby 2,5.1
jQuery
Haml
jbuilder

2実装要件

順に説明していきながら以下の実装要件を解決していきます。先に内容を見たい方はリンクから当該記事をご覧ください。

コメントがあればコメントを表示
商品が売れていなければコメント可能
ユーザー登録をしていなければコメント不可
商品が売れたらコメント不可
出品者がコメントしたら、ニックネームの下に出品者と表示
コメントを投稿して非同期通信で表示させる際にコメントを上から古い順番に並べる
コメントがなければ *まだコメントはありません。と表示
非同期の状態でも連続してコメントを入力したい
非同期の状態でも連続して送信ボタンを押したい

3事前準備

3-1基本準備

3-2コントローラーの設定あたりから具体的な内容になってくるので、事前準備が不要な方はそちらからご覧ください。

それでは初めていきます。
まず、Gemfileにjquery-railsが記載されていることを確認しましょう。
なければ下記を記載して bundle install です!!

Gemfile
gem 'jquery-rails
app/assets/javascripts/applications.js

//= require jquery
//= require jquery_ujs
//= require_tree .

ルートはこの様にして基本的にはproductsコントローラーにネストする形でコメント機能を操作しようと思います。
これにより、コメントに紐づくproductのid情報を含んだパスの受け取りが可能になります。

Routes.rb
  resources :products do
    resources :comments, only: [:create]
  end

次にコメントができる準備です。
モデルとコントローラーを作成してなければ作成します。

まずモデルです。
rails g model comment
でマイグレーションファイルとモデルファイルができますよね。
マイグレーションファイルには追加したいテーブルを記載します。

XXXXXXXXXXXXXX_create_comments.rb
class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.integer :user_id,     null: false
      t.integer :product_id,  null: false
      t.text :text,           null: false
      t.timestamps
    end
  end
end

モデルにはアソシエーションとバリデーションを記載します。

app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :product
  belongs_to :user

  validates :text, presence: true
end

そして大事なのですが
rails db:migrate
を忘れずに!!!

次はコントローラーです。
rails g controller comments
これでコントローラーとビューのフォルダができました。

3-2コントローラーの設定

まず、comment_paramsを作成します。

paramsの中でコメントテーブルの
user_idカラムにcurrent_user.id及びproduct_idカラムにproduct_id
を保存しパラメーター(comment_params)に渡せる様にしています。

今回コメントコントローラーにはルートで定義した createを記載します。

app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    @comment = Comment.create(comment_params)
    @sellerId = @comment.product.user_id
    respond_to do |format|
      format.html { redirect_to product_path(params[:product_id])}
      format.json
    end
  end

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

end

createメソッドの中には Cmment.create(comment_params)の内容を @commentに入れてあげます。

4非同期通信解説

4-1jbuilder解説

さて、ここから非同期通信の準備をしていきましょう。
コントローラー内に記載した下記のコードは respond_to doで .html もしくは .jsonで 定義したformatで処理を分けています。

app/controllers/comments_controller.rb
    respond_to do |format|
      format.html { redirect_to product_path(params[:product_id])}
      format.json
    end

今回はproducts/show.html.hamlのビューファイルで動作をさせたいので、commentsフォルダ内ではhamlは作成しないのですが、
jsonを動かして上げるためにrailsにデフォルトで実装されているjbuilderというテンプレートエンジンを使います。
views/commentsフォルダ配下にcreate.json.jbuilderを作成しましょう。

views/comments/create.json.jbuilder
json.text  @comment.text
json.user_id  @comment.user.id
json.user_name  @comment.user.name
json.seller_id  @sellerId

jbuilderではコントローラーで生成した変数に格納されたデータを基にJSON形式の文字列データを生成します。

上記の記載はコントローラーで定義した変数を

controller
@comment = Comment.create(comment_params)
@sellerId = @comment.product.user_id

jbuider内でキー名に置き換えて、.jsファイル内でデータを操作することができる様にします

jbuilder
json.キー名 "値"
# {"キー名": "値"}

json.text  @comment.text
json.user_id  @comment.user.id
json.user_name  @comment.user.name
json.seller_id  @sellerId
# {.text: @comment.text, .user_id: @comment.user.id,.user_name: @comment.user.name, .seller_id: @sellerId}

4-2haml解説

ここで今回使うviewファイルを紹介します。
商品確認ページの中で商品に関するコメントのやりとりを想定しているので、productsフォルダのshow.html.hamlを使用します。

views/products/show.html.haml
      .showMain__content__topContent__commentBox
        .comments
          コメント一覧
        - if @comments.exists?
          - @comments.each do |c|
            .comments__box
              = link_to c.user.name, user_profiles_path(c.user.id), class: "comments__box__name"
              - if c.user.id === @product.user.id
                .comments__box__name__mark 出品者
              .comments__box__text
                %p
                  = c.text
        - else
          .comments__box
            .comments__box__noComment
              *まだコメントはありません。
        - if current_user
          = form_with(model: [@product, @comment], local: true, id: "comment_create") do |f|
            = f.text_area :text, id: :count_comment, placeholder: "コメントする", maxlength: "400", class: "showMain__content__topContent__commentBox__textField", rows: "4"
            .showMain__content__topContent__commentBox__noticeMsg
              相手のことを考え丁寧なコメントを心がけましょう。
              %br
              不快な言葉遣いなどは利用制限や退会処分となることがあります。
            %br
            - if @product.buyer_id.present?
              .showMain__content__topContent__commentBox__submitBtnSold
                売り切れのためコメントできません
            -else
              = button_tag type: 'submit', class: 'showMain__content__topContent__commentBox__submitBtn' do
                %i.fa.fa-comment
                コメントする
        - else
          .showMain__content__topContent__commentBox__noticeMsg
            *コメントの投稿には新規登録/ログインが必要です。

ビューファイルの実装内容は下記です。
スクリーンショット 2020-08-19 1 20 33
ビューの中で以下の条件分岐をしています。

データと同期した際にコメントがあれば表示、なければ非表示
        - if @comments.exists? #コメントがあればtureを返す
         コメント表示
        - else
         *まだコメントはありません。
ログインしていればコメント可能、していなければ不可能
        - if current_user #ログインしているユーザーを取得できればtureを返す
         コメントボックスを表示させてコメントができるようにする
        - else
         *コメントの投稿には新規登録/ログインが必要です。

スクリーンショット 2020-08-19 21 37 35

購入者のidが商品テーブルに存在していればコメント不可能、いなければ可能
        - if @product.buyer_id.present? #すでに売れていて購入者idがプロダクテテーブルにあればtureを返す
         売り切れのためコメントできません
        - else
         コメントする

スクリーンショット 2020-08-19 21 36 14

これで、まず以下の要件を満たすことができます。

コメントがあればコメントを表示
商品が売れていなければコメント可能
ユーザー登録をしていなければコメント不可
商品が売れたらコメント不可

4-3jsファイル解説

ここからはjQuery、Ajaxについての解説です
まず全貌はこちらとなります。

comments.js
  #Comment機能
  $(function(){
    function buildHTML(comment){
      let html = `<div class="comments__box">
                      <a class="comments__box__name" href=/users/profile/${comment.user_id}>${comment.user_name}</a>
                      <div class="comments__box__text">${comment.text}</div>
                    </div>`
      return html;
    }
    function buildOwnerHTML(comment){
      let html = `<div class="comments__box">
                      <a class="comments__box__name" href=/users/profile/${comment.user_id}>${comment.user_name}</a>
                      <div class="comments__box__name__mark">出品者</div>
                      <div class="comments__box__text">${comment.text}</div>
                    </div>`
      return html;
    }
    $('#comment_create').on('submit', function(e){
      e.preventDefault();
      console.log(this)
      let formData = new FormData(this);
      let url = $(this).attr('action')
      $.ajax({
        url: url,
        type: 'POST',
        data: formData,
        dataType: 'json',
        processData: false,
        contentType: false
      })
      .done(function(data){
        if (data.seller_id == data.user_id) {
          let html = buildOwnerHTML(data);
          $('#comment_create').before(html);
        } else {
          let html = buildHTML(data);
          $('#comment_create').before(html);
        }
        $('.comments__box__noComment').remove()
        $('.showMain__content__topContent__commentBox__textField').val('');
        $('.showMain__content__topContent__commentBox__submitBtn').prop('disabled', false);
      })
      .fail(function(){
        alert('error');
      })
    })
  })

jsファイルでは先ほどjbuilderで受け取ったキーを使用していきます。

jsでのビューの設定

はい!!順を追って説明します。
まずビューですが、ビューについては下記のbuildOwnerHTMLが出品者表示、buildHTMLが通常表示となる様に設定しています。

comments.js
    function buildHTML(comment){
      let html = `<div class="comments__box">
                      <a class="comments__box__name" href=/users/profile/${comment.user_id}>${comment.user_name}</a>
                      <div class="comments__box__text">${comment.text}</div>
                    </div>`
      return html;
    }
    function buildOwnerHTML(comment){
      let html = `<div class="comments__box">
                      <a class="comments__box__name" href=/users/profile/${comment.user_id}>${comment.user_name}</a>
                      <div class="comments__box__name__mark">出品者</div>
                      <div class="comments__box__text">${comment.text}</div>
                    </div>`
      return html;
    }
  • ビューのHTMLはテンプレートリテラルで記載していますが、プレースホルダー${}を用いることでテンプレートリテラルの中での変数の使用が可能になります。

  • HTMLの中でコメント内容やユーザーネームが表示される様にjbuilderで設定したcomment.textcomment.user_nameをプレースホルダーで囲んでデータを引っ張ってきています。

  • また、aタグを用いてhref属性でリンク先を指定することにより、ユーザーネームを押下したらユーザーのプロフィール画面へ飛ぶ様にしています。設定したプロフィール画面のurlは rails routesで確認すると、/users/profile/:idとなっているのですが、この:idにjbuilderで設定した、comment.user_idをプレースホルダーで囲んで、${comment.user_id}としてあげることで、コメントしたユーザーのプロフィール画面へ飛ぶようにしてあげています。

このbuildOwnerHTML(出品者表示)とbuildHTML(通常表示)は出品者がコメントしたら、ニックネームの下に出品者と表示と言う要件を満たすためにあとで使用します。

4-3-2取得データ基の指定

次はおなじみのコードですね。

comments.js
$(function(){
  $('#comment_create').on('submit', function(e){
    e.preventDefault();
    var formData = new FormData(this);
    let url = $(this).attr('action')
  })
})
  • $('#comment_create')とありますが、show.html.hamlの中の form_withに id:"comment_create"を設定しています。
    form_withではフォーム用を生成しますが、このフォームが送信されたら動くプログラムです。

  • 送信されたらということなので、 .on('submit', function(e){})を使用しています。ちなみにもうご存知だとは思いますが、この()の中のeは event のeです。発生したイベントをeで受け渡しています。

  • form_withでフォームを送信する際に、デフォルトだと自動的に通信が行われてしまいますが、今回は非同期通信を行いますので、e.preventDefault();で自動で起こるイベントを無効化します。

  • ここでFormData();というものができてますが、FormData();はform_withで送信したフォームの情報を取得しています。FormData(this);のthisは直前の”メソッド、関数"などを取得できる便利なものですが、ここでは、'#comment_create'を取得しています。つまり、FormData('#comment_create')となるということですね。

  • new FormData(this);のnewは新規でフォームデータを取得するということです。

  • その後ろのlet url = $(this).attr('action')ですが、
    まず、$(this)で先ほどのフォームデータの要素をjQueryオブジェクトに変換しています。

  • そして、 attrですが、これはよく使用するので、是非覚えておいてください。attrは日本語でいうところのアトリビュートですが、HTMLが持つ idやclassなどの属性を変更、取得、設定することができます。

  • このattrで、フォームデータの中から、'action'を取得しています。この'action'をconsole.log(this)で見ると、下記の様にurlを取得していることが理解できます。このurlは/products/1/commentsとなっていますが、ここから、商品のid1に対するコメントだということがわかります。
    スクリーンショット 2020-08-19 2 18 41

まとめると、'#comment_create'というidが付与された新しいform_withのフォームの内容をnew FromDataで取得して、formDataという変数に代入し、formDataの中のurlを取得しているということです。

4-3-3JSONデータ取得(.ajaxの中についてみていきます)

Ajaxとは"Asynchronous JavaScript + XHR(XMLHttpRequest)"の略です。XHRはJavaScriptなどのウェブブラウザ搭載のスクリプト言語でサーバとのHTTP通信を行うための、組み込みオブジェクト(API)です。Google Mapとかが使っている非同期で地図を確認できる技術ですね!!

細かく解説していきます。
まず、コードです。

comments.js
      $.ajax({
        url: url,
        type: 'POST',
        data: formData,
        dataType: 'json',
        processData: false,
        contentType: false
      })
  • url:これはリクエストが送信されるURLです。今回はattrで取得したurlを指定しています。
  • type:これはRailsで言うところのHTTPリクエストメソッド('POST','GET'等)です。今回は'POST'を指定しています。
  • data:これはサーバーへ送信するデータです。今回はformDataの値を指定しており、上記で説明したXHRの機能を果たしています。
  • dataType:これはサーバーから返されるデータの型です。今回は'json'を指定しています。
  • processData:dataに指定している対象を文字列に変換するかを指定できるものです。初期値はtrueになっており、文字列に自動変換されてエラーとなります。今回はfalseとして文字列となるのを防ぎます。FormDataでデータ取得時に記載すると覚えておきましょう。
  • contentType:これはサーバーへデータ送信を行う際のコンテンツタイプです。FormDataでデータ取得時に記載します。デフォルトでは"application/x-www-form-urlencoded; charset=UTF-8"が指定されており、ほとんどの場合、特に変更する必要はありませんが、コンテンツタイプを変更したいときは指定します。FormDataでのデータ取得時は必ずfalseとするで問題ありません。

ちなみに、以下のコンソールを見るとjbuilderで指定されたJSONデータが送信されていることがわかりやすく理解できますね。
スクリーンショット 2020-08-19 19 38 24

ここからはこのコンソールで表示されているデータを頭に置いて解説を聞いていただけるとよりわかりやすいと思います。

4-3-4取得データの処理(.done.failの中についてみていきます)

以下では.ajaxで完了した処理が成功か失敗かで処理を分岐しています。

comments.js
       .done(function(data){
        if (data.seller_id == data.user_id) {
          let html = buildOwnerHTML(data);
          $('#comment_create').before(html);
        } else {
          let html = buildHTML(data);
          $('#comment_create').before(html);
        }
        $('.comments__box__noComment').remove()
        $('.showMain__content__topContent__commentBox__textField').val('');
        $('.showMain__content__topContent__commentBox__submitBtn').prop('disabled', false);
      })
      .fail(function(){
        alert('error');
      })

まず、シンブルな.failの中身について説明します。
.failでは処理が失敗したときにerrorと言うエラーメッセ時をaleartを使って表示させています。

次に.doneについてです。
.doneの中では非同期通信に成功した場合の処理が走ります。非同期通信に成功したら、一番はじめに返される値はcreate.json.juilderで指定したデータです。そのデータは上で定義しているdata:に入っているので、function(data)としています。

指定したい要件に

  • 出品者がコメントしたら、ニックネームの下に出品者と表示

と言うものがあるのですが、この要件を満たすために.doneのなかで表示するビューを変更するif文を書いています。
出品者を判断するメソッドはjbuilderの中で

data.seller_id @sellerId

としています。

このdata.seller_idはコントローラーで指定した@sellerIdが入っていますが、@sellerIdを紐解くと

@comment = Comment.create(comment_params)
@sellerId = @comment.product.user_id

と出品されている商品のuser_id(productテーブルのuser_id)を紐づいけています。
このdata.seller_idと現在コメントしているユーザーのidつまり、ログインしているユーザーのidが一致しているかを条件として分岐します。

ログインしているユーザーのidは、コントローラーのcomment_paramsで取得したuser_id: current_user.idを引っ張って、@commentに代入し、jbuilderで

json.user_id  @comment.user.id

としていることから、if文の中では下記の様に条件を設定します。

if文
if (data.seller_id == data.user_id)

これでtrueを返せば出品者と表示、falseを返せば出品者を非表示とします。

if文
        if (data.seller_id == data.user_id) {
          let html = buildOwnerHTML(data);
          $('#comment_create').before(html);
        } else {
          let html = buildHTML(data);
          $('#comment_create').before(html);
        }
  • trueの場合は上で定義したbuildOwnerHTML(出品者表示)に、falseの場合はbuildHTML(通常表示)にdataを渡してhtml変数とします。
    これで
出品者がコメントしたらニックネームの下に出品者と表示

と言う要件を満たしています。
スクリーンショット 2020-08-19 20 06 36

その後は$('#comment_create').before(html)となっていますが、ここで、show.html.hamlにどの様な記載があるのか確かめてみましょう。

5要素の操作

スクリーンショット 2020-08-19 18 32 09

上のスクショにid="comment_create"が青く網掛けされているのが見えると思いますが、非同期でHTMLを表示させる際に、この'#comment_create'と言う要素の前・後どちらに表示するかを操作します。

5-1beforeについて

comments.js
   $('#comment_create').before(html);

.before()()の中に指定した要素やテキストをclassやidの前に追加します。
今回は、$('#comment_create').before(html)となっていますが、$('#comment_create')と言うidの前に、html(buildOwnerHTML(data) もしくは buildHTML(data))を追加して表示させます。
これで非同期通信で、必ずtest_areaの上にコメントが表示されるようになります。
そうすることで

コメントを投稿して非同期通信で表示させる際にコメントを上から古い順番に並べる

と言う要件を満たします。

5-2appendについて

comments.js
   $('#comment_create').append(html);

.append()()の中に指定した要素やテキストをclassやidの後ろに追加します。
もし、$('#comment_create')と言うidの後ろに、html(buildOwnerHTML(data) もしくは buildHTML(data))を追加して表示させたい場合は、$('#comment_create').append(html)`とすることで実現できると言うわけです。

5-3removeについて

comments.js
   $('.comments__box__noComment').remove()

.remove()は指定した要素をその配下の子要素ごと削除するメソッドです。
以下をご覧ください。
スクリーンショット 2020-08-19 19 58 20
今回は、デフォルトでコメントがなければ *まだコメントはありません。 と表示させています。
非同期通信でコメントがついた段階で、 *まだコメントはありません。 と言う表示を消すために.doneの処理の中で$('.comments__box__noComment')と言う要素に対して.remove()を使用することで

コメントがなければまだコメントはありませんと表示

と言う要件を満たしています。

5-4valについて

comments.js
   $('.showMain__content__topContent__commentBox__textField').val('');

.val()はHTMLのvalue値を設定・変更・取得することができるメソッドです。
下記をご覧ください。
スクリーンショット 2020-08-19 19 48 59

このコメントのtext_areaは.showMain__content__topContent__commentBox__textFieldと言うクラスなのですが、.val('')を設定することによって上のスクショのはいと言う表示が非同期の状態でも表示されず

非同期の状態でも連続してコメントを入力したい

と言う要件を満たすことができます。

5-5propについて

comments.js
   $('.showMain__content__topContent__commentBox__submitBtn').prop('disabled', false);

.prop()は、property(プロパティ)と言う意味で、()の中にプロパティを入れます、例えばチェックボックスにチェックを入れたいときに.prop('checked')とすることでチェックを入れて表示を操作できたりします。
今回は送信ボタンを押下すると、プロパティが'disabled'になり、非同期で再度送信ボタンを押せなくなってしまうのを防ぐために.prop('disabled', false)としています。
そうすることでボタンを押下してもボタンのプロパティが'disabled'にならず

非同期の状態でも連続して送信ボタンを押したい

と言う要件を満たしています。
本記事で出てきたprop()とattr()はとても便利なのですが、こちらの記事がとてもわかりやすかったので参考までに。
https://agohack.com/attr-prop-attributes/

#6まとめ
web開発はいろいろなやり方があり、奥が深いので駆け出しの方はこれを一つのやり方として、他にもいろいろ試してくださいね。勉強中なので、もっとこうした方が良いなどのコメントは是非どんどんください。

要件へのリンク

コメントがあればコメントを表示
商品が売れていなければコメント可能
ユーザー登録をしていなければコメント不可
商品が売れたらコメント不可
出品者がコメントしたら、ニックネームの下に出品者と表示
コメントを投稿して非同期通信で表示させる際にコメントを上から古い順番に並べる
コメントがなければ *まだコメントはありません。と表示
非同期の状態でも連続してコメントを入力したい
非同期の状態でも連続して送信ボタンを押したい

参考にした記事

jbuilder
https://github.com/rails/jbuilder

ajax
http://js.studio-kingdom.com/jquery/ajax/ajax
https://ja.wikipedia.org/wiki/XMLHttpRequest
https://www.ryotaku.com/entry/2019/01/16/224131
https://qiita.com/tanutanu/items/239abfe88bbbeec772bf

preventDefault
https://developer.mozilla.org/ja/docs/Web/API/Event/preventDefault

this
https://qiita.com/takeharu/items/9935ce476a17d6258e27

aleart
https://techacademy.jp/magazine/5486

val
https://www.sejuku.net/blog/45297

prop
https://agohack.com/attr-prop-attributes/

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?