はじめに
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 です!!
gem 'jquery-rails
//= require jquery
//= require jquery_ujs
//= require_tree .
ルートはこの様にして基本的にはproductsコントローラーにネストする形でコメント機能を操作しようと思います。
これにより、コメントに紐づくproductのid情報を含んだパスの受け取りが可能になります。
resources :products do
resources :comments, only: [:create]
end
次にコメントができる準備です。
モデルとコントローラーを作成してなければ作成します。
まずモデルです。
rails g model comment
でマイグレーションファイルとモデルファイルができますよね。
マイグレーションファイルには追加したいテーブルを記載します。
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
モデルにはアソシエーションとバリデーションを記載します。
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を記載します。
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で処理を分けています。
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を作成しましょう。
json.text @comment.text
json.user_id @comment.user.id
json.user_name @comment.user.name
json.seller_id @sellerId
jbuilderではコントローラーで生成した変数に格納されたデータを基にJSON形式の文字列データを生成します。
上記の記載はコントローラーで定義した変数を
@comment = Comment.create(comment_params)
@sellerId = @comment.product.user_id
jbuider内でキー名に置き換えて、.jsファイル内でデータを操作することができる様にします
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を使用します。
.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
*コメントの投稿には新規登録/ログインが必要です。
ビューファイルの実装内容は下記です。
ビューの中で以下の条件分岐をしています。
- if @comments.exists? #コメントがあればtureを返す
コメント表示
- else
*まだコメントはありません。
- if current_user #ログインしているユーザーを取得できればtureを返す
コメントボックスを表示させてコメントができるようにする
- else
*コメントの投稿には新規登録/ログインが必要です。
- if @product.buyer_id.present? #すでに売れていて購入者idがプロダクテテーブルにあればtureを返す
売り切れのためコメントできません
- else
コメントする
これで、まず以下の要件を満たすことができます。
コメントがあればコメントを表示
商品が売れていなければコメント可能
ユーザー登録をしていなければコメント不可
商品が売れたらコメント不可
4-3jsファイル解説
ここからはjQuery、Ajaxについての解説です
まず全貌はこちらとなります。
#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
が通常表示となる様に設定しています。
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.text
やcomment.user_name
をプレースホルダーで囲んでデータを引っ張ってきています。 -
また、aタグを用いてhref属性でリンク先を指定することにより、ユーザーネームを押下したらユーザーのプロフィール画面へ飛ぶ様にしています。設定したプロフィール画面のurlは
rails routes
で確認すると、/users/profile/:id
となっているのですが、この:id
にjbuilderで設定した、comment.user_id
をプレースホルダーで囲んで、${comment.user_id}
としてあげることで、コメントしたユーザーのプロフィール画面へ飛ぶようにしてあげています。
この
buildOwnerHTML
(出品者表示)とbuildHTML
(通常表示)は出品者がコメントしたら、ニックネームの下に出品者と表示
と言う要件を満たすためにあとで使用します。
4-3-2取得データ基の指定
次はおなじみのコードですね。
$(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に対するコメントだということがわかります。
まとめると、'#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とかが使っている非同期で地図を確認できる技術ですね!!
細かく解説していきます。
まず、コードです。
$.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データが送信されていることがわかりやすく理解できますね。
ここからはこのコンソールで表示されているデータを頭に置いて解説を聞いていただけるとよりわかりやすいと思います。
4-3-4取得データの処理(.done.failの中についてみていきます)
以下では.ajaxで完了した処理が成功か失敗かで処理を分岐しています。
.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 (data.seller_id == data.user_id)
これでtrue
を返せば出品者と表示、false
を返せば出品者を非表示とします。
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変数とします。
これで
出品者がコメントしたらニックネームの下に出品者と表示
その後は$('#comment_create').before(html)
となっていますが、ここで、show.html.haml
にどの様な記載があるのか確かめてみましょう。
5要素の操作
上のスクショにid="comment_create"
が青く網掛けされているのが見えると思いますが、非同期でHTMLを表示させる際に、この'#comment_create'
と言う要素の前・後どちらに表示するかを操作します。
5-1beforeについて
$('#comment_create').before(html);
.before()
は()
の中に指定した要素やテキストをclassやidの前に追加します。
今回は、$('#comment_create').before(html)
となっていますが、$('#comment_create')
と言うidの前に、html
(buildOwnerHTML(data) もしくは buildHTML(data))を追加して表示させます。
これで非同期通信で、必ずtest_areaの上にコメントが表示されるようになります。
そうすることで
コメントを投稿して非同期通信で表示させる際にコメントを上から古い順番に並べる
と言う要件を満たします。
5-2appendについて
$('#comment_create').append(html);
.append()
は()
の中に指定した要素やテキストをclassやidの後ろに追加します。
もし、$('#comment_create')と言うidの後ろに、
html(buildOwnerHTML(data) もしくは buildHTML(data))を追加して表示させたい場合は、
$('#comment_create').append(html)`とすることで実現できると言うわけです。
5-3removeについて
$('.comments__box__noComment').remove()
.remove()
は指定した要素をその配下の子要素ごと削除するメソッドです。
以下をご覧ください。
今回は、デフォルトでコメントがなければ *まだコメントはありません。
と表示させています。
非同期通信でコメントがついた段階で、 *まだコメントはありません。
と言う表示を消すために.done
の処理の中で$('.comments__box__noComment')
と言う要素に対して.remove()
を使用することで
コメントがなければまだコメントはありませんと表示
と言う要件を満たしています。
5-4valについて
$('.showMain__content__topContent__commentBox__textField').val('');
.val()
はHTMLのvalue値を設定・変更・取得することができるメソッドです。
下記をご覧ください。
このコメントのtext_areaは.showMain__content__topContent__commentBox__textField
と言うクラスなのですが、.val('')
を設定することによって上のスクショのはい
と言う表示が非同期の状態でも表示されず
非同期の状態でも連続してコメントを入力したい
と言う要件を満たすことができます。
5-5propについて
$('.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