LoginSignup
3
6

More than 3 years have passed since last update.

【Rails・JavaScript・Ajax】いいね機能を2通りの方法で作ってみた

Last updated at Posted at 2020-12-15

開始

この記事は 駆け出しエンジニアの第一歩! Advent Calendar 2020 の 16日目 の記事です。

AdventCalendar初参加記事です。エンジニア2年目です。

いいね機能の記事自体は何番煎じ感はありますが、やりたかったことは、JSで書くのはコードも増えて大変という主張です。

いいね機能を作ります。

まず、モデルは両パターン共通で大体こんな感じ。

app/models/like.rb
class Like < ApplicationRecord
  belongs_to :movie
  belongs_to :user

  LIKED_COLOR = '#ff3366'.freeze
  UNLIKED_COLOR = '#A0A0A0'.freeze
end
app/models/movie.rb
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版

ルーティングを定義。

config/routes.rb
resources :movies, only: :show
resources :likes, only: %i[create destroy]

コントローラのポイントとしては、非同期で create , destroy アクションが発生する場合、
それぞれ create.js.slim destroy.js.slim を自動的に render するので、それぞれに渡す @moviebefore_action で定義しておいたことくらい。

app/controllers/likes_controller.rb
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 を使って非同期でアクションを発生させるようにしています。

app/views/movies/show.html.slim
#js-like-button
  = render 'like', movie: @movie
app/views/movies/_like.html.slim
- 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の記法がちょっと独特かも。 createdestroy もrenderする内容は同じ。

app/views/likes/create.js.slim
| document.getElementById('js-like-button').innerHTML = "#{j(render 'movies/like', movie: @movie)}";
app/views/likes/destroy.js.slim
| document.getElementById('js-like-button').innerHTML = "#{j(render 'movie/like', movie: @user)}";

パターン1はこれで完成。Railsのおかげで、さくっとできてしまいます。

【パターン2】 JavaScript版

ルーティングを定義。 いいね機能用の createdestroy アクションは、JavaScriptにJSONを渡すAPIとして定義します。

config/routes.rb
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 } を付与する

app/controllers/api/v1/likes_controller.rb
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_idmovie_id を定義しています。

app/views/movies/show.html.slim
#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}"

いいねボタンがクリックされたときに、 createdestroy のAPIを叩くJSを書きます。

  • JSのリクエストはfetch APIで書いています。
    • しかもECMAScript2015以降の記法でちゃんと書いたつもりです。
  • rgbTo16() はRGBカラーを16進数に変換して、比較できるようにしています。
  • 色によって叩くAPIが POST(create) なのか DELETE(destroy) なのかを判別しているが、微妙なやり方だったかもしれない。
app/javascript/likes.js
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 ~ で書いちゃってもいいんだろうか。

app/javascript/packs/application.js
require('../likes')

パターン2は以上になります。
渾身のJSコードですが、長いですね汗

まとめ

簡単なことはRailsが提供してくれるものを使うと早い。

参考リンクは思い出したら貼ります。

感想やレビューいただけると嬉しいです。

3
6
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
3
6