開始
この記事は 駆け出しエンジニアの第一歩! Advent Calendar 2020 の 16日目 の記事です。
AdventCalendar初参加記事です。エンジニア2年目です。
いいね機能の記事自体は何番煎じ感はありますが、やりたかったことは、JSで書くのはコードも増えて大変という主張です。
いいね機能を作ります。
まず、モデルは両パターン共通で大体こんな感じ。
class Like < ApplicationRecord
belongs_to :movie
belongs_to :user
LIKED_COLOR = '#ff3366'.freeze
UNLIKED_COLOR = '#A0A0A0'.freeze
end
class Movie < ApplicationRecord
has_many :likes, dependent: :destroy
def like_by(user)
likes.where(likes: { user_id: user }).last
end
def liked_by?(user)
like_by(user).present?
end
end
【パターン1】 Rails Way版
ルーティングを定義。
resources :movies, only: :show
resources :likes, only: %i[create destroy]
コントローラのポイントとしては、非同期で create
, destroy
アクションが発生する場合、
それぞれ create.js.slim
destroy.js.slim
を自動的に render
するので、それぞれに渡す @movie
を before_action
で定義しておいたことくらい。
class LikesController < ApplicationController
before_action :set_movie
def create
Like.create!(user_id: current_user, movie_id: params[:movie_id])
end
def destroy
Like.find(params[:id]).destroy!
end
private
def set_movie
@movie = Movie.find(params[:movie_id])
end
end
viewは今回はslimで書いてしまいます。どうでもいいですが、ハートマークはfontawesome使ってる想定。
link_to
は、
-
params[:movie_id]
をコントローラに渡せるように引数にmovie_id: movie.id
を定義しており、 -
remote: true
を使って非同期でアクションを発生させるようにしています。
#js-like-button
= render 'like', movie: @movie
- if movie.liked_by?(current_user)
= link_to like_path(movie.like_by(current_user).id, movie_id: movie.id), method: :delete, remote: :true do
i.fas.fa-heart style="color: #{Like::LIKED_COLOR}"
- else
= link_to likes_path(movie_id: movie.id), method: :post, remote: :true do
i.fas.fa-heart style="color: #{Like::UNLIKED_COLOR}"
js.slimの記法がちょっと独特かも。 create
も destroy
もrenderする内容は同じ。
| document.getElementById('js-like-button').innerHTML = "#{j(render 'movies/like', movie: @movie)}";
| document.getElementById('js-like-button').innerHTML = "#{j(render 'movie/like', movie: @user)}";
パターン1はこれで完成。Railsのおかげで、さくっとできてしまいます。
【パターン2】 JavaScript版
ルーティングを定義。 いいね機能用の create
と destroy
アクションは、JavaScriptにJSONを渡すAPIとして定義します。
resources :movies, only: :show
namespace :api, format: :json do
namespace :v1 do
resources :likes, only: %i[create destroy]
end
end
API用のコントローラーを定義しています。
skip_forgery_protection
を書いておかないと、CSRFに引っかかってJavaScript側からAPIを叩けません。※あまり詳しくわかってないので、有識者の方はご教授いただけますと幸いです。
※ 2021年4月19日追記
別の記事でRailsがAjaxリクエストを受け取る際のCSRF対策について書きました。
CSRF対策をちゃんとやるなら、fetch のパラメータに headers: { 'X-CSRF-Token': csrfToken } を付与する
module Api
module V1
class LikesController < ApplicationController
skip_forgery_protection
def create
like = Like.create!(user: current_user, movie_id: params[:movie_id])
render json: { like_id: like.id }
end
def destroy
Like.find(params[:id]).destroy!
render json: { } # ちょっとブサイクだが、jsonを返さないとjs側で処理ができなかった。
end
end
end
end
viewは一旦、現在いいねされているかどうかを表示しておく。
input type='hidden'
で、JSで受け取るためのパラメーター like_id
と movie_id
を定義しています。
#js-like-button
- like_button_color = @movie.liked_by?(current_user) ? Like::LIKED_COLOR : Like::UNLIKED_COLOR
input type='hidden' id='like_id' value="#{@movie.like_by(current_user).id}"
input type='hidden' id='movie_id' value="#{@movie.id}"
i.fas.fa-heart style="color: #{like_button_color}"
いいねボタンがクリックされたときに、 create
と destroy
のAPIを叩くJSを書きます。
- JSのリクエストはfetch APIで書いています。
- しかもECMAScript2015以降の記法でちゃんと書いたつもりです。
-
rgbTo16()
はRGBカラーを16進数に変換して、比較できるようにしています。 - 色によって叩くAPIが
POST(create)
なのかDELETE(destroy)
なのかを判別しているが、微妙なやり方だったかもしれない。
document.addEventListener('turbolinks:load', () => {
const LIKED_COLOR = '#ff3366';
const UNLIKED_COLOR = '#a0a0a0';
const LIKE_ENDPOINT = '/api/v1/likes';
const rgbTo16 = rgb => {
return '#' + rgb.match(/\d+/g).map((value) => {
return ('0' + parseInt(value).toString(16)).slice(-2)
}).join('');
}
const sendRequest = async (endpoint, method, json) => {
const response = await fetch(endpoint, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
method: method,
credentials: 'same-origin',
body: JSON.stringify(json)
});
if (!response.ok) {
throw Error(response.statusText);
} else {
return response.json();
}
}
const createLike = movieId => {
sendRequest(LIKE_ENDPOINT, 'POST', { movie_id: movieId })
.then(data => {
document.getElementById('like_id').value = data.like_id
});
}
const deleteLike = likeId => {
const DELETE_LIKE_ENDPOINT = LIKE_ENDPOINT + '/' + `${likeId}`;
sendRequest(DELETE_LIKE_ENDPOINT, 'DELETE', { id: likeId })
.then(() => {
document.getElementById('like_id').value = '';
});
}
const likeButton = document.getElementById('js-like-button');
if (!!likeButton) {
likeButton.addEventListener('click', () => {
const currentColor = rgbTo16(likeButton.style.color);
const likeId = document.getElementById('like_id').value;
const movieId = document.getElementById('movie_id').value;
if (currentColor === UNLIKED_COLOR) {
likeButton.style.color = LIKED_COLOR;
createLike(movieId);
}
else {
likeButton.style.color = UNLIKED_COLOR;
deleteLike(likeId);
}
});
}
});
忘れずにapplication.jsで、書いたファイルを読み込んでおく。
いつも思うんだが、application.jsって import ~ from ~
で書いちゃってもいいんだろうか。
require('../likes')
パターン2は以上になります。
渾身のJSコードですが、長いですね汗
まとめ
簡単なことはRailsが提供してくれるものを使うと早い。
参考リンクは思い出したら貼ります。
感想やレビューいただけると嬉しいです。