【Rails】Twitterのいいね機能を非同期で実装する方法【Ajax】

# 目標
![chatspace9.gif](https://qiita-image-store.s3.amazonaws.com/0/178077/b54cbac9-6f1c-376c-92b0-0af522ecbe18.gif)


* RailsでTwitterの「いいね！」機能を再現する
* TweetモデルとUserモデル（deviseを使用）は作成済みとする
* viewはerbではなくhamlで記述しています
* いいねのハートマーク❤︎はboostrapのGlyphiconを使います。

# 手順
* likes_tableの作成
* LikeモデルのAssociationを定義
* routes.rbでルーティングの設定
* likesコントローラーの作成
* 部分テンプレートでviewを作成
* ajaxを使った非同期化

# likes_tableの作成
* ターミナルで`rails g model Like`を実行し、Likeモデルを作成します。
* 下記のようにmigrationファイルを編集し、外部キーをreferencesとforeign_keyを使用して設定します。これにより、user_idカラムとtweet_idカラムを作成できます。
* ターミナルで`rake db:migrate`を実行し、テーブルを作成します。

````ruby:20170522021358_create_likes.rb
class CreateLikes < ActiveRecord::Migration[5.0]
  def change
    create_table :likes do |t|
      t.references :user, foreign_key: true, null: false
      t.references :tweet, foreign_key: true, null: false
      t.timestamps
    end
  end
end
````

# LikeモデルのAssociationを定義
* UserモデルとTweetモデルはLikeモデルに対して１対多の関係にあるので、次のようにAssociationを定義します。
* オプションに`dependent: :destroy`を記述することで、tweetやuserを削除した特に関係するlikeも自動的に削除されるようにします。
* また、likeしているtweetやuserを取得するために、likeを中間テーブルとしたliking_usersとlike_tweetsを設定します。

````ruby:models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :tweet
end
````

````ruby:models/tweet.rb
class Tweet < ApplicationRecord
  belongs_to :user

  #いいね機能のアソシエーション
  has_many :likes, dependent: :destroy
  has_many :liking_users, through: :likes, source: :user
end
````

````ruby:models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  has_many :tweets, dependent: :destroy

  # いいね機能のアソシエーション
  has_many :likes, dependent: :destroy
  has_many :like_tweets, through: :likes, source: :tweet
end

````

# routes.rbでルーティングの設定
* likeアクションをpostメソッド、unlikeアクションをdeleteメソッドを使ってルーティングを記述します。
* どのtweetに対するlikeか判断するため、URI Patternは`/like/:tweet_id`のように記述します。
* オプションで`as:`を記述することでPrefixの名前を指定できます。

````ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users

  resources :tweets do
  end

  resources :users, only: [:show] do
  end

  post   '/like/:tweet_id' => 'likes#like',   as: 'like'
  delete '/like/:tweet_id' => 'likes#unlike', as: 'unlike'

  root 'tweets#index'
end
````

# likesコントローラーの作成
* ターミナルで`rails g controller likes`を実行し、likesコントローラーを作成します。
* Userモデルの呼び出しをcurrent_userで行うことで、自動的にuser_idにcurrent_user.idが指定されます。
* set_variablesで変数を定義していますが、`@id_name`と`@id_heart`は非同期の時に使うのもなので、後ほど説明します。

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

  def like
    like = current_user.likes.new(tweet_id: @tweet.id)
    like.save
  end

  def unlike
    like = current_user.likes.find_by(tweet_id: @tweet.id)
    like.destroy
  end

  private

  def set_variables
    @tweet = Tweet.find(params[:tweet_id])
    @id_name = "#like-link-#{@tweet.id}"
    @id_heart = "#heart-#{@tweet.id}"
  end

end
````

# 部分テンプレートでviewを作成
* 部分テンプレート`_like_links.html.haml`を作成し、いいねボタン部分を記述します。
* `if (いいねしている場合) else (いいねしていない場合)`で表示を変えます。
* `gem 'bootstrap-sass'`を導入すれば、クラスに`.glyphicon`をつけることでアイコンを表示できます。
* `tweet.likes.count`でtweetにlikeされた数を取得できます。
* 以上でいいね機能が動作するようになりました。

* これを非同期で行いたい場合、`link_to`のオプションに`remote: true`を入れることで、リンクを押した時にajaxが発火するようになります。
* ajaxの動作の際に各tweetを識別する必要があるため、`tweet.id`を各divのidに記述します。


````haml:views/tweets/index.html.haml
/@tweetsをrenderのcollectionでそれぞれを取り出してtweetとして渡す
= render partial: 'tweets/tweets', collection: @tweets, as: :tweet
````

````haml:views/tweets/_tweets.html.haml
/さらにいいねボタン部分はrenderで部分テンプレートを呼び出す
= render 'likes/like_links', tweet: tweet
````

````haml:views/likes/_like_links.html.haml
%div.like-link{id: "like-link-#{tweet.id}"}
  - if current_user.likes.find_by(tweet_id: tweet.id)
    = link_to unlike_path(tweet.id), method: :delete, remote: true do
      .btn.btn-default.glyphicon.glyphicon-heart{id: "heart-#{tweet.id}"}
        = tweet.likes.count.to_s
  - else
    = link_to like_path(tweet.id), method: :post, remote: true do
      .btn.btn-default.glyphicon.glyphicon-heart-empty{id: "heart-#{tweet.id}"}
        = tweet.likes.count.to_s
````

# ajaxを使った非同期化
* likeアクション作動後に実行される`like.js.erb`と、unlikeアクション作動後に実行される`unlike.js.erb`を作成します。
* コントローラーで`@id_name = "#like-link-#{@tweet.id}"`を定義し、押したリンクの`tweet.id`の入ったid属性を生成します。
* 同様に、コントローラーで`@id_heart = "#heart-#{@tweet.id}"`で押したリンクの`tweet.id`の入ったid属性を生成します。
* htmlメソッドを使って`@id_name`の部分を新しく書き換えます。
* jsファイル内でrenderメソッドを用いる場合は`escape_javascript`メソッドでrender部分を囲みます。 
* `reomveClass`や`addClass`で`gluphicalicon`クラスを切り替えます。

````js:views/likes/like.js.erb
$("<%= @id_name %>").html('<%= escape_javascript(render("likes/like_links", tweet: @tweet  )) %>');
$("<%= @id_heart %>").removeClass('glyphicon-heart-empty');
$("<%= @id_heart %>").addClass('glyphicon-heart');
````

````js:views/likes/unlike.js.erb
$("<%= @id_name %>").html('<%= escape_javascript(render("likes/like_links", tweet: @tweet  )) %>');
$("<%= @id_heart %>").addClass('glyphicon-heart-empty');
$("<%= @id_heart %>").removeClass('glyphicon-heart');
````

##### 以上で完成です。
内容に不備等ありましたら、お手数ですがコメントにてお願いします。

# 追記
こちらの記事の方が簡単に作れるかもしれません。

* railsとjsを使ったお手軽「いいね♡機能」
http://qiita.com/YuitoSato/items/94913d6a349a530b2ea2