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

Railsでいいね機能を非同期で実装。js.hamlを使用

なにこれ

Railsでツイッターのように1つツイートに対して、
1人のユーザーがいいねを1回できることができ、
いいねボタンを押すと非同期で情報が変わるように作成します。

注意事項

初学者のコメント機能を実装した時の備忘録として書いてます。
長々と解説してますが、まずはコピペして実践してみるのをおすすめします。
筆者が言語化して理解するためにこの記事を書いてます。
ご指摘がありましたら何なりとお申し付け下さい。

前提条件

userテーブル、tweetテーブルは作成済みとして進みます。

全体の流れ

Likeモデルとコントローラーを作成
アソシエーションを組む
ルーティングの設定
コントローラーを編集する
いいねの部分テンプレートを作る
create.js.hamlとdestroy.js.hamlファイルを作成して非同期通信が行えるようにする。
以上!

LikeモデルとLikeコントローラーを作成

ターミナル
rails g model Like tweet_id:integer user_id:integer
rails db:migrate
rails controller likes create destroy

ここから基本一部抜粋です

モデルとアソシエーションの設定

user.rb
 has_many :likes

1つのuserは複数のlikeを持つのでhas_manyです。

tweet.rb
 has_many :likes, dependent: :destroy

 #既にいいねしているか確認するメソッド
  def like_user(user_id)
   likes.find_by(user_id: user_id)
  end

1つのツイートは複数のいいねを持つのでhas_manyです。
ツイートが消えたらいいねも消えてほしいのでdependent: :destroyを追加します。

like_userメソッドで、current_userが既にいいねをしているか確認します。
このメソッドのおかげでいいね済みor未いいねを判別してビューを変えます。

like.rb
belongs_to :tweet
belongs_to :user

validates :tweet_id, uniqueness: { scope: :user_id }

1つのいいねは1つずつのツイートとユーザーしか持たないので、belongs_toです。

バリデーションででuniquenessを追加することで
1つツイートに1つのユーザーしかいいねができないよう制限をかけます。
ルーティングの設定

ルーティングの設定をします。

routes
  resources :tweets do
#resources commentsはツイートに2つネストしてるだけです。気にしないで下さい。
    resources :comments, only: [:create, :edit, :update, :destroy]
    resources :likes, only: [:create, :destroy]
  end

ツイートにネストされた状態にします。
コメント機能などと同様にツイートに紐付いていいねができるので。

コントローラの編集

ライクコントローラーの編集

likes_controller.rb
  before_action :set_tweet

  def create
    like = Like.create(user_id: current_user.id, tweet_id: @tweet.id)
    like.save
    # @likes = Like.where(tweet_id: @tweet.id)
    # @tweet.reload
  end

  def destroy
    like = Like.find_by(user_id: current_user.id, tweet_id: @tweet.id)
    like.destroy
    # @likes = Like.where(tweet_id: @tweet.id)
    # @tweet.reload
  end

  private
  def set_tweet
    # @tweet = Tweet.find_by(id: params[:tweet_id])
    @tweet = Tweet.find(params[:tweet_id])
  end

解説します。
set_tweetメソッドで、現在表示しているorボタンを押したツイートを探して取得します。
set_tweetメソッドを消す代わりに、以下のように記述してもokです。
意味は同じです。書き方が違うだけです。

like = Like.create(user_id: current_user.id, tweet_id: params[:tweet_id])

createアクションの解説です。

likes_controller.rb
  def create
    like = Like.create(user_id: current_user.id, tweet_id: @tweet.id)
    # @likes = Like.where(tweet_id: @tweet.id)
    # @tweet.reload
  end

Like.createで新しいライクを作成します。
user_idにはcurrent_user.idを代入。
tweet_idは@tweet.idで現在取得しているツイートを代入します。
like.saveなどは必要ないですが、コードの可読性を高めるために書いてます
(そのコードがどんな処理をしてるか第三者から見て分かりやすいように)

destroyアクションは、いいねを削除している以外はcreateアクションとやってることは同じなので省略

likes_controller.rb
  # @likes = Like.where(tweet_id: @tweet.id)
    # @tweet.reload

コメントで伏せ字にしてある2行です。
私が参考にしたサイトは、書くことを推奨していたのですが、
自分が実験すると削除しても問題なく動作し、どこにこの変数が使われているか
分からなかったため、伏せ字にしてあります。

ツイートコントローラーの中身は編集しませんが、ビューの説明用に一応載せます。
1度も@like = Like.newも作りません。直接createするので必要なかったです。

tweet_controller.rb
def index
    @tweets = Tweet.includes(:user).order('updated_at desc').page(params[:page]).per(5)
  end

 def show
    @tweet = Tweet.find(params[:id])
    @comment = Comment.new
    @comments = @tweet.comments.includes(:user).order('created_at asc')
  end

次はビューの編集をします。

views/tweets/index.html.haml
.main
  .tweets
    .tweets__list
      = render 'tweet'

renderでツイートの一覧を表示しています。

views/tweets/_tweet.html.haml
- @tweets.each do |tweet|
  .tweets__list--item
    %div{id: "tweet_like_#{tweet.id}"}
      = render 'likes/like', tweet: tweet

下から1行目と2行目が重要です。
tweet_idというidを指定することで、コメント投稿機能と同様に
どのツイートなのかを識別しています。
js.hamlでrenderする送信先を指定している、イメージです。

例)tweet_idが1なら、

になる。
いいねアクションが起こると、このidで判断して情報を更新している。イメージです
views/tweets/_tweet.html.haml
      = render 'likes/like', tweet: tweet

renderの後に tweet: tweetと記載してますが、これは必要な情報です。
原因は分かりませんが、この記述がないとrender先である/views/likes/_like.html.haml/
で、変数tweetが読み込んでくれません。

自分の解釈だと、_tweetsで@tweetsをeach文でtweetに変えて1ツイートずつ表示しているので、文字としては同じtweetなんですが、render先だとtweetという変数が定義されてないので、tweet: tweet と記載する。みたいな感じです。
render先でも同じ変数として自動的に使わせてくれないんでしょうか??
renderさん難しい。

ツイート詳細ページにもrenderでいいねページを追加します。

views/tweets/show.html.haml
  .like
    %div{id: "like_#{@tweet.id}"}
      = render 'likes/like', tweet: @tweet

流れは先程と同じなのですが、showの場合はrenderでtweet: @tweetと指定しています。
この理由は、tweets_controllerでインスタンス変数@tweetと定義しているので、
このままだと/views/likes/_like.html.hamlで変数@tweetは使用できないので、
tweetで使えるようにしてます。

次の/views/likes/_like.html.hamlが一番の難関です!!

views/likes/_like.html.haml
- if user_signed_in?
  - if tweet.like_user(current_user.id)
    = link_to tweet_like_path(tweet.likes, tweet_id: tweet.id), method: :delete, remote: true do
      %i.fa.fa-heart{"aria-hidden" => "true", style: "color: red;"}
        = tweet.likes.count
  - else
    = link_to tweet_likes_path(tweet.id), method: :post, remote: true do
      %i.fa.fa-heart{"aria-hidden" => "true", style: "color: #C0C0C0;"}
        = tweet.likes.count
- else
  %i.fa.fa-heart{"aria-hidden" => "true"}
    = tweet.likes.count
dden" => "true"}
    = item.likes.count

rails routesも確認します。

tweet_likes POST   /tweets/:tweet_id/likes(.:format)                                                       likes#create
tweet_like DELETE /tweets/:tweet_id/likes/:id(.:format)                                                    likes#destroy

ここは大事なので全部の行で解説します。上から順に行きます。

ユーザーがサインインしてるか判別
like_userメソッドでそのツイートに対して既にいいねしてるか判別します。
既にいいねしてる場合は、いいねを削除するリンクを表示させます。

views/likes/_like.html.haml
tweet_like_path(tweet.likes, tweet_id: tweet.id)

この書き方で、likes#destroyを呼び出すことができます。
ネストしたリンク先を指定するのが複雑すぎる。詳しい方にご教授願いたいです。
likesの場合は複数あるからlikesだけど、commentとかの場合だと1つしかないから単数形にしないとダメみたいな感覚です。commentが複数だったら日本語的にもおかしいもんね

自分が調べた限りですと、以下の書き方でも正解でした。

views/likes/_like.html.haml
tweet_id: tweet.id ,tweet.likes[0].id
#likes[0]ってどういうこと笑。これが一番よく分からなかった。
tweet.id ,tweet.likes
#これが分かりやすいと思った。
tweet, tweet.likes
#すごい短い
tweet.likes, tweet_id: tweet.id
#わかり易さ重視。ちなみに順番入れ替えるとsyntax errorエラー出ました。よく分からん。

%iタグはボタン表示です。
tweet.likes.countでいいね数の一覧を表示します。
そのツイートが所持?繋がっている?いいね数です。
tweet.like_user(current_user.id)でelseだったら、まだいいねをしてないので
新しくいいねができるリンクを表示させます。
rails routesを見てもらえたら分かると思うんですけど、
新規投稿は1回しかid指定が必要ないから簡単なんですよね。
その後は先程と同じです。

ユーザーがサインインしてない場合は、リンクを表示せずにいいね数だけをカウントします。

js.hamlファイルを作成していきます

views/likes/create.js.haml
$("#like_#{@tweet.id}").html("#{j(render partial: 'likes/like', locals: { tweet: @tweet })}");
views/likes/destroy.js.haml
$("#like_#{@tweet.id}").html("#{j(render partial: 'likes/like', locals: { tweet: @tweet })}");

はい、実はどちらも中身は完全に同じです。
/tweets/_tweetと/tweets/showで指定してたidの部分に
renderしてフォームを送って部分テンプレートを呼び出して更新させるイメージです。

locals: { tweet: @tweet }

これの意味は正直理解できてません。でもこれがないと、
それぞれcreateとdestroyアクション発火時にDB上にデータは保存される
(いいねしたのは追加されてる)けど、非同期通信でエラーが発生します。
500エラーが出て部分テンプレートが更新されません。以下がエラー文です。

POST http://localhost:3000/tweets/5/likes 500 (Internal Server Error)

自分なりの解釈ですが、出来る限り説明します。

結論だけ先に言うと、renderでインスタンス変数@tweetを変数tweet両方とも使えるようにした。だと思います。

いいねでcreateアクションが起きるとどこの画面が更新されるかの流れで考えます。
tweet/からいいねを押した場合だと、
いいねをした時にまずrenderする前である_tweetに戻ります。
_tweetだと、@tweetsをeachしてtweetで繰り返し処理してます。
再度renderする前まで読み込まれて、render先でも変数tweetが使えるようになって無限ループする。みたいなイメージです。
でも、このイメージだと、2つ問題があります。
create.js.hamlで$("#like
#{@tweet.id}”)という記述がありました。
この場合だと、_tweetの時のビューの記述は以下のようでした。

_tweet.html.haml
%div{id: "like_#{tweet.id}"}

はい、idの指定部分がおかしいです。
先程の_likeまでの流れだと、インスタンス変数@tweetを変数tweetにしてました。
create.js.hamlの記述だと、tweetのidを指定する変数が違うので読み込まないと思いました。でも結論で言ったとおり変数が2つ両方とも使えるっていう認識だったら上手くいく。

tweets/showの方は文字通りだったので、省略
_tweetとshowでidを使い分けたほうが、
可読性は高まりそうだけど動くからこれでいいかなー
localより後に書いた部分は消せないし、renderって難しい。

今回も長くなりましたが、以上になります!
ここまで読んでいただきありがとうございます!

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
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした