【Rails×Ajax】いいね機能の実装で上手く出来ないあなたへの2つの注意喚起 #学習者向け


目的

はじめまして。
今回、Railsでいいね機能をQiita等の記事通りに行っても上手く行かない!という方へ向けた、ちょっとした実装の際のチェック項目を列挙させていただきます。

前提条件

対象となる読者

  • 「いいね機能」の実装において、railsの同期処理では問題なく処理されるが、Ajax通信が上手く行かない

開発環境

  • ruby 2.5.1
  • Rails 5.2.4.1
  • mysql Ver 14.14

実装済み機能

  • 同期処理で「いいね機能」が正しく処理されていること

筆者が参考にした記事

Ajax処理のいいね機能の実装方法のチェック項目

ずばり、先に結論をここで提示させていただきます

1. 部分テンプレートの呼び出しが正しく相対パスで指定されているか

2. インスタンス変数の指定がfavorites_controller.rbで指定されているか

の2点です。では、詳しく見ていきましょう。

1. 部分テンプレートの呼び出しが正しく相対パスで指定されているか

よく陥りがちなミスの一つですね。実際にどのように間違えて実装しどの様なエラー文が出たのでしょうか?

view/items/show.html.haml
.btn-bar
  .btn-box
    = render partial: "favorite_ajax", locals: { item: @item }
-# view/items/_favorite_ajax.html.hamlでいいねボタンを部分テンプレートを作成した

view/items/_favorite_ajax.html.haml
- if user_signed_in? -# ユーザーがログインしているか判断
  - if item.favorited_by?(current_user) -# ログイン中のユーザーがいいねしているかしていないかを判断
    = link_to item_favorites_path(item.id), method: :delete, class: "favorite red", remote: true do -# リクエストをjs形式で送信
      = icon('fas', 'heart')
      いいね!
      = item.favorites.count
  - else
    = link_to item_favorites_path(item.id), method: :post, class: "favorite", remote: true do -# リクエストをjs形式で送信
      = icon('far', 'heart')
      いいね!
      = item.favorites.count
- else
  = link_to new_user_session_path, class: "favorite", remote: false do -# リクエストをhtml形式で送信
    = icon('far', 'heart')
    いいね!
    = item.favorites.count
view/favorites/create.js.haml(失敗例)
$('.btn-box').html("#{escape_javascript(render partial: "favorite_ajax", locals: { item: @item })}");
-# この記述ではview/favorites/_favorite_ajax.html.hamlを呼び出していることとなる。従って、対応するファイルが無いことからTemplate::Error(Missing partial)が発生
view/favorites/destroy.js.haml(失敗例)
$('.btn-box').html("#{escape_javascript(render partial: "favorite_ajax", locals: { item: @item })}");
-# この記述ではview/favorites/_favorite_ajax.html.hamlを呼び出していることとなる。従って、対応するファイルが無いことからTemplate::Error(Missing partial)が発生

エラー文

items_controller.rbのshowアクションのビューでいいね機能の実装をしています。また、いいね機能のDBへの保存・削除はfavorites_controller.rbのcreateアクション・destroyアクションで実装をしています。

今回、いいねボタンを押した際にビューが切り替わる部分をview/items/_favorite_ajax.html.hamlで切り出し部分テンプレートを作成しました。ajaxではview/favorites/create.js.haml, view/favorites/destroy.js.hamlをそれぞれ用意し、view/items/_favorite_ajax.html.hamlを呼び出したかったのですが、相対パスの指定が誤っていました。以下のように修正するとTemplate::Error(Missing partial)は解消されます。

view/favorites/create.js.haml
$('.btn-box').html("#{escape_javascript(render partial: "items/favorite_ajax", locals: { item: @item })}");
-# partial: にitems/ を追加
view/favorites/destroy.js.haml
$('.btn-box').html("#{escape_javascript(render partial: "items/favorite_ajax", locals: { item: @item })}");
-# partial: にitems/ を追加

2. インスタンス変数の指定がfavorites_controller.rbで指定されているか

こちらはまず、どんなエラー文が出たか確認して見ましょう

renderの中身のitem.favorited_by?に対して

undefined method `favorited_by?' for nil:NilClass

とエラーが出ています。ここでいうitemとはitems_controller.rbのshowアクションで定義されているインスタンス変数@itemをrenderの中身ではitemとして記述している、という意味です。favorited_by?については、item.rbで事前に定義した「ログイン中のユーザーがいいねしているかしていないかを判断」するメソッドです。

models/item.rb
class Item < ApplicationRecord
# (中略)
  def favorited_by?(user)
    favorites.where(user_id: user.id).exists?
  end
end

このことから、
 render内ではitem.favorited_by?が定義されていない
→ render内ではitemそのものが定義されていない
view/favorites/destroy.js.hamlでは、@itemが定義されていない
favorites_controller.rbでは、@itemが定義されていない!!

ということが判明しました。確認してみると確かにfavorites_controller.rbでは、@itemが定義されていなかったので、以下のように記述を加えたところ、正しくAjax処理が実行されました。
(items_controller.rbでも同様のset_itemメソッドを定義済みです)

favorites_controller.rb
class FavoritesController < ApplicationController
  before_action :authenticate_user!
# 追記==========================================================================
  before_action :set_item 
# ==============================================================================
  def create
    favorite = current_user.favorites.build(item_id: params[:item_id])
    if favorite.save
    else
      flash.now[:alert] = favorite.errors.full_messages
    end
  end

  def destroy
    favorite = Favorite.find_by(item_id: params[:item_id], user_id: current_user.id)
    if favorite.destroy
    else
      flash.now[:alert] = '削除できませんでした。'
    end
  end

  private
# 追記==========================================================================
  def set_item
   @item = Item.find(params[:item_id])
  end
# ==============================================================================
end

まとめ

いかがだったでしょうか。
いいね機能のAjaxは、実装の手順そのものはすごくシンプルです。しかし、いいね機能専用のビューを用意していなかったり、部分テンプレートの保存場所の違いによって記述内容が異なるケースがあります。当たり前のことではあるのですが、記事通り実装してみて上手く出来なかった時、解決の一助となれば幸いです。

※私自身初めてのQiitaの投稿です!
ご指摘等ございましたらコメントにてお待ちしております。