LoginSignup
6

More than 1 year has passed since last update.

posted at

updated at

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

開始

この記事は 駆け出しエンジニアの第一歩! 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が提供してくれるものを使うと早い。

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

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

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
What you can do with signing up
6