【Railsチュートリアル】いいね機能の実装(アニメーション付)

#はじめに

前回の記事[改【Railsチュートリアル】プロフィール画像をアプリ内で設定できるように変更](https://qiita.com/supyolo888/items/c1073ada0b04fb2f9e7f)に引き続きRailsチュートリアル第6版sample_appの機能拡張を進めていく。
今回は以下の様ないいね機能を実装していく。
instagramのいいねボタンによく似たハートが膨らむアニメーションを付けている。

![like.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2200191/4356d96a-67eb-42a5-6601-9e1a9e8d4a1e.gif)

**前提**
第6版sample_appが完成している

#Favoriteモデル

Favoriteテーブルを追加

```
$ rails g model Favorite user_id:integer micropost_id:integer
```

user_idとmicropost_idにインデックスを追加し、組み合わせがユニークであるという複合キーインデックスも追加する。

```rb:db/migrate/[timestamp]_create_favorites.rb
class CreateFavorites < ActiveRecord::Migration[6.1]
  def change
    create_table :favorites do |t|
      t.integer :user_id
      t.integer :micropost_id

      t.timestamps
    end
    add_index :favorites, :user_id
    add_index :favorites, :micropost_id
    add_index :favorites, [:user_id, :micropost_id], unique: true
  end
end
```

```
$ rails db:migrate
```

ユーザーとマイクロポストに、Favoriteに対して1対多の関連づけを追加し、また`has_many through`を用いて`user.likes`と`micropost.liked_by`を使えるようにする。

```rb:app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  has_many :favorites, dependent: :destroy
  has_many :likes, through: :favorites, source: :micropost
  .
  .
  .
end
```

```rb:app/models/micropost.rb
class Micropost < ApplicationRecord
  .
  .
  .
  has_many :favorites, dependent: :destroy
  has_many :liked_by, through: :favorites, source: :user
  .
  .
  .
end
```

```rb:app/models/favorite.rb
class Favorite < ApplicationRecord
  belongs_to :user
  belongs_to :micropost
  validates :user_id, presence: true
  validates :micropost_id, presence: true
end
```

ユーザーモデルにメソッドを追加する。

```rb:app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # マイクロポストをライクする
  def like(micropost)
    likes << micropost
  end
  
  # マイクロポストをライク解除する
  def unlike(micropost)
    favorites.find_by(micropost_id: micropost.id).destroy
  end
  
  # 現在のユーザーがライクしていたらtrueを返す
  def likes?(micropost)
    likes.include?(micropost)
  end

  private
  .
  .
  .
end
```

ルーティングを追加する。

```rb:config/routes.rb
Rails.application.routes.draw do
  .
  .
  .
  resources :users do
    member do
      get :following, :followers , :likes
    end
  end
  .
  .
  .
end
```

#likeページの作成

ステータスページにlike数を追加。

```rb:app/views/shared/_status.html.erb
<% @user ||= current_user %>
  .
  .
  .
  <a href="<%= likes_user_path(@user) %>">
    <strong id="likes" class="stat">
      <%= @user.likes.count %>
    </strong>
    likes
  </a>
</div>
```

<img width="400" alt=スクリーンショット 2021-11-28 3.16.20.png src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2200191/a1dd0706-8e23-867b-d0d6-62da77808dcf.png>
ユーザーコントローラーにlikeアクションを追加。

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def likes
    @title = "Likes"
    @user  = User.find(params[:id])
    @microposts = @user.likes.paginate(page: params[:page])
    render 'show_like'
  end

  private
  .
  .
  .
end
```

ビューを作成する。
gravatarの部分は[こちらの記事](https://qiita.com/supyolo888/items/c1073ada0b04fb2f9e7f)でimage_tagに変更している。

```rb:app/viwes/users/show_like.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <% if @user.avatar? %>
        <%= image_tag @user.avatar.url, width: "100px", height: "100px", class:"icon"%>
      <% else%>
        <%= image_tag "default_user.jpeg", width: "100px", height: "100px", class:"icon"%>
      <% end %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @user.likes.any? %>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>
```

![スクリーンショット 2021-11-28 3.16.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2200191/501cd352-cf85-8a7c-278c-bc22cd836f3a.png)

#likeボタンの作成

yarnを用いてFontAwesomeをインストールする。

```
$ yarn add @fortawesome/fontawesome-free
```

以下の記述を追加する。

```js:app/javascript/packs/application.js
.
.
.
import '@fortawesome/fontawesome-free/js/all';
```

```scss:app/assets/stylesheets/custom.scss
$fa-font-path: '@fortawesome/fontawesome-free/webfonts';
@import '@fortawesome/fontawesome-free/scss/fontawesome';
@import '@fortawesome/fontawesome-free/scss/solid';
@import '@fortawesome/fontawesome-free/scss/regular';
@import '@fortawesome/fontawesome-free/scss/brands';
@import '@fortawesome/fontawesome-free/scss/v4-shims';
.
.
.
```

ルーティングを追加する。

```rb:config/routes.rb
Rails.application.routes.draw do
  .
  .
  .
  resources :favorites, only: [:create, :destroy]
end
```

マイクロポストパーシャルにrenderを追加する。

```rb:app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
  .
  .
  .
  </span>  <span class="like" id="like_form_<%= micropost.id %>">
    <% if micropost.liked_by.include?(current_user) %>
      <%= render "microposts/unlike", micropost: micropost %>
    <% else %>
      <%= render "microposts/like", micropost: micropost %>
    <% end %>
  </span>
</li>
```

likeパーシャル、unkikeパーシャルを追加する。

```rb:app/viws/microposts/_like.html.erb
<span class="timestamp"><%=micropost.liked_by.count%>&nbsp;like</span>
<%= form_for(current_user.favorites.build, remote: true, class:"btnlike") do |f| %>
  <div><%= hidden_field_tag :micropost_id, micropost.id %></div>
  <%= button_tag(class: "btn-like" ) do %>
<i class="far fa-heart heart-empty "></i>  <% end %>
<% end %>
```

```rb:app/viws/microposts/_unlike.html.erb
<span class="timestamp"><%=micropost.liked_by.count%>&nbsp;like</span>
<%= form_for(current_user.favorites.find_by(micropost_id: micropost.id),
             html: { method: :delete }, remote: true) do |f| %>
  <%= button_tag( class: "btn-like") do %>
<i class="fas fa-heart heart"></i>
  <% end %>
<% end %>
```

cssを追加する。

```rb:app/assets/stylesheets/custom.scss
.
.
.
.heart {
  color: rgb(255, 74, 74);
  font-size: 25px;
  animation: anime 1s;
}
@keyframes anime{
  15% { transform: scale(1.3); }
  20% { transform: scale(1); }
  }

.heart-empty {
  font-size: 25px;
  -webkit-text-stroke: 5px white
}
.btn-like:hover > .heart-empty {
  color: rgb(168, 168, 168);
}
.btn-like {
  position: relative;
  left: 60px;
}
.btn-like {
  border: none;
  background-color: rgb(255, 255, 255);
  :hover {
    background-color: rgb(255, 255, 255);
  }
}
```

Favoriteコントローラーを作成し、以下を記述する。

```
$ rails g controller Favorites
```

```rb:app/controllers/favorites_controller.rb
class FavoritesController < ApplicationController
  before_action :logged_in_user

  def create
    micropost = Micropost.find(params[:micropost_id])
    current_user.like(micropost)
    redirect_back(fallback_location: root_url)
  end

  def destroy
    micropost = Favorite.find(params[:id]).micropost
    current_user.unlike(micropost)
    redirect_back(fallback_location: root_url)
  end
end
```

#Ajaxによる非同期化

like_html.erbとunlike_html.erbに`remote:true`を追加する。

```rb:app/views/microposts/_like.html.erb
<%= form_for(current_user.favorites.build, remote: true) do |f| %>
.
.
.
```

```rb:app/views/microposts/_unlike.html.erb
<%= form_for(current_user.favorites.find_by(micropost_id: micropost.id),
             html: { method: :delete }, remote: true) do |f| %>
.
.
.
```

Favoriteコントローラーを変更する。

```rb:app/contorollers/favorites_contoroller.rb
class FavoritesController < ApplicationController
  before_action :logged_in_user

  def create
    @user = current_user
    @micropost = Micropost.find(params[:micropost_id])
    current_user.like(@micropost)
    respond_to do |format|
      format.html { redirect_back(fallback_location: root_url) }
      format.js
    end
  end

  def destroy
    @user = current_user
    @micropost = Favorite.find(params[:id]).micropost
    current_user.unlike(@micropost)
    respond_to do |format|
      format.html { redirect_back(fallback_location: root_url) }
      format.js
    end
  end
end
```

JS-ERbファイルを作成する。

```rb:app/views/favorites/create.js.erb
$("#likes").html('<%= @user.likes.count %>');
$("#like_form_<%= @micropost.id %>").html("<%= escape_javascript(render('microposts/unlike',
                micropost: @micropost)) %>");
```

```rb:app/views/favorites/destroy.js.erb
$("#likes").html('<%= @user.likes.count %>');
$("#like_form_<%= @micropost.id %>").html("<%= escape_javascript(render('microposts/like',
                micropost: @micropost)) %>");
```