ブックマークボタンのajax化
解説
ajaxとは?
- 非同期通信と呼ばれる通信方法(画面が白くなった後に画面が切り替わる通信は同期通信)
- JavaScriptでサーバー側との通信を「非同期」で行い
- 非同期通信の場合、サーバーの処理中でも他の作業を行える
- link_toにremote: trueを指定すると、レンダリング処理がhtmlではなくjsファイルで実行されるようになる
実装
ブックマークコントローラを修正
app/controllers/bookmarks_controller.rb class BookmarksController < ApplicationController def create @board = Board.find(params[:board_id]) current_user.bookmark(@board) end def destroy @board = current_user.bookmarks.find(params[:id]).board current_user.unbookmark(@board) end end
ブックマークボタンをajax化
app/views/boards/_bookmark.html.erb <%= link_to bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}", class:"float-right", method: :post, # remote: trueを指定してajax化 remote: true do %> <%= icon 'far', 'star' %> <% end %>
app/views/boards/_unbookmark.html.erb <%= link_to bookmark_path(current_user.bookmarks.find_by(board_id: board.id)), id: "js-bookmark-button-for-board-#{board.id}", class:"float-right", method: :delete, # remote: trueを指定してajax化 remote: true do %> <%= icon 'fas', 'star' %> <% end %>
ブックマークボタンの切り替え処理を追加
app/views/bookmarks/create.js.erb $("#js-bookmark-button-for-board-<%= @board.id %>").replaceWith("<%= j(render('boards/unbookmark', board: @board)) %>");
app/views/bookmarks/destroy.js.erb $("#js-bookmark-button-for-board-<%= @board.id %>").replaceWith("<%= j(render('boards/bookmark', board: @board)) %>");
ブックマーク機能
Bookmarkモデルを生成して制約を追加する
$ rails g model Bookmark user:references board:references`
一意性制約をつける
class CreateBookmarks < ActiveRecord::Migration[5.2] def change create_table :bookmarks do |t| t.references :user, foreign_key: true t.references :board, foreign_key: true t.timestamps # 下記を追加して同じ掲示板に何度もお気に入りするのを防ぐ t.index [:user_id, :board_id], unique: true end end end
その後、$ rails db:migrate
モデルにも同じ内容を追加する
bookmark.rb class Bookmark < ApplicationRecord belongs_to :user belongs_to :board validates :user_id, uniqueness: { scope: :board_id } end
モデルにアソシエーションを追加
user.rb has_many :boards, dependent: :destroy has_many :comments, dependent: :destroy # has_many :bookmarks, dependent: :destroy # has_many :bookmark_boards, through: :bookmarks, source: :board
ルーティングの追加
resources :users, only: %i[new create] resources :boards do resources :comments, only: %i[create update destroy], shallow: true collection do get :bookmarks end end resources :bookmarks, only: %i[create destroy]
bookmarks_controllerの生成
$ rails g controller bookmarks create destroy
bookmark処理と判定する処理をモデルに追加
- コントローラーを圧迫したくないのでモデルに記入
user.rb # <<で引数で渡した掲示板レコードが、中間テーブルに自動的に保存される def bookmark(board) bookmark_boards << board end def unbookmark(board) bookmark_boards.destroy(board) end # bookmarkしているか判定 def bookmark?(board) bookmark_boards.include?(board) end
中間テーブル 中間テーブルは多対多の関係を表現するためのテーブル 中間テーブルを用いた処理(Rails) - Qiita
include?メソッド 引数の掲示板レコードが含まれていたらtrue,そうでなければfalseを返す
コントローラー追記
bookmarks_controller class BookmarksController < ApplicationController def create board = Board.find(params[:board_id]) current_user.bookmark(board) # redirect_backで直前のページに戻す redirect_back fallback_location: root_path, success: t('defaults.message.bookmark') end def destroy board = current_user.bookmarks.find(params[:id]).board current_user.unbookmark(board) redirect_back fallback_location: root_path, success: t('defaults.message.unbookmark') end end
お気に入りした掲示板の一覧表示するためのアクション追加
- includesでN+1 問題対策する Rails で includes して N+1 問題対策 - Qiita
boards_controller.rb def bookmarks @bookmark_boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc) end
viewの設定
お気に入りボタンを用意
views/board/_board.html.erb <% if current_user.own?(board) %> <%= render 'crud_menus', board: board %> <% else %> <%= render 'bookmark_button', board:board %> <% end %>
- <% if current_user.own?(board) %>は以前作成した判定のメソッドを使用している
def own?(object) id == object.user_id end
bookmark_buttonを作成
view/boards/_bookmark_button.html.erb <% if current_user.bookmark?(board) %> <%= render 'unbookmark', { board: board } %> <% else %> <%= render 'bookmark', { board: board } %> <% end %>
- こちらでは以前作成した判定メソッドを使用
def bookmark?(board) bookmark_boards.include?(board) end
お気に入り解除のボタン
view/boards/_unbookmark.html.erb <%= link_to bookmark_path(current_user.bookmarks.find_by(board_id: board.id)), id: "js-bookmark-button-for-board-#{board.id}", class: 'float-right', method: :delete do %> <%= icon 'fas', 'star' %> <% end %>
お気に入りするボタン
<%= link_to bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}",class: 'float-right', method: :post do %> <%= icon 'far', 'star' %> <% end %>
お気に入り一覧画面の作成
views/boards/bookmarks <省略> <!-- 掲示板一覧 --> <% if @bookmark_boards.present? %> <%= render @bookmark_boards %> <% else %> <p><%= t('bookmarks.bookmarks.no_result') %></p> <% end %>
掲示板画像アップロード機能を作る
アップローダーの作成
$ bundle exec rails g uploader BoardImage
デフォルトの画像ファイルとアップロード可能なファイルの種類を指定する
class BoardImageUploader < CarrierWave::Uploader::Base # carrierwaveを通じた画像のアップロード先をどこにするのかを指定して、指定されたディレクトリに、アップロードされたファイルが保存されていく def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # 画像が投稿されていない場合でもデフォルト画像を表示させる(uploaders/board_image_uploder.rb内の画像をデフォルトにする) def default_url 'board_placeholder.png' end # アップロード可能なファイル種別を指定 def extension_whitelist %w(jpg jpeg gif png) end end
アップロード先のフォルダを、.gitignoreに指定
/public/uploads
ローカルでアップした画像ファイルをリモートにアップロードしないようにgitignoreを追記
Boardテーブルに画像のカラムを追加する
Boardにboard_imageを追加
$ bundle exec rails g migration AddBoardImageToBoards board_image:string $ bundle exec rails db:migrate
Boardモデルに、アップローダーの仕様を宣言
class Board < ApplicationRecord mount_uploader :board_image, BoardImageUploader #追記 belongs_to :user validates :title, presence: true, length: { maximum: 255 } validates :body, presence: true, length: { maximum: 65_535 } end
ControllerとViewに、画像ファイルのフィールドを追加する
コントローラで、画像ファイルの入力を受け付ける
app/controllers/boards_controller.rb # reqireでデータのオブジェクト名を指定して、permitで保存処理のできるキーを追加する def board_params params.require(:board).permit(:title, :body, :board_image, :board_image_cache) end
掲示板のフォームに、画像ファイルの入力フィールドを追加
app/views/boards/_form.html.erb <%= form_with model: board, local: true do |f| %> <%= render 'shared/error_messages', object: f.object %> <div class="form-group"> <%= f.label :title %> <%= f.text_field :title, class: 'form-control' %> </div> <div class="form-group"> <%= f.label :body %> <%= f.text_area :body, class: 'form-control', rows: 10 %> </div> <div class="form-group"> <%= f.label :board_image %> <%= f.file_field :board_image, class: 'form-control mb-3', accept: 'image/*' %> <%= f.hidden_field :board_image_cache %> </div> <div class='mt-3 mb-3'> <%= image_tag board.board_image.url, id: 'preview', size: '300x200' %> </div> <%= f.submit class: 'btn btn-primary' %> <% end %>
掲示板の部分テンプレートに、アップロードした画像のURLを指定
app/views/boards/_board.html.erb <div class="col-sm-12 col-lg-4 mb-3"> <div id="board-id-<%= board.id %>"> <div class="card"> <%= image_tag board.board_image_url, class: 'card-img-top', size: '300x200' %> <div class="card-body"> <h4 class="card-title"> <a href="#"> <%= board.title %> </a> </h4> <div class='mr10 float-right'> <a href="#"><%= icon 'fas', 'trash', class: 'pr-1' %></a> <a href="#"><%= icon 'fa', 'pen' %></a> </div> <ul class="list-inline"> <li class="list-inline-item"> <%= icon 'far', 'user' %> <%= board.user.decorate.full_name %> </li> <li class="list-inline-item"> <%= icon 'far', 'calendar' %> <%= l board.created_at, format: :long %> </li> </ul> <p class="card-text"><%= board.body %></p> </div> </div> </div> </div>
メッセージを追加
config/locales/activerecord/ja.yml ja: activerecord: attributes: board: title: 'タイトル' body: '本文' board_image: 'サムネイル'
config/locales/carrierwave/ja.yml ja: errors: messages: carrierwave_processing_error: '処理できませんでした' carrierwave_integrity_error: 'は許可されていないファイルタイプです' carrierwave_download_error: 'はダウンロードできません' extension_whitelist_error: "は %{allowed_types}の形式でアップロードしてください" extension_blacklist_error: "%{extension}ファイルのアップロードは許可されていません。アップロードできないファイルタイプ: %{prohibited_types}" content_type_whitelist_error: "%{content_type}ファイルのアップロードは許可されていません。アップロードできるファイルタイプ: %{allowed_types}" content_type_blacklist_error: "%{content_type}ファイルのアップロードは許可されていません" rmagick_processing_error: "rmagickがファイルを処理できませんでした。画像を確認してください。エラーメッセージ: %{e}" mini_magick_processing_error: "MiniMagickがファイルを処理できませんでした。画像を確認してください。エラーメッセージ: %{e}" min_size_error: "を%{min_size}以上のサイズにしてください" max_size_error: "を%{max_size}以下のサイズにしてください"
マイグレーションファイル
ルーティングの仕組み
ルーティングとは
- 受け取ったリクエスト(URLとHTTPメソッドの組み合わせ)をもとに、コントローラー&アクションを案内すること
基本となるアクション
アクション名 | 意味 | HTTPメソッド | URL |
---|---|---|---|
index | 一覧表示 | GET | /tasks |
show | 詳細表示 | GET | /tasks/:id |
new | 新規登録 | GET | /tasks/new |
create | 登録 | POST | /tasks |
edit | 編集画面 | GET | /tasks/:id/edit |
update | 更新 | PATCH,PUT | /tasks/:id |
destroy | 削除 | DELETE | /tasks/:id |
ルーティングの記述
- 1つづつ記入するか、resourcesオプションを使用するかの2つの方法がある
# 1つづつ記入する # 左側にはHTTPメソッドとURL、右側にはコントローラー名#アクション名を記述 get '/blog' => 'blogs#index' # resourcesを使用 resources :tasks
新規登録機能の作成
一覧画面に新規登録のリンクを作成する
h1 タスク一覧 # btnとbtn-primaryの2つのCSSクラスを与えることで、bootstrapがリンクをボタンにような見た目にする = link_to '新規登録', new_task_path, class: 'btn btn-primary'
- new_task_pathは、リンク先のURL
翻訳情報を追加する
- バラバラに記述しないでいいように一箇所にまとめる
has_many: "%{record}が存在しているので削除できません" # ここから追加 models: task: タスク attributes: task: id: ID name: 名称 description: 詳しい説明 created_at: 登録日時 updated_at: 更新日時
新規登録のためのアクションを実装
def new # この1行を追加 @task = Task.new end
- アクションからビューに渡したいデータをインスタンス変数にいれる
新規登録画面のビューを実装する
- ジェネレータにより、作成されたファイルを編集していく
- form_withはHTMLのform要素を作成するメソッド
- ブロック引数fに対して、text_fieldなどのメソッドを呼び出すことでフィールドを出力
h1 タスクの新規登録 .nav.justify-content-end = link_to '一覧', tasks_path, class: 'nav-link' = form_with model: task, local: true do |f| .form-group # 入力欄の名前を表示 = f.label :name # 入力欄を出力 = f.text_field :name, class: 'form-control', id: 'task_name' .form-group # 説明欄の名前を表示 = f.label :description # 説明欄を出力 = f.text_area :description, rows: 5, class: 'form-control', id: 'task_description' # submitヘルパーを呼び出して,submitボタンを表すHTMLが表示される = f.submit nil, class: 'btn btn-primary'
登録アクションの実装
def create @task = Task.new(task_params) @task.save redirect_to @task, notice: "タスク 「#{@task.name}」を登録しました。" end
- task_paramsには、フォームから送られてきた情報が想定通り{task: {...}}の形であることを確認して、{...}の中から、名称と詳しい説明の情報を抜き取る役割がある
レンダーとリダイレクト
- レンダー・・・アクションに続きビューを表示させること(renderメソッド)
- リダイレクト・・・アクション後にビューを表示せず、別のURLに案内すること(redirect)
Flashメッセージ
.container # ここから追加 - if flash.notice.present? .alert.alert-success = flash.notice # ここまで = yield
# :flashというキーの値にハッシュとして渡せば、どんなキー(:notice,:alertなど)のデータも渡すことができる redirect_to tasks_url, flash: {warning: "何かの警告メッセージ"} # 直後にレンダーするビューに対してデータを伝えることができる flash.now[:alert] = "〇〇" flash.now.alert = "〇〇"
コントローラとビュー
コントローラを作成する
# bin/rails g controller コントローラ名 [アクション名 アクション名] $ bin/rails g controller tasks index show new edit
- ルーティング・・・URLとHTTPメソッドの組み合わせから、リクエストを処理すべきコントローラーとアクションを特定すること
- リクエストを処理するコントローラとアクションは、ブラウザからのリクエストに含まれるURLとHTTPメソッドによって決定する
- コントローラのアクションを設定するときは、入口となるURLとHTTPメソッドをあわせて考える必要がある
- controllerのジェネレーターでアクションを指定するとアクションと同名のビューも作成される
HTTPメソッドがGETのアクションは同名のビューを使うことが多いので、HTTPメソッドがGETになるアクション名をジェネレーターコマンドの引数として指定すると便利
ジェネレーターコマンドを実行するとアクションについて個別にルーティングの設定が追加される
- ルーティングを一括で設定したいので、config/routes.rbから設定を削除する
# 全部消す get 'tasks/index' get 'tasks/show' get 'tasks/new' get 'tasks/edit' # ここまで # 追加 # index,show,new,edit,create,update,destroyすべてのアクションに関するルーティングを設定してくれる resources :tasks # Railsのデフォルト画面ではなくタスク一覧が表示されるようにする root to 'tasks#index'