はじめに
一覧ページでのいいね機能を実装しようとしましたが、一番上の星をいいねしたら全てのチームにいいねがついてしまうという問題が発生しました。この問題が起きたことで大変学習になったので、思考の整理とアウトプットとして記事にまとめさせていただきました。また、いいね機能は多くのアプリで実装することが多いと思います。同じような問題が起きている方や別の実装方法など、ご意見いただけたら幸いです!
概要
まず大前提にテーブルは、users
, teams
, likes
の3つでlikes
は
users
とteams
の中間テーブルになっています。
AjaxでHTTPメソッドのGET
, POST
, DELETE
ができるように実装していきます。
起きている問題・解決したいこと(仮説)
起きている問題と解決したいことを簡単に書いていきたいと思います。
起きている問題
- 全ての記事にいいねがついてしまう。
- 全てのチームのidを取得していたつもりが一番上のチームのidしか取得できていない。(デベロッパーツールで確認)
解決したいことと仮説
- 配列にして取得できていると思ったができていないので配列から一つずつチームを取得できるようにする。
- いいね対象を判断できるようにクラスをつけないといけない。
- クリックした対象に動的idが付与されていれば実装できるのではないか。
Ajaxで実装すること
- いいねの表示
- いいねされたら黄色い星を表示
- いいねされているものをクリックしたら白い星を表示
LikesController
show
, create
, destroy
アクションを使用しています。
Railsでの定義と違うところはリクエストに対して返すのがjson形式のデータということです。
class LikesController < ApplicationController
before_action :authenticate_user!
before_action :set_like
def show
like_status = current_user.has_liked?(@team)
render json: { hasLiked: like_status }
end
def create
@team.likes.create!(user_id: current_user.id)
render json: { status: 'ok' }
end
def destroy
like = @team.likes.find_by!(user_id: current_user.id)
like.destroy!
render json: { status: 'ok' }
end
private
def set_like
@team = Team.find(params[:team_id])
end
end
user.rb
has_liked?
はuser.rb
で定義しています。showアクション内の処理のようにcurrent_userがteamをlikeしてる?
と直感的にわかりやすいと思い、定義しております。
def has_liked?(team)
likes.exists?(team_id: team.id)
end
index.html.haml
HTTPリクエストを送るにはJavaScriptでidを取得できるようにしなくてはいけないので、テンプレートにカスタムデータを記述しています。data-<情報>
とすることで任意の属性を付与することができます。今回はチームに対していいねをしたいのでteam
のid
が必要です。
※routes
を確認するとどこにリクエストを送れば良いかよくわかります。
たとえばですが、以下index.html.haml
のように記述してデベロッパーツールで確認すると
data-team-id="1"
id="active-star1"
このようにdata属性とチームのidが入ったidを取得することができます。
チームのidが入ったidは一覧ページでのいいね機能なので、クリックしたときに特定のチームをいいねするという状況になります。それを判定するために記述しております。実際にjQueryのコードを見ていただいたほうが早いと思いますが、簡単に説明させていただきますと、active-star
というid
だけだと、active-star
というidは複数あるので、全てのチームにいいねをつける処理が実行されてしまいます。そのため、動的なid(今回はチームのid)
を付与しております。数字の1
の部分がチームのidです。
ここで重要なのは、
- カスタムデータ
- hiddenでいいねの表示を隠していること
hiddenを使っている理由はいいねの状態を確認し表示するということをAjaxで表示するためです。
.hidden.active-star{id: "active-star#{teams.id}", data: {team_id: tams.id}}
= image_tag 'star-yellow.png'
.hidden.in-active-star{id: "in-active-star#{teams.id}", data: {team_id: teams.id}}
= image_tag 'star-white.png'
.hidden {
display: none;
}
jQuery
本題のいいね機能の実装部分です。
記述が冗長になってしまったので、コメントを入れております。重要なのはcsrfToken
をリクエスト時に持たせることです。これをしないと422 (Unprocessable Entity)というエラー
が起きます。なぜかというとGETと違ってPOSTなどの処理はデータベースの変更をするリクエストなので、簡単に操作されては困るため制約がついています。そのため、rails-ujs
を使ってaxios
でリクエスト時にcsrfToken
というのを持たせるようにしております。
import $ from 'jquery'
import axios from 'axios'
import { csrfToken } from 'rails-ujs'
// リクエスト時にCSRFトークンを持たせる
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken()
document.addEventListener('DOMContentLoaded', () => {
// ロード時にいいねされていない星を配列で取得
$('.in-active-star').each(function (index, element) {
// テンプレートで記述したカスタムデータを取得
const likeData = $(element).data()
// カスタムデータからチームIDを取得
const teamId = likeData.teamId
// カスタムデータを入れてGETリクエストを送る
axios.get(`/teams/${teamId}/like`)
// リクエストを送ったらレスポンスが返ってくる
.then((response) => {
// responseでrenderされたlikeの状態を取得(true or false)
const inActiveStatus = response.data.hasLiked
// falseであればいいねされていない => 白い星を表示するために、'hidden'を取り外す
if ( inActiveStatus === false ) {
$(element).removeClass('hidden')
}
})
})
// ロード時にいいねされている星を配列で取得
$('.active-star').each(function (index, element) {
// テンプレートで記述したカスタムデータを取得
const likeData = $(element).data()
// カスタムデータからチームIDを取得
const teamId = likeData.teamId
// カスタムデータを入れてGETリクエストを送る
axios.get(`/teams/${teamId}/like`)
// リクエストを送ったらレスポンスが返ってくる
.then((response) => {
// responseでrenderされたlikeの状態を取得(true or false)
const activeStatus = response.data.hasLiked
// trueであればいいねされている => 黄色い星を表示するために、'hidden'を取り外す
if ( activeStatus === true) {
$(element).removeClass('hidden')
}
})
})
// #create いいねをつけたいときの処理
$('.in-active-star').on('click', (e) => {
e.preventDefault();
const dataset = $(e.currentTarget).data()
// クリックした要素のidを取得
const teamId = dataset.teamId
// teamIdを使いPOSTリクエストを送る
axios.post(`/teams/${teamId}/like`)
.then((response) => {
// リクエスト成功なら処理を行う
if (response.data.status === 'ok') {
$(`#in-active-star${teamId}`).addClass('hidden');
$(`#active-star${teamId}`).removeClass('hidden');
}
})
// エラー時の処理
.catch((e) => {
window.alert('Error')
console.log(e)
})
})
// #destroy いいねを外したいときの処理
$('.active-star').on('click', (e) => {
e.preventDefault();
const dataset = $(e.currentTarget).data()
// クリックした要素のidを取得
const teamId = dataset.teamId
// teamIdを使いdeleteメソッドを使う
axios.delete(`/teams/${teamId}/like`)
.then((response) => {
// リクエスト成功なら処理を行う
if (response.data.status === 'ok') {
$(`#active-star${teamId}`).addClass('hidden');
$(`#in-active-star${teamId}`).removeClass('hidden');
}
})
// エラー時の処理
.catch((e) => {
window.alert('Error')
console.log(e)
})
})
})
まとめ
- Ajax処理はデベロッパーツールを使い
debugger
やconsole.log()
を使い値が取れているか確認をしっかりすると開発が捗る。 - 詳細ページのように一つしかいいねがないときは意識していなかったが、一覧ページなど複数ある中の一つというように特定したいときは個別のidを付与することで実装できる。
- POSTやDELETE時にはデータベースの操作をすることからCSRFトークンというものが必要になる。
最後に
今回はjQueryを使って実装しました。Vue.jsの学習も始めたので、生JSやjQueryをもっと理解したら開発に使っていきたいなと思っています。このように実装するのもひとつの方法だと思いますが、他にもたくさんの実装方法があると思います。他のライブラリやフレームワークではどのように実装するのか、また一度書いたコードもリファクタリングを積極的にやっていけたらと思います!