この記事の内容
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
流れ
- jQueryの準備
- いいねボタンを作成
- コントローラ記述
- remote: trueにてjs.erbファイルを呼び出し
- js.erbファイル作成
実行
1. jQueryの準備
非同期化するにあたり、Rails内でjQueryを使えるように準備します。
まずはgemの導入です。
gem 'jquery-rails'
ターミナルで bundle install
します。
そしてapplication.jsに記述を追加します。
//= require jquery3
//= require rails-ujs
//= require_tree .
順番が重要です。jqueryを最初に読み込む必要があります。
2. いいねボタンを作成
後から使い回ししやすいように、部分テンプレート化しています。
_dream.html.erb
ではテーブルでtodoリストを表示しているので、いいねの項目がtd内に入っています。
id
の記載については5の項目で説明します。
# いいね機能該当部分
<td id="like-<%= dream.id %>">
<%= render partial: "like", locals: { dream: dream } %>
</td>
_like.html.erb
ではすでにいいねがあるかないかで★か☆かを出し分けて(Materializeのアイコンを使用しています)、最後にいいね数をdream.likes.length
で表示しています。
<% 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を定義していきます。
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
ルーティングも忘れずに。
post '/dreams/:dream_id/likes' => "likes#create"
delete '/dreams/:dream_id/likes' => "likes#destroy"
この時点で、非同期ではないですがいいね機能が実装できているはず。
4. remote: trueにてjs.erbファイルを呼び出し
いいねボタンのlink_to
にremote: true
を追加します。
<% 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
について説明します。
<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ファイルの記述だけです。
$("#like-<%= @dream.id %>").html("<%= j(render partial: 'users/like', locals: { dream: @dream }) %>");
$("#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(非同期通信)を実装しよう!(いいね機能のデモ付)
分かりにくい点・間違っている点などがありましたらご指摘いただきますよう、よろしくお願いいたします。