Railsチュートリアルのサンプルアプリにlike(いいね)機能を拡張する

[Railsチュートリアル](https://railstutorial.jp/)で作成したサンプルアプリに、like(いいね)機能を拡張します。

## 環境
・Rails 5.1.6
・Railsチュートリアル第4版(Rails5.1) 14章まで実装済み

## 仕様
・プロフィールにユーザーのlike数が表示される
・like数をクリックすると、likeしたマイクロポスト一覧が表示される
・マイクロポストにlikeボタンとlikeされた回数が表示される
・likeボタンをクリックすると、like数とボタンのlike/unlikeが非同期で更新される

完成版の動作は以下の通りです。
![動作イメージ(軽量).gif](https://qiita-image-store.s3.amazonaws.com/0/362742/a05d4e62-7559-27b8-a4dd-d735f1fbff6f.gif)




## FavoriteRelationshipModel
### テーブルの作成
ユーザーは複数のマイクロポストをlikeでき、また、マイクロポストは複数のユーザーからlikeされます。そのため、フォロー機能のときと同じように、中間テーブルを作成して多対多の関係を作っていきます。モデル名は、チュートリアルにならってFavoriteRelationshipとします。このFavoriteRelationshipモデルは、ユーザとマイクロポストを結びつけるため、user_idとmicropost_idを属性として持たせます。まずは以下のコマンドを実行して、マイグレーションを生成します。
`rails generate model FavoriteRelationship user_id:integer micropost_id:integer`

次に、インデックスを追加します。さらに、user_idとmicropost_idの組み合わせがユニークであるという制約も追加します。

```ruby:/sample_app/db/migrate/[timestamp]_create_favorite_relationships.rb
class CreateFavoriteRelationships < ActiveRecord::Migration[5.1]
  def change
    create_table :favorite_relationships do |t|
      t.integer :user_id
      t.integer :micropost_id

      t.timestamps
    end
    #indexを追加
    add_index :favorite_relationships, :user_id
    add_index :favorite_relationships, :micropost_id
    add_index :favorite_relationships, [:user_id, :micropost_id], unique: true
  end
end
```
最後に以下のコマンドを実行して、FavoriteRelationshipテーブルを作成します。
`rails db:migrate`

### User/Micropostの関連付け
ユーザーは複数のマイクロポストをlikeでき、マイクロポストは複数のユーザーからlikeされるという多対多の関係を、FavoriteRelationshipモデルを用いて定義します。
まず、ユーザーが、likeしたマイクロポストを複数持つことを、FavoriteRelationshipモデルを用いて以下のように定義します。これにより、`user.likes`でそのユーザーがlikeしたマイクロポスト一覧を取得可能になります。

```ruby:/sample_app/app/models/user.rb
class User < ApplicationRecord
  :
  :
  has_many :favorite_relationships, dependent: :destroy
  has_many :likes, through: :favorite_relationships, source: :micropost
  :
  :
end
```
同様にして、マイクロポストが、likeしたユーザーを複数持つことを以下のように定義します。これにより、`micropost.liked_by`で、そのマイクロポストをlikeしたユーザー一覧が取得できるようになります。

```ruby:/sample_app/app/models/micropost.rb
class Micropost < ApplicationRecord
  :
  :
  has_many :favorite_relationships, dependent: :destroy
  has_many :liked_by, through: :favorite_relationships, source: :user
  :
  :  
end
```
最後に、FavoriteRelationshipをユーザーとマイクロポストの両方に属するように定義します。

```ruby:/sample_app/app/models/favorite_relationship.rb
class FavoriteRelationship < ApplicationRecord
  belongs_to :user
  belongs_to :micropost
  validates :user_id, presence: true
  validates :micropost_id, presence: true
end
```

### likes関連のメソッドの追加
マイクロポストのlikeやlike解除を簡単に行えるようにするために、便利なメソッドを追加します。

```ruby:/sample_app/app/models/user.rb
class User < ApplicationRecord

class UserTest < ActiveSupport::TestCase
  :
  :
  # マイクロポストをライクする
  def like(micropost)
    likes << micropost
  end

  # マイクロポストをライク解除する
  def unlike(micropost)
    favorite_relationships.find_by(micropost_id: micropost.id).destroy
  end

  # 現在のユーザーがライクしていたらtrueを返す
  def likes?(micropost)
    likes.include?(micropost)
  end

  private
  :
  :
end
```
これにより、`user.like(micropost)`でマイクロポストをlikeし、`user.unlike(micropost)`でマイクロポストのlikeを解除することができるようになりました。

## likeページの作成
ユーザーのステータス部分にlike数を表示し、そこをクリックするとlikeしたマイクロポスト一覧を表示できるようにします。完成イメージは以下の通りです。
<img width="1156" alt="likeページ.png" src="https://qiita-image-store.s3.amazonaws.com/0/362742/85d442bd-2867-415c-bc2f-1ebceb3b9830.png">

### like数の表示
ユーザーのステータス部分に、そのユーザーがlikeしたマイクロポストの総数を表示します。
まずは、以下のようにルーティングを追加します。

```ruby:/sample_app/config/routes.rb
:
:
resources :users do
  member do
    get :following, :followers , :likes
  end
end
:
:
```
これにより、以下のようなルーティングテーブルが生成されます。

| HTTPリクエスト | URL           | アクション          | 名前付きルート       |
|:-----------|:---------------|:----------------|:------------------|
| GET        | /users/1/likes | likes           | likes_user_path(1)|

次に、統計情報パーシャルに以下を追記することによって、プロフィールページとHomeページのステータス部分にlike数を表示します。

```erb:/sample_app/app/views/shared/_stats.html.erb
.
.
<a href="<%= likes_user_path(@user) %>">
    <strong id="likes" class="stat">
      <%= @user.likes.count %>
    </strong>
    likes
</a>
</div>
```
これで、ユーザーのステータスにlike数が追加されました。

### likeしたマイクロポスト一覧の表示
ユーザーのプロフィール内のlike数をクリックすると、そのユーザーがこれまでにlikeしたマイクロポスト一覧が表示されるようにします。
まず、`users_controller.rb`にlikesアクションを追加します。

```ruby:/sample_app/app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                        :following, :followers, :likes]
:
:
def likes
    @title = "Likes"
    @user  = User.find(params[:id])
    @microposts = @user.likes.paginate(page: params[:page])
    render 'show_like'
end
  
  private
:
:
```
likesアクションでは、`@microposts`にユーザーがlikeしているマイクロポストを代入し、`show_like`ビューを描画しています。このビューは以下の通り作成します。

```erb:/sample_app/app/views/users/show_like.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <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>
```
以上で、likeしたマイクロポスト一覧を表示できるようになりました。

## likeボタンの作成
マイクロポストにlikeボタンを表示し、クリックでlike/unlikeの切り替えができるようにします。

### likeボタンの表示
likeページのときと同様に、まずはルーティングの追加から始めます。以下のように、FavoriteRelationshipリソース用のルーティングを追加します。

```ruby:/sample_app/config/routes.rb
:
:
resources :favorite_relationships, only: [:create, :destroy]
end
```

次に、micropostパーシャルに、like/unlikeボタンの表示を追加します。以下では、current_userがそのマイクロポストをlikeしているかどうかで、表示するボタンを切り替えています。また、後でAjaxを実装して非同期でボタンを切り替えるために、ユニークなidをspanタグに割り振っています。

```erb:/sample_app/app/views/microposts/_micropost.html.erb
:
:
  <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ボタンとunlikeボタンのパーシャルは、それぞれ以下の通りです。ボタンには、Bootstrap3に用意されているglyphiconと呼ばれるアイコン集の中のハートアイコンを用いています。また、likeされた数を`micropost.liked_by.count`で取得し、ハートアイコンの隣に表示しています。

```erb:/sample_app/app/views/microposts/_like.html.erb
<%= form_for(current_user.favorite_relationships.build) do |f| %>
  <div><%= hidden_field_tag :micropost_id, micropost.id %></div>
  <%= button_tag(class: "btn btn-default" ) do %>
    <%= content_tag :span, micropost.liked_by.count, class: "glyphicon glyphicon-heart-empty" %>
  <% end %>
<% end %>
```

```erb:/sample_app/app/views/microposts/_unlike.html.erb
<%= form_for(current_user.favorite_relationships.find_by(micropost_id: micropost.id),
             html: { method: :delete }) do |f| %>
  <%= button_tag( class: "btn btn-default") do %>
    <%= content_tag :span, micropost.liked_by.count, class: "glyphicon glyphicon-heart" %>
  <% end %>
<% end %>
```
最後に、ボタンの配置や色を変更するために、`custom.scss`に以下を追記します。

```scss:/sample_app/app/assets/stylesheets/custom.scss
:
:
/* likes */

.glyphicon-heart {
  color: red;
}
.btn-default:hover > .glyphicon-heart-empty {
  color: red;
}
.btn-default {
  position: relative;
  left: 60px;
}
```
ここまでで、マイクロポストにlike/unlikeボタンが表示されました。次は、これを動作させてlike機能を完成させます。


### likeボタンの動作
`rails generate controller FavoriteRelationships`でFavoriteRelationshipsコントローラーを作成し、コントローラー内にcreateアクションとdestroyアクションを記述します。


```ruby:/sample_app/app/controllers/favorite_relationships_controller.rb
class FavoriteRelationshipsController < 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 = FavoriteRelationship.find(params[:id]).micropost
    current_user.unlike(micropost)
    redirect_back(fallback_location: root_url)
  end
end
```
ここまででlike機能が完成しました。次は、これを非同期で動作するように拡張します。


## Ajaxによる非同期化
Ajaxを用いて、like/unlikeボタンをクリックした際に、ページを更新せずにlike数の更新とlike/unlikeボタンの切り替えを行うようにします。
まずは、`like_form`と`unlike_form`に、`remote:true`を追加します。

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

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

次に、FavoriteRelationshipsコントローラでAjaxリクエストに対応します。

```ruby:/sample_app/app/controllers/favorite_relationships_controller.rb
class FavoriteRelationshipsController < 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 = FavoriteRelationship.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
```
最後に、Ajaxリクエストを受信時に呼び出されるJS-ERbファイルを作成します。以下では、likes数の更新と、like/unlikeボタンの切り替えを行なっています。

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

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

以上で完成です。
内容に不備がございましたら、コメントにてお知らせいただければ幸いです。