Rails・Haml・Ajaxでいいね機能を実装

# いいね機能とは

ユーザーが投稿した記事や商品に対して、「いいね」をつけます。
以下が完成形です。

[![Image from Gyazo](https://i.gyazo.com/bde73666c6fc44234caadd0b151b392f.gif)](https://gyazo.com/bde73666c6fc44234caadd0b151b392f)

# 実装方法
今回はRails、Hamlという環境で実装します。
ページのリロードはせず、非同期通信で画面を変化させます。

ポイントは以下の通りです。
・ユーザーはひとつの記事や商品に対し、一度しか「いいね」できない。
・「いいね」するとボタンの色が変わり数字が増え、もう一度押すと取り消され元のボタンに戻る。

# 実装の準備

では、順をおって実装していきましょう。
今回はフリーマーケットサイトにおいて、ユーザーが出品した商品に対しての「いいね」になります。

### 各モデルの準備

ユーザー(user)が商品(product)に対し「いいね(like)」するので、
・users
・products
・likes
以上三つのモデル、テーブルが必要です。

ですが、今回は「いいね」の実装ということで、userとproductがすでに存在している前提でのお話になります。

いいね(like)は、それをしたユーザー(user)とそれをされた商品(product)に紐づいているので、それに応じたアソシエーションを組む必要があります。

では、それぞれをつくっていきましょう。

```:ターミナル
$ rails g model like
```
Modelの命名規則は単数形なので、ここでも単数形で書くのが良さそうです（複数形で書いても勝手に単数形モデルになりますが・・・）。

### アソシエーションを組む
ユーザー(user)と商品(product)は「いいね(like)」をたくさん持っていて、「いいね(like)」はそれらに属しています。

ですので、カラムをつくるときは、likesテーブルに「user_id」と「product_id」を持たせます。

```ruby:db/migrate
class CreateLikes < ActiveRecord::Migration[5.2]
  def change
    create_table :likes do |t|
      t.references :product
      t.references :user
      t.timestamps
    end
  end
end
```

```:ターミナル
$ rake db:migrate
```

アソシエーションは、

```ruby:app/models/user.rb
class User < ApplicationRecord
 has_many :likes, dependent: :destroy
end
```

```ruby:app/models/product.rb
class Product < ApplicationRecord
 has_many :likes, dependent: :destroy
end
```

```ruby:app/models/like.rb
class Like < ApplicationRecord

 belongs_to :product
 belongs_to :user

 validates :user_id, presence: true
 validates :product_id, presence: true
 validates_uniqueness_of :product_id, scope: :user_id

end
```

「dependent: :destroy」
　userやproductが削除された際は、それに関連するlikeも一緒に消してねという意味です。

「validates :user_id, presence: true」「validates :product_id, presence: true」
　likeを生成するときはuser_idとproduct_idが空ではだめだよというバリデーション。

「validates_uniqueness_of :product_id, scope: :user_id」
　ひとつのproductにひとりのuserが複数回いいねできないよう、product_idとuser_idの組み合わせはひとつだけだよというバリデーションです。

とりあえずの紐づけができました。

### Likesコントローラーの作成

コントローラーも作ります。
「いいね(like)」は「生成」と「削除」のふたつが想定されるので、createアクションとdestroyアクションがありますね。

```:ターミナル
$ rails g controller likes
```
コントローラーの命名規則は複数形です。

```ruby:app/controllers/likes_controller.rb
class LikesController < ApplicationController

  def create
  end

  def destroy
  end

end
```

### ルーティング

「いいね(like)」はproductに対してのもので、likesコントローラーで持ち主のproduct_idが必要なので、productにネストさせます。

しかし、今回はすでにproductがuserにネストされているので、禁断の２段階ネストになっています。

```ruby:config/routes.rb
resources :users, only: [:show, :edit, :update] do
 resources :products, only: [:new, :create, :show] do
  resources :likes, only: [:create, :destroy]
 end
end
```

ここまでが準備です。

# 実装する

では、実装していきましょう。

まず、全体のイメージをざっくり説明です。
実装は大きく分けて三つ、
１.ユーザーがすでに「いいね」しているかどうかで、ボタンのスタイルが違う。
2.ボタンを押したら数字が増え、もう一度押すと数字が減る。
3.ボタンが押されるというアクションによって、ボタンの状態を切り替える。

まず、１
これはビューにおいてボタンのスタイルを２つつくり、ifを使って場合分けします。

次に、２
「いいね」の数はテーブルにあるレコードの数なので、「いいね」するとレコードが増え、その分数字が増える。減るのは、レコードが削除されるため。

最後に、３
これは非同期通信によるものです。
具体的には、１で説明したビューが部分テンプレートになっており、ボタンが押されたことにより、この部分テンプレートに送る値や条件を変化させています。

この説明ではわかりづらすぎるので、実際に手順を追っていきます。

### カウント機能の作成

likesテーブルにあるproduct_idごとのレコードを集計します。
まずは、likeモデルに追加。

```ruby:app/models/like.rb
class Like < ApplicationRecord
#counter_cache: :likes_count　を追加
 belongs_to :product, counter_cache: :likes_count
 belongs_to :user

end
```

「counter_cache: :likes_count」
　リレーションされたlikeの数を数え、productのlikes_countカラムに入れるという意味。likeがcreateされるとlike_countが＋１、destroyされると-1します。

よって、productsテーブルにlikes_countカラムを追加しなければなりません。

```:ターミナル
$ rails g migration AddClomunToLikes
```

```ruby:db/migrate
class AddClomunToLikes < ActiveRecord::Migration[5.2]
  def change
    add_column :products, :likes_count, :integer
  end
end
```

```:ターミナル
$ rake db:migrate
```

こんな感じのものができあがります。
[![Image from Gyazo](https://i.gyazo.com/6753730e2dae3655aabc2f1c87810c2a.png)](https://gyazo.com/6753730e2dae3655aabc2f1c87810c2a)

実際に「いいね」していくと、このように数字がカウントされます。

### ビューの作成

ビューの作成ですが、まずはユーザーがすでに「いいね」しているのかどうかのメソッドをつくります。

```ruby:app/models/product.rb
class Product < ApplicationRecord
 ...

#以下を追加
 def like_user(user_id)
  likes.find_by(user_id: user_id)
 end
end
```
引数として渡されたuser_idが、likesテーブルのuser_idカラムにすでに存在しているかを確かめ、true、falseを返します。


```ruby:app/views/products/show.html.haml
...

.item-button-container
  .item-button-container__left
    = render partial: 'likes/like', locals: { product: @product, products: @products, likes: @likes, like: @like}

...
```

```ruby:app/views/likes/_like.rb
#ユーザーがサインインしているかどうか
- if user_signed_in?
　#ログインしているユーザーがすでに「いいね」しているかどうか
  - if product.like_user(current_user.id)
    .item-button-container__left__dislike
      =link_to user_product_like_path(current_user, product, like), method: "DELETE", remote: true do
        %i.fas.fa-heart
        %span いいね！
        %span 
          = product.likes_count
  - else
    .item-button-container__left__like
      =link_to user_product_likes_path(current_user, product), method: "POST", remote: true do
        %i.fas.fa-heart
        %span いいね！
        %span
          = product.likes_count
-else
  .item-button-container__left__like
    %i.fas.fa-heart
      %span いいね！
      %span = product.like_count
```

「= product.likes_count」で、さきほどのいいねのカウント数が表示されます。

# Ajax通信

ボタンを押すというアクションによってAjax通信をします。

といっても、link_toメソッドにremote: trueを記述しているので、jsファイルに通信のあれこれを定義することはありません。すでにこのlink（今回のボタン）はAjaxになっています。

「いいね」を押した先のコントローラー

```ruby:app/controllers/likes_controller.rb
class LikesController < ApplicationController

  def create
    @like = Like.create(user_id: current_user.id, product_id: params[:product_id])
    @likes = Like.where(product_id: params[:product_id])
    get_product
  end

  def destroy
    @like = Like.find_by(user_id: current_user.id, product_id: params[:product_id])
    @like.destroy
    @likes = Like.where(product_id: params[:product_id])
    get_product
  end

  def get_product
    @product = Product.find(params[:product_id])
  end
end
```

ボタンによってcreate、destroyをするとlikes_countの数値が変わるので、数値の変わったlikesとproductを改めて取得してビューに渡します。

最後に、これらの値が渡ったjsファイルをつくります。

```:app/views/likes/create.js.haml

$(".item-button-container__left").html("#{escape_javascript(render partial: 'like', locals: { product: @product, products: @products, likes: @likes, like: @like})}")
```

```:app/views/likes/destroy.js.haml
$(".item-button-container__left").html("#{escape_javascript(render partial: 'like', locals: { product: @product, products: @products, likes: @likes, like: @like})}")
```

ここでは部分テンプレートのみを書き換えています。

つまり、部分テンプレートに渡す値を「いいね」が追加、削除された後のものに置き換えているということです。
これにより、現在のユーザーの「いいね」も追加されたり削除されるので、条件分岐によりボタンのスタイルが変わります。

# まとめ
Ajax通信で部分テンプレートを差し替えるというテクニックは他にも応用ができそうです。

仕組みが複雑で説明がとても難しく散漫な記事となってしまいましたので、参考記事とともにご覧いただければ幸いです。

https://qiita.com/YuitoSato/items/94913d6a349a530b2ea2
https://www.prime-architect.co.jp/myblog/ruby-on-rails-1559