Help us understand the problem. What is going on with this article?

いいね機能を非同期で実装【Rails】【jQuery】

この記事の内容

like.gif
TODOリストを共有できるアプリを作っていて、いいね機能を非同期にて実装しました。
すでにたくさんのQiita記事がありますが、つまったポイントもあったので、自分なりにまとめ直してみます。
(コンセプトは「人生でやりたいこと100のリストの共有」なので、todoをdreamという言葉を使って表現しています。)

前提

Rails 5.2.3

構成

userの詳細ページにdreamリストが表示されています。
viewの構成としては、
/views/users/show.html.erb内で同じ階層の_dream.html.erbが部分テンプレートとして呼ばれ、userのdreamを繰り返し表示しています。

CSSフレームワークはMaterializeを使用しています。

アソシエーション:
users - has_many :dreams, has_many :likes
dreams - belongs_to :user, has_many :likes
likes - belongs_to :user, belongs_to :dream

流れ

  1. jQueryの準備
  2. いいねボタンを作成
  3. コントローラ記述
  4. remote: trueにてjs.erbファイルを呼び出し
  5. js.erbファイル作成

実行

1. jQueryの準備

非同期化するにあたり、Rails内でjQueryを使えるように準備します。
まずはgemの導入です。

Gemfile
gem 'jquery-rails'

ターミナルで bundle installします。
そしてapplication.jsに記述を追加します。

app/assets/javascripts/application.js
//= require jquery3
//= require rails-ujs
//= require_tree .

順番が重要です。jqueryを最初に読み込む必要があります。

2. いいねボタンを作成

後から使い回ししやすいように、部分テンプレート化しています。

_dream.html.erbではテーブルでtodoリストを表示しているので、いいねの項目がtd内に入っています。
idの記載については5の項目で説明します。

app/views/users/_dream.html.erb
# いいね機能該当部分
<td id="like-<%= dream.id %>">
  <%= render partial: "like", locals: { dream: dream } %>
</td>

_like.html.erbではすでにいいねがあるかないかで★か☆かを出し分けて(Materializeのアイコンを使用しています)、最後にいいね数をdream.likes.lengthで表示しています。

app/views/users/_like.html.erb
<% if Like.find_by(user_id: current_user.id, dream_id: dream.id) %>
  <%= link_to "/dreams/#{dream.id}/likes", method: :delete %>
    <i class="material-icons">star</i>
  <% end %>
<% else %>
  <%= link_to "/dreams/#{dream.id}/likes", method: :post %>
    <i class="material-icons">star_border</i>
    <% end %>
<% end %>
<%= dream.likes.length %>

3. コントローラ記述

いいねのcreateとdestroyを定義していきます。

app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :set_dream

  def create
    @like = Like.create(user_id: current_user.id, dream_id: @dream.id)
  end

  def destroy
    @like = Like.find_by(user_id: current_user.id, dream_id: @dream.id)
    @like.destroy
  end

  private
  def set_dream
    @dream = Dream.find(params[:dream_id])
  end
end

ルーティングも忘れずに。

config/routes.rb
post '/dreams/:dream_id/likes' => "likes#create"
delete '/dreams/:dream_id/likes' => "likes#destroy"

この時点で、非同期ではないですがいいね機能が実装できているはず。

4. remote: trueにてjs.erbファイルを呼び出し

いいねボタンのlink_toremote: trueを追加します。

app/views/users/_like.html.erb
<% if Like.find_by(user_id: current_user.id, dream_id: dream.id) %>
  <%= link_to "/dreams/#{dream.id}/likes", method: :delete, remote: true do %>
    <i class="material-icons">star</i>
  <% end %>
<% else %>
  <%= link_to "/dreams/#{dream.id}/likes", method: :post, remote: true do %>
    <i class="material-icons">star_border</i>
    <% end %>
<% end %>
<%= dream.likes.length %>

この記述により、通常であれば、link_toで呼ばれるアクションに対応するhtml.erbファイルを呼び出すところ、js.erbファイルを呼び出せるようになります。
なのでページ遷移を行わず非同期で通信が行われるようになります。

5. js.erbファイル作成

js.erbファイルはその名前の通り、javascriptのファイルでありながら、ERBタグを使うことでrubyのコードを書ける優れものです。
コントローラーで定義したインスタンスを<% %><%= %>を使うことでそのままjsファイルに記述できます。

まずはcreateとdestroyそれぞれのアクションに対応するjs.erbファイルを作成します。
app/views/likes/create.js.erb
app/views/likes/destroy.js.erb

ここで2.で記述していたidについて説明します。

app/views/users/_like.html.erb(再掲)
<td id="like-<%= dream.id %>">
  <%= render partial: "like", locals: { dream: dream } %>
</td>

jsでイベントを発火させるためには、idまたはclassでセレクタを指定しますが、今回はどのdreamに対するいいねなのかを判別するために、id内にそのdreamのidを含める必要があります。
上のように書くことによって、id="like-1"のようなidを指定することができます。

ここまで来たらあとはjs.erbファイルの記述だけです。

app/views/likes/create.js.erb
$("#like-<%= @dream.id %>").html("<%= j(render partial: 'users/like', locals: { dream: @dream }) %>");
app/views/likes/destroy.js.erb
$("#like-<%= @dream.id %>").html("<%= j(render partial: 'users/like', locals: { dream: @dream }) %>");

これだけです。

まずはセレクタの指定ですが、js.erbファイルなのでコントローラで定義した変数が使えます。
id="like-<%= dream.id %>"に対応するように指定します。

そして、jQueryのhtml()メソッドで、指定したセレクタのhtmlを置き換えます。

その置き換える内容が
"<%= j(render partial: 'users/like', locals: { dream: @dream }) %>"の部分です。
部分テンプレートの_like.html.erbを呼び出しています。
(renderの前にあるjは、escape_javascriptのエイリアスで、改行と括弧をエスケープしてくれるメソッドです。)

これにより、likeが更新された状態で、ifで条件分岐されたりlikes.countが表示されたりします。

非同期処理の実現!

一度流れをつかむことができれば応用が効きそうなので、今後もいろいろなところで使ってみようと思います。

参考

Railsで remote: true と js.erbを使って簡単にAjax(非同期通信)を実装しよう!(いいね機能のデモ付)


分かりにくい点・間違っている点などがありましたらご指摘いただきますよう、よろしくお願いいたします。

fumikao
大学卒業後4年間、証券会社で営業をしていました。 退職してプログラミングスクールで3ヶ月学習し、無事Webエンジニアになりました!
https://www.fumika.tech/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away