【Ruby on Rails】非同期通信でブックマーク機能の実装方法（お気に入り、いいね）


#対象者

* ブックマーク機能を実装したい方
* お気に入り、いいね機能を実装したい方

#目的

* ブックマークして記事の保存や後で見るための機能を実装をする

#実際の手順と実例
###1.前提

* 投稿機能実装済
* device機能実装済
* ArticleとUserモデルがある


###2.モデルの作成

ターミナルでモデルを作成します。

```ターミナル
$ rails g model Bookmark user:references article:references
```

実行後はマイグレーションファイルを編集します。

```db/migrate/000000000000_create_bookmarks.rb
class CreateBookmarks < ActiveRecord::Migration[5.2]
  def change
    create_table :bookmarks do |t|
      t.references :user, foreign_key: true, null: false
      t.references :article, foreign_key: true, null: false #①

      t.timestamps
    end
  end
end
```

①null:falseを設定したのはデータベースへの空の保存を防ぐためになります。

続いてブックマークモデルのアソシエーションとバリデーションを設定していきます。

```app/models/bookmark.rb 

class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :article

  validates :user_id, uniqueness: { scope: :article_id } #①
end
````
①二度連続（重複）して登録することを防いでいます。（ロード中等）


続いて各モデルにアソシエーション設定をしていきます

````app/models/article.rb
class Article < ApplicationRecord
  belongs_to :user
  has_many :bookmarks, dependent: :destroy

  def bookmarked_by?(user)
    bookmarks.where(user_id: user).exists? #①
  end
````
①既にブックマークしていないか検証しています。


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


###3.コントローラーの作成

````ターミナル
$ rails g controller bookmarks
````

````app/controllers/public/bookmarks_controller.rb 
class Public::BookmarksController < ApplicationController
  before_action :authenticate_user!

  def create
    @article = Article.find(params[:article_id])
    @bookmark = @article.bookmarks.new(user_id: current_user.id)　#①
    if @bookmark.save
    else
      redirect_to request.referer #②
    end
  end

  def destroy
    @article = Article.find(params[:article_id])
    @bookmark = @article.bookmarks.find_by(user_id: current_user.id)
    if @bookmark.present? #③
      @bookmark.destroy
    else
      redirect_to request.referer
    end
  end
end
````
①まずarticle_idを取得し、その後にuser_idにcurrent_userを紐付けて@bookmarkに渡しています。

②非同期処理を行うため投稿が成功した場合はリロードせずに返します。投稿失敗した場合に現在いるページにリダイレクトされるようになっています。

③既にブックマーク済かどうか判断しています。ブックマーク済なら、削除が実行されます。


ルーティングは以下の通りです。
articleにネストさせています。

```config/routes.rb
  resources :articles, except: [:index] do
    resource :bookmarks, only: [:create, :destroy]
  end
```

###4.Viewの作成・修正

ブックマークボタンを部分テンプレート化してどこでも使えるようにします。下記は記事の詳細ページに設置しています。

````app/views/articles/show.html.erb
  <div id="bookmark_button_<%= @article.id %>"#①
    <%= render "bookmarks/bookmark", article: @article %>　#②
  </div>
````

①bookmark_buttonをキーにして非同期処理を呼び出しています。
②部分テンプレート内の変数はご自身のコントローラーで設定した値を入れてください。ここではarticles_controller.rbに＠article = Article.find(params[:id])を定義してます。


以下部分テンプレートの設定です。

`````app/views/bookmarks/_bookmark.html.erb
<% if article.bookmarked_by?(current_user) %>
  <%= link_to "", article_bookmarks_path(article), method: :delete, remote: true, class: "fas fa-bookmark" %></td>
<% else %>
  <%= link_to "", article_bookmarks_path(article), method: :post, remote: true, class: "far fa-bookmark" %></td>
<% end %>
````

* remote:trueでjavascript形式のリクエストを送信しています。
* fas fa-bookmarkでブックマークボタンを表しています（fontawesome）

https://fontawesome.com/v5.15/icons/bookmark?style=regular


###5.jsファイル作成

```app/views/bookmarks/create.js.erb
$('#bookmark_button_<%= @article.id %>').html("<%= j(render "bookmarks/bookmark", article: @article) %>");
```

```app/views/bookmarks/destroy.js.erb
$('#bookmark_button_<%= @article.id %>').html("<%= j(render "bookmarks/bookmark", article: @article) %>");
```

⚠シングルクオーテーションかダブルクオーテーションかでエラーになるので注意してください！


これで実装完了です！


##参照
[【Rails×Ajax】いいね機能ハンズオン](https://qiita.com/naberina/items/c6b5c8d7756cb882fb20)


####投稿者コメント

ブックマークでもいいねでもやることは変わらないので、次回実装するときはサクサク実装できそうです。

####My Profile
プログラミング学習歴３ヶ月目のアカウントです！
プログラミングスクールで学んだ内容や自分が躓いた箇所等のアウトプットの為に発信しています。
また、プログラミング初学者の方にわかりやすく、簡潔にまとめて情報共有できればと考えています。
もし、投稿した記事の中に誤り等ございましたら、コメント欄でご教授いただけると幸いです。