ブックマークボタンの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

コントローラー追記

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

お気に入りした掲示板の一覧表示するためのアクション追加

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}以下のサイズにしてください"

マイグレーションファイル

使用用途

  • データベースにテーブルの作成やカラムの追加を行う場合に利用する

マイグレーションファイルの実行

$ rails db:migrate

マイグレーションファイルの状態を確認する

$ rails db:migrate
  • テーブルにversionがあればup,なければdownと表示される

編集を行いたい場合

  • rails db:rollbackで最新のマイグレーションファイルのバージョンがテーブルから削除される
  • これを利用して、編集を加えたい場合はまずrails db:rollbackをして、versionをupからdownにしたあと、マイグレーションファイルを修正し、rails db:migrateで読み込みを行う
  • 絶対にupの状態でマイグレーションファイルは削除、編集したりしない

ルーティングの仕組み

ルーティングとは

  • 受け取ったリクエスト(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'

Railsのデフォルト画面ではなくタスク一覧が表示されるようにする