タイトルのように、クリックするごとに数字をカウントアップし、非同期でデータを保存する方法を紹介します
イメージとしては、動画を確認してください⬇️
— Kamotetu (@Kamotetu2) November 27, 2019
自分もプログラミング初学者なので、もっとやり方あると思いますが、ググっても良い記事がなく、同じような実装に悩まれている方が多かったので、「とりあえず実装できる」レベルですが、誰かの参考になればと思います
あと、ここで例として紹介しているアプリケーションの内容は小説を定義(maintitle)し、その小説の内容(story)を随時アップしていくという内容になっていますので、なんとなくイメージしていただくと記事の内容が理解しやすいかなと思います
また、マイグレーションファイルの作成や、コントローラの作成など、ここで紹介していない部分の実装等については、別の方の記事等を参考にしてください
それではちょっと長くなりますが紹介していきます
- ruby 2.5.1
- rails 5.2.3
使用しているgem
- 'gon'
- 'device'
- 'jquery-rails'
class CreateReviews < ActiveRecord::Migration[5.2]
def change
create_table :reviews do |t|
t.integer :user_id
t.integer :story_id
t.integer :maintitle_id
t.integer :review
t.timestamps
end
end
end
class Review < ApplicationRecord
belongs_to :user
belongs_to :story
belongs_to :maintitle
end
resources :maintitles do
# 省略
resources :stories do
post 'like_review'
delete 'unlike_review' #まだ実装できてません・・
resources :comments, only: [:create]
end
end
before_action :set_maintitle, only: [:new,
:create,
:show,
:like_review,
# 省略]
before_action :set_story, only: [:show,
# 省略]
////////////////////////////////////////////////////////
def show
#set_maintitle
#set_story
@user = User.find(@story.user_id)
if user_signed_in?
@review = Review.find_by(story_id: @story.id, user_id: current_user.id)
if @review != nil
gon.my_review_count = @review.review + 1
else
@review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0)
gon.my_review_count = @review.review + 1
end
end
@reviews = Review.where(story_id: @story.id)
@a = []
@reviews.each do |review|
p = review.review
@a.push(p)
end
@review_all_count = @a.sum
gon.review_all_count = @a.sum + 1
end
////////////////////////////////////////////////////////
def like_review
# set_maintitle
@story = Story.find(params[:story_id])
@review = Review.find_by(story_id: @story.id, user_id: current_user.id)
p = @review.review + 1
@review.update(review: p)
render json: @story.id
end
////////////////////////////////////////////////////////
private
def set_maintitle
@maintitle = Maintitle.find(params[:maintitle_id])
end
def set_story
@story = Story.find(params[:id])
end
# 省略
<%if @story.user_id != current_user.id%>
<div class="like_review_area">
<%=link_to maintitle_story_like_review_path(@maintitle, @story), method: :post, remote: :true, class: :like_review_btn do%>
<div class="like_review_link">
<div class="like_review", id="<%=@maintitle.id%>">
面白かった数だけここをクリック!
</div>
</div>
<%end%>
</div>
<%end%>
<%if user_signed_in?%>
<%if @story.user_id != current_user.id%>
<div class="show_review_my_count_area">
あなたの評価:
<i class="fa fa-thumbs-o-up ">
</i>
<i class="review_my_count_area">
<%if @review.present?%>
<i class="my_count">
<%=@review.review%>
</i>
<%else%>
<i class="my_count">
0
</i>
<%end%>
</i>
</div>
<%end%>
<%end%>
function appendFaThoumbsOUpMore(my_review_count) {
var fa_thumbs_o_up_more = $(".review_my_count_area");
var my_count_more = `<i class="my_count">
${my_review_count}
</i>`
fa_thumbs_o_up_more.append(my_count_more);
};
function appendFaThoumbsOUpAll(all_review_count) {
var fa_thumbs_o_up_all = $(".review_all_count_area");
var all_count = `<i class="all_count">
${all_review_count}
</i>`
fa_thumbs_o_up_all.append(all_count);
};
$(function() {
$(document).on("ajax:success", ".like_review_btn", function(e) {
e.preventDefault();
$(".my_count").remove();
$(".all_count").remove();
var my_review_count = gon.my_review_count++ ;
var all_review_count = gon.review_all_count++ ;
appendFaThoumbsOUpMore(my_review_count);
appendFaThoumbsOUpAll(all_review_count);
});
});
ずらずら記載してすいません
ということでポイントを説明していきます
reviewテーブルを作成
自分は、誰がどの投稿に対して評価をつけたかわかるように、
class CreateReviews < ActiveRecord::Migration[5.2]
def change
create_table :reviews do |t|
t.integer :user_id
t.integer :story_id
t.integer :maintitle_id
t.integer :review
t.timestamps
end
end
end
としましたが、単に評価の数だけならuser_idとかいらないと思います
あとそれぞれに
null: false
foreign_key: true
もつけた方がいいかもですね
modelにアソシエーションを定義
class Review < ApplicationRecord
belongs_to :user
belongs_to :story
belongs_to :maintitle
end
これはこのままでいいと思います
※ コードは載せませんが、ここでアソシエーションを組んでいる相手方には、has_many :reviewsを書いてください
routeを設定
resources :maintitles do
# 省略
resources :stories do
post 'like_review'
delete 'unlike_review' #まだ実装できてません・・
resources :comments, only: [:create]
end
end
maintitlesにstoriesをネストさせ、storiesコントローラ内に
- like_review
- unlike_review
のアクションを設定します(後述)
ネストさせる意味は、reviewテーブルにmaintitle_idとstory_idを保存するためにやってます
controllerを設定
before_action :set_maintitle, only: [:new,
:create,
:show,
:like_review,
# 省略]
before_action :set_story, only: [:show,
# 省略]
////////////////////////////////////////////////////////
def show
#set_maintitle
#set_story
@user = User.find(@story.user_id)
if user_signed_in?
@review = Review.find_by(story_id: @story.id, user_id: current_user.id)
if @review != nil
gon.my_review_count = @review.review + 1
else
@review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0)
gon.my_review_count = @review.review + 1
end
end
@reviews = Review.where(story_id: @story.id)
@a = []
@reviews.each do |review|
p = review.review
@a.push(p)
end
@review_all_count = @a.sum
gon.review_all_count = @a.sum + 1
end
////////////////////////////////////////////////////////
def like_review
# set_maintitle
@story = Story.find(params[:story_id])
@review = Review.find_by(story_id: @story.id, user_id: current_user.id)
p = @review.review + 1
@review.update(review: p)
render json: @story.id
end
////////////////////////////////////////////////////////
private
def set_maintitle
@maintitle = Maintitle.find(params[:maintitle_id])
end
def set_story
@story = Story.find(params[:id])
end
ごちゃごちゃしててすいません・・
ここから実装についてのポイントになってきます
まず、before_actionで、set_maintitleとset_storyをshowアクションに反映させます
で、ログインしていれば評価ができる仕様にしてますので、
if user_signed_in?
を使用します
@review = Review.find_by(story_id: @story.id, user_id: current_user.id)
これで、現在閲覧している小説の内容のidと、自分のidを持ったreviewレコードを探して取得します
すでに訪れていた場合はデータがありますが(後述)、初めて訪れた場合はもちろんnilとなります
if @review != nil
gon.my_review_count = @review.review + 1
else
@review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0)
gon.my_review_count = @review.review + 1
end
これで、すでに訪れていた場合、gon.my_review_countというjsに渡す変数に、現在のreviewの数に+1させた数字を代入し、初めて訪れた場合はreviewに0を入れてcreateし、さらに前記同様変数に+1させた数字を代入します(後述)
@reviews = Review.where(story_id: @story.id)
@a = []
@reviews.each do |review|
p = review.review
@a.push(p)
end
@review_all_count = @a.sum
gon.review_all_count = @a.sum + 1
この部分はstoryに対する各userの総評価数を取得する記述です
変数@reviewsに、whereでReviewテーブルから@story.idに一致するテーブルを全て取得し代入します
ここで、取得したデータからreviewカラムのデータを格納する空の配列@aを定義しておきます
whereで取得すると、配列でデータが取得されるので、eachで回し、reviewカラムのデータを抽出し、変数pへ代入し、空配列@aへpushにより格納していきます
そして、変数@review_all_countへ@aに入ったreviewデータの合計を.sumを用いて計算し、代入します
そして、jsへデータを渡すため、変数gon.review_all_countへ@a.sum + 1と、総評価数に+1をして代入します
like_reviewアクションを定義する
def like_review
# set_maintitle
@story = Story.find(params[:story_id])
@review = Review.find_by(story_id: @story.id, user_id: current_user.id)
p = @review.review + 1
@review.update(review: p)
render json: @story.id
end
このアクションで、reviewカラムにアクション毎(クリック毎)に+1させていきます
まず、現在のstoryのデータを取得し、変数@storyへ代入します
そして、変数@reviewへReviewテーブルからstory_id,user_idで条件指定したレコードを取得します
変数pへ、取得したReviewレコードのreviewカラムのデータへ+1したデータを代入します
最後に、そのテーブルのreviewカラムをアップデートします
※ render json: @story.idについては自分もよくわかっていませんが、この記述がないと、like_reviewのviewに飛ぼうとします
結果like_reviewアクションにタイムロスが生じてしまいますので、とりあえず書いておきましょう(ターミナルで確認するとわかります)
でもエラーにはならないんだよなーよくわからん
viewを作成する
<%=link_to maintitle_story_like_review_path(@maintitle, @story), method: :post, remote: :true, class: :like_review_btn do%>
viewは好みで作成していただくとして、大事なのは⬆️の一文です
link_toでlike_reviewアクションを実行します(pathはrake routesで確認してください)
このとき「remote: :true」を記述することにより、ajaxにより非同期でアクションが実行されます
よって、データ上の処理はこの時点でアクション毎に+1されていきます
(自分はまだajaxの仕組みをよく理解していませんので、詳しく知りたい方は別の記事等ググって調べてみてください)
また、showに初めて訪れた際、reviewカラムに0を入れてcreateすると前述しましたが、初めて訪れた直後というのは、何もデータが作成されていません
初めて訪れたときの処理の流れを説明すると、
- showに訪れる
- createされる
という順番のため、createしたデータを渡したくてもエラーになります
そのため、
<%if @review.present?%>
<i class="my_count">
<%=@review.review%>
</i>
<%else%>
<i class="my_count">
0
</i>
<%end%>
と、@reviewにデータが入っていない条件分岐を定義してあげて、エラーを回避&初期のreviewのデータ「0」を表示させなければなりません
jsでアクション毎に数字を表示する
function appendFaThoumbsOUpMore(my_review_count) {
var fa_thumbs_o_up_more = $(".review_my_count_area");
var my_count_more = `<i class="my_count">
${my_review_count}
</i>`
fa_thumbs_o_up_more.append(my_count_more);
};
function appendFaThoumbsOUpAll(all_review_count) {
var fa_thumbs_o_up_all = $(".review_all_count_area");
var all_count = `<i class="all_count">
${all_review_count}
</i>`
fa_thumbs_o_up_all.append(all_count);
};
$(function() {
$(document).on("ajax:success", ".like_review_btn", function(e) {
e.preventDefault();
$(".my_count").remove();
$(".all_count").remove();
var my_review_count = gon.my_review_count++ ;
var all_review_count = gon.review_all_count++ ;
appendFaThoumbsOUpMore(my_review_count);
appendFaThoumbsOUpAll(all_review_count);
});
});
さて、最後の説明になります
まず、jsの大まかな処理の流れとして、
- like_reviewアクションが実行されたら(ajax通信が行われたら)イベント発火
- 古いデータの入ったhtml要素を排除
- 新しいデータの入ったhtml要素を挿入
という流れになります
まず、上段のhtmlは置いておいて、下段を見ていきます
$(document).on("ajax:success", ".like_review_btn", function(e) {
この記述は「like_review_btn」というクラスを持ったhtml要素がajax通信を成功したら処理を行うという意味になります
$(".my_count").remove();
$(".all_count").remove();
この記述で、それぞれのクラスを持ったhtml要素を排除します
var my_review_count = gon.my_review_count++ ;
var all_review_count = gon.review_all_count++ ;
これは、先ほどgonで定義した変数に+1したものを、それぞれ変数へ代入するものとなっています
ここで、controllerで定義したgonの変数について説明します
controllerでなぜ+1したかというと、データの表示はあくまで、「アクションが実行された後のデータ」を表示しなければなりません
なので、jsの++のみでは、1度目のアクションでの表示が「showに訪れた時のデータ」が表示されてしまいます
よって、コントローラで(jsでもできると思いますが)+1してあげることで、1度目のアクションでの表示がちゃんと+1されて表示することができるのです
(console.log等で確認してみるとわかります)
appendFaThoumbsOUpMore(my_review_count);
appendFaThoumbsOUpAll(all_review_count);
///////////////////////////////////////////////
function appendFaThoumbsOUpMore(my_review_count) {
var fa_thumbs_o_up_more = $(".review_my_count_area");
var my_count_more = `<i class="my_count">
${my_review_count}
</i>`
fa_thumbs_o_up_more.append(my_count_more);
};
function appendFaThoumbsOUpAll(all_review_count) {
var fa_thumbs_o_up_all = $(".review_all_count_area");
var all_count = `<i class="all_count">
${all_review_count}
</i>`
fa_thumbs_o_up_all.append(all_count);
};
ここでは、先ほどremoveで排除したhtml要素へ、データを更新したhtml要素を挿入する記述となっています
以上で説明を終了します!!
長々読んでいただきありがとうございました
文章書くの苦手なので非常にわかりにくかったかとは思いますが、これでとりあえずは実装できます
また、これで「評価の高い投稿」や、「自分が評価した投稿」等の検索や並び替えなども可能となります(と思う)
あと、今回の実装では最初、「1度目のアクションがcreateアクション」「2度目からupdateアクション」という2段構えでやっていたのですが、どうにもうまく行かなかったため、「showへ訪れたらcreateアクション」するという方法にシフトチェンジしました
なので、評価していなくてもデータが作成されるため、本当にこの実装の仕方でいいのか疑問が残っています
ここは要検討ですね
また、冒頭でも言いましたが、自分は初学者ですので、もっと簡単にできる等あれば、是非教えていただけるとありがたく感じます
ということで失礼します!