モデルの作成
まずは rails g model Bookmark でBookmarkモデルを作成。
rails g model Bookmark
rails db:migrate をするその前に、マイグレーションファイルを編集しましょう!
class CreateBookmarks < ActiveRecord::Migration[6.1]
def change
create_table :bookmarks do |t|
t.references :user, foreign_key: true, null: false
t.references :post, foreign_key: true, null: false
t.timestamps
end
end
end
user=Bookmarkしたユーザー
post=Bookmarkされた投稿
マイグレーションファイルを編集し終わったらrails db:migrateを忘れずに!
rails db:migrate
アソシエーション
UserモデルのidやPostモデルのidと関連付けをしていきましょう!
☆Bookmarkモデル
class Bookmark < ApplicationRecord
belongs_to :post
belongs_to :user
end
☆Postモデルにも関連付けが必要
class Post < ApplicationRecord
belongs_to :user
has_many :bookmarks, dependent: :destroy①
def bookmarked_by?(user)②
bookmarks.where(user_id: user).exists?
end
end
①dependent: :destroyは親(ここでいう投稿=post)が削除されると、関連する子(bookmark)も全部消しますよーという記述。
②bookmarked_by?メソッドってなに??
このメソッドで、引数で渡されたユーザーidがBookmarksテーブル内に存在(exists?)するかどうかを調べます。
存在していればtrue、存在していなければfalseを返すようにしています。その結果、
存在しない=「まだBookmarkしていない→createへ」
存在する=「すでにBookmark済み→destroyへ」の流れとなります。
☆最後にUserモデルにも関連付け追加
class User < ApplicationRecord
:
has_many :posts, dependent: :destroy
has_many :bookmarks, dependent: :destroy
end
これで各モデルへのアソシエーションの記述は終わりです!
ルーティング
resources :posts do
resource :bookmarks, only: [:create, :destroy]
end
さて次はルーティングに行きましょう。
「どの投稿をBookmarkしたのか」判別できないといけないため、posts コントローラーにネストします。
ここで「あれ、resourcesじゃなくてなぜBookmarkはresourceなの?」と思いませんでした?
resource、つまり単数形にすると「/:id」がURLにつかなくなります。
Bookmark機能は、1投稿に対して1ユーザーが1回しかBookmarkできませんよね?
つまりBookmark対象の投稿(post)idとユーザーidとがわかっていれば、
BookmarkのidはURLに含める必要がない、つまりparams[:id]を使わなくても良いため
resourceの単数形でURLに/:idを含めないようになっています。
つまりresourceは「それ自身のidが分からなくても、関連する他のモデルのidから特定できる」
という場合に使うことが多いと認識しておきましょう。
結果出来上がったルーティングはこうなります。
post_bookmarks DELETE /posts/:post_id/bookmarks(.:format) bookmarks#destroy
POST /posts/:post_id/bookmarks(.:format) bookmarks#create
コントローラー
次にBookmarkのコントローラーを作成します。複数形で作成するのを忘れずに。
rails g controller bookmarks
コントローラーを作成したらアクションを定義しましょう。
class Public::BookmarksController < ApplicationController
before_action :authenticate_user!
def create
@post = Post.find(params[:post_id])①
bookmark = current_user.bookmarks.new(post_id: @post.id)②
bookmark.save
end
def destroy
@post = Post.find(params[:post_id])①
bookmark = current_user.bookmarks.find_by(post_id: @post.id)
bookmark.destroy
end
end
Bookmrakは専用のviewは必要ないため、アクションはcreateとdestroyのみでよいです。
①でBookmarkする投稿のidを(params[:post_id])で取得します。
②post_idにはルーティングに記載してある「:post_id」を指定し、
current_user(ログイン中のユーザー)がBookmarkした投稿がわかるようにしています。
Viewの作成
<% if @post.bookmarked_by?(current_user) %>①
<%= link_to post_bookmarks_path(@post), method: :delete, remote: true do %> ②
★ <%= @post.bookmarks.count %> Bookmark解除
<% end %>
<% else %>
<%= link_to post_bookmarks_path(@post), method: :post, remote: true do %>
☆ <%= @post.bookmarks.count %> Bookmark
<% end %>
<% end %>
さて、いよいよ大詰めです。
①のbookmarked_by?メソッドは、先ほどPostモデルで説明したメソッドです。
引数で(current_user)を渡すことで、ユーザーが存在していればtrue(destroyアクション)、
存在していなければfalse(createアクション)を返します。
②非同期通信にしたいので、remote: trueを入れることでjsファイルを探しに行ってくれます。
最後に非同期の実装
まずはjsファイルの作成が必要です。
bookmarksのviewの下にcreate.js.erbとdestroy.js.erbを作成しましょう。
$('#bookmarks_buttons_<%= @post.id %>').html("<%= j(render "bookmarks/bookmarks", post: @post) %>");
$('#bookmarks_buttons_<%= @post.id %>').html("<%= j(render "bookmarks/bookmarks", post: @post) %>");
注意点
コードを書き終えたものの、最初は非同期にならないどころか、Bookmarkボタンを押しても反応しませんでした。
ちゃんと合っていると思うけどなー、なにがダメなんだろう。。としばらく考えきづいたことが!!
def create
@post = Post.find(params[:post_id])①
bookmark = current_user.bookmarks.new(post_id: @post.id)②
if bookmark.save
redirect_to request.referer
else
redirect_to request.referer
end
end
最初はこんな風にコードを書いていたんですね。
このif以下の条件分岐を消して、redirect_toを削除、
bookmark.saveだけにすると非同期で反応しました!
これでBookmark機能が完成しました。