resourceを使ったプロフィール編集機能の実装

ルーティングの設定

今回、プロフィール詳細画面と編集画面へのurlは/profile, /profile/editと 現在ログインしているユーザーの詳細と編集画面だけ表示できればいいので、urlでユーザーのidを参照する必要がない。このような時に使うのが単数型のリソース routes.rbに

#routes.rb
resource :profile, only: %i[show edit update]

を記載、bundle exec rails routesでルーティングを確認すると

ターミナル
edit_profile GET    /profile/edit(.:format)                                                                  profiles#edit
                  profile GET    /profile(.:format)                                                                       profiles#show
                          PATCH  /profile(.:format)                                                                       profiles#update
                          PUT    /profile(.:format)                                                                       profiles#update

とidを使わないルーティングを生成できるし、urlヘルパーも使えるようになる ちなみに複数リソース(resources)と単数リソース(resource)もルーティング先のコントローラはデフォルトでリソース名の複数形名称である

コントローラー

bundle exec rails g controller profilesを実行してprofilesコントローラーの作成 コードは以下のようになる

#profiles_controller.rb
class ProfilesController < ApplicationController
   before_action :set_user, only: %i[edit update]
   
   def edit; end
 
   def update
     if @user.update(user_params)
       redirect_to profile_path, success: t('defaults.message.updated', item: User.model_name.human)
     else
       flash.now['danger'] = t('defaults.message.not_updated', item: User.model_name.human)
       render :edit
     end
   end
 
   def show; end
 
   private
 
   def set_user
     @user = User.find(current_user.id)
   end
 
   def user_params
     params.require(:user).permit(:email, :last_name, :first_name, :avatar)
   end
 end

set_user@user = current_userとしてしまうとプロフィール名変更に失敗したときに、画面上で名前が変わってしまう。これは@userにはcurrent_userの保存されている場所の値(メモリ番地)が代入されており、これによりcurrent_userの値を参照しているので、@userをupdateしてしまうと@userを通してcurrent_userの値が影響を受けてしまうためである。なので、@userにはdbから取得したオブジェクトを利用している

アバターカラムの追加

今回は詳細ページにユーザーにアバター画像を追加するのでアバター画像のカラムを用意する カラム名avatarにするのでbundle exec rails g uploader Avatarを実行してアバター画像の設定ファイルを生成したらbundle exec rails g migration add_avatar_to_users avatar:stringを実行して、bundle exec rails db:migrateを実行してavatarカラムを追加 user.rbにmount_uploader :avatar, AvatarUploaderを記載して設定ファイルを適用させる 設定ファイルは以下の感じ

#avatar_uploader.rb
class AvatarUploader < CarrierWave::Uploader::Base
  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  # include CarrierWave::MiniMagick

  # Choose what kind of storage to use for this uploader:
  storage :file
  # storage :fog

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Provide a default URL as a default if there hasn't been a file uploaded:
  def default_url
    'sample.jpg'
  end

  # Process files as they are uploaded:
  # process scale: [200, 300]
  #
  # def scale(width, height)
  #   # do something
  # end

  # Create different versions of your uploaded files:
  # version :thumb do
  #   process resize_to_fit: [50, 50]
  # end

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  def extension_whitelist
    %w[jpg jpeg gif png]
  end

  # Override the filename of the uploaded files:
  # Avoid using model.id or version_name here, see uploader/store.rb for details.
  # def filename
  #   "something.jpg" if original_filename
  # end
end

ビュー

ユーザー詳細ページと編集ページのビューは以下のように記載

#show.html.erb
<% content_for :title do %>
  プロフィール
<% end %>

<div class="container">
  <div class="row">
      <div class="col-md-6 offset-md-1 col-lg-6 offset-lg-2">
        <h1><%= t '.title' %></h1>
      </div>
      <div class="col-md-4 offset-md-1 col-lg-2 offset-lg-1">
        <%= button_to t('.button'), {controller: 'profiles', action: 'edit' },  method: :get, class: "btn btn-success float-right" %>
      </div>
      <div class="col-md-11 offset-md-1 col-lg-9 offset-lg-2">
        <table class="table">
          <tr>
            <td>
              <strong><%= User.human_attribute_name(:email) %></strong>
            </td>
            <td><%= current_user.email %></td>
          </tr>
          <tr>
            <td>
              <strong><%= t('.name') %></strong>
            </td>
            <td><%= current_user.decorate.full_name %></td>
          </tr>
          <tr>
            <td>
              <strong><%= User.human_attribute_name(:avatar) %></strong>
            </td>
            <td><%= image_tag current_user.avatar.url,class: 'rounded-circle mr15',width: '40',height: '40' %></td>
          </tr>
        </table>
      </div>
  </div>
</div>

#edit.html.erb
<% content_for :title do %>
  プロフィール編集
<% end %>

<div class="container">
  <div class="row">
    <div class=" col-md-10 offset-md-1 col-lg-8 offset-lg-2">
      <h1><%= t '.title' %></h1>
      <%= form_with model: current_user, url: profile_path, method: :patch, local: true do |form| %>
        <%= render 'shared/error_messages', object: form.object %>
        <div class="form-group">
          <%= form.label :email, User.human_attribute_name(:email) %>
          <%= form.text_field :email, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= form.label :last_name, User.human_attribute_name(:last_name) %>
          <%= form.text_field :last_name,class: "form-control" %>
        </div>
        <div class="form-group">
          <%= form.label :first_name, User.human_attribute_name(:first_name) %>
          <%= form.text_field :first_name,class: "form-control" %>
        </div>
        <div class="form-group">
          <%= form.label :avatar, User.human_attribute_name(:avatar) %>
          <%= form.file_field :avatar,class: "form-control" %>
        </div>
        <div class="actions">
          <%= form.submit t('defaults.update'),class: "btn btn-primary" %>
        </div>
      <% end %>
    </div>
  </div>
</div>

レイアウトは

CSS · Bootstrap

Bootstrapの使い方 導入方法と基本・レスポンシブデザインを徹底解説 - WEBST8のブログ

あたりを参考に作成

参考文献

Rails のルーティング - Railsガイド

掲示板の検索機能の実装

ransackとは?

簡単に検索フォームを作成するためのgem

インストール

Gemfileに

gem 'ransack'

を記載して、bundle install

使い方

使い方はシンプルモードとアドバンスモードの2つがある
今回は、シンプルモードを使って、検索時、掲示板のタイトルまたは本文、ブックマークされた掲示板一覧ページで検索した場合はブックマークした掲示板の中から検索条件に一致したものを表示させるようにする

コントローラ

掲示板一覧(boards_controller.rbのindexアクション)とブックマーク一覧(boards_controller.rbのbookmarkアクション)を以下のように記述

#boards_controller.rb
def index
    @search = Board.ransack(params[:q])
    @boards = @search.result(distinct: true).includes(%i[user bookmarks]).order(created_at: :desc).page(params[:page])
end

def bookmarks
    @search = current_user.bookmark_boards.ransack(params[:q])
    @boards = @search.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
end

params[:q]に検索パラメーターが渡されて、モデル名.ransack(params[:q])と記載することで、@searchと検索オブジェクトが生成される。これに@boards = @search.result(distinct: true)~~と記載することで検索結果を得られるようになる
distinct: trueで取得するデータの重複を取り除いている。これは掲示板と紐づいたコメントなどを検索する場合、同一の掲示板で複数のコメントの検索に引っかかっても、同じ掲示板が複数表示されないようにしてくれる。今回は掲示板だけのカラム検索なので特に必要ない。

ビュー

掲示板一覧(boards/index.html.erb)とブックマーク一覧(boards/bookmarks.html.erb)にそれぞれ以下を記載

#boards/index.html.erb
<%= search_form_for @search do |f| %>
  <div class="input-group mb-3">
    <%= f.search_field :title_or_body_cont, class:"form-control", placeholder: t('defaults.search_word') %>
     <div class="input-group-append">
       <%= f.submit "検索", class: "btn btn-primary" %>
     </div>
   </div>
<% end %>

#boards/bookmarks.html.erb
<%= search_form_for @search ,url: bookmarks_boards_path do |f| %>
  <div class="input-group mb-3">
    <%= f.search_field :title_or_body_cont, class:"form-control", placeholder: t('defaults.search_word') %>
     <div class="input-group-append">
       <%= f.submit "検索", class: "btn btn-primary" %>
     </div>
   </div>
<% end %>

search_form_forform_forの検索フォームバージョンみたいなもの f.search_field :title_or_body_cont, ~掲示板のタイトル(title)と本文(body)カラムに対して、最後の_contでLIKE句を利用した部分一致検索ができるようにしている
複数の属性を検索条件に指定する場合はattribute_name_orattribute_name・・・と記載する この_cont部分を述語(Predicate)と言い、他にも条件と一致したものを検索する_eq
~以上を満たすものを検索する_gteqとかなど様々な検索条件を指定できる述語がある
掲示板一覧とブックマーク一覧の検索フォームビューの違いはurlである それぞれ、検索条件をindexアクションbookmarksアクションに送りたいのでurlでルーティングヘルパーを使って指定している

参考文献

GitHub - activerecord-hackery/ransack: Object-based searching.

Ransackで簡単に検索フォームを作る73のレシピ - 猫Rails

掲示板のページネーション

kaminariのインストール

今回はページネーションをkaminari gemを使って実装する Gemfileに gem 'kaminari' を追記してbundle install —path vendor/bundle
今回の場合掲示板一覧画面とブックマーク一覧に1ページあたり20件の掲示板を表示するようにする

bundle exec rails g kaminari:config

を実行すると config/initializers/kaminari_config.rbとページネーションのデフォルトの設定ファイルを生成する
boardモデルに1ページあたり20件の掲示板を取得するようにboard.rbに paginates_per 20を記載 または、先ほど生成されたconfig/initializers/kamakiri_config.rbに

Kaminari.configure do |config|
  config.default_per_page = 20
end

と記載、設定の記載なので後者の書き方の方が良い?

コントローラの修正

boardsコントローラに

def index
  @boards = Board.all.includes(%i[user bookmarks]).order(created_at: :desc).page(params[:page])
end
def bookmarks
    @boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc).page(params[:page])
end

と.page params[:page]を記載 ページネーションのビューはbootstrap4のレイアウトを適用するために

bundle exec rails g kaminari:views bootstrap4

を実行することでapp/views/kaminari/以下にページネーションの指定したレイアウトを適用したビューファイルを生成してくれる

ページネーションの表示

あとは掲示板一覧のビューとブックマーク一覧に

#boards/index.html.erb
<div class="row">
    <div class="col-12">
      <div class="row">
      <% if @boards.blank? %>
        <p><%= t('.no_board') %></p>
      <% else %>
        <%= render @boards %>
      <% end %>
      </div>
    </div>
  </div>
  <%= paginate @boards %>
</div>

#bookmarks.html.erb
<div class="row">
    <div class="col-12">
      <div class="row">
      <% if @boards.blank? %>
        <p><%= t('.no_bookmark_board') %></p>
      <% else %>
        <%= render @boards %>
      <% end %>
      </div>
    </div>
  </div>
  <%= paginate @boards %>
</div>

と<%= paginate @boards %>を記載してサーバーを起動してページネーション機能が実装されているか確認する
rails cで@boards.page(1).limit_valueで1ページあたりの取得件数が20になっているか確認

参考文献

GitHub - kaminari/kaminari: ⚡ A Scope & Engine based, clean, powerful, customizable and sophisticated paginator for Ruby webapps

【Rails】kaminariの使い方をマスターしよう! | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

bootstrap4が適用されたkaminariのviewを作成したい - その辺にいるWebエンジニアの備忘録

コメント投稿、削除機能のajax化

コメント作成、削除処理のajax

・コメントの投稿フォームを処理をajax化するので、form_withをremote: trueにして、サーバーのログでjs形式で送信されているか確認 ちなみに、form_withはデフォルトでremote: trueなので明示的に記載しなくても良い

#comments/_form.html.erb
<div class="row mb-3">
  <div class="col-lg-8 offset-lg-2">
    <%= form_with model: [board, comment], remote: true,id: 'new_comment' do |form| %>
      <%= form.label :body %>
      <%= form.text_area :body, class: "form-control mb-3", row: "4", placeholder: Comment.human_attribute_name(:body) %>
      <%= form.submit t('defaults.post'), class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

コメントの削除のリンクのlink_toにremote: trueを追加、リンク先をcomment_path(comment)と指定

#comments/_comment.html.erb
<tr id="comment-<%= comment.id %>">

                                      中略

  <% if current_user.own?(comment) %>
    <td class="action">
      <ul class="list-inline justify-content-center" style="float: right;">
 
                                      中略
    
        <li class="list-inline-item">
          <%= link_to comment_path(comment), method: :delete, remote: true, class: 'js-delete-comment-button', data: {confirm: 'よろしいですか'} do %>
            <%= icon 'fas', 'trash' %>
          <% end %>
        </li>
      </ul>
    </td>
  <% end %>
</tr>

comments_controller.rbの修正

#comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_comment, only: %i[update destroy]
  def create
    @comment = current_user.comments.create(comment_params)
  end

  def destroy
    @comment.destroy!
  end

  private

  def set_comment
    @comment = current_user.comments.find(params[:id])
  end

redirect_backの処理を削除して、set_commentで削除するコメントのインスタンスを取得して置くことでアクションを簡潔にしている

コメント作成、削除時の動的レンダリング処理を追加

#comments/create.js.erb
$("#error-messages").remove();
<% if @comment.errors.present? %>
  $("#new_comment").prepend("<%= j(render('shared/error_messages', object: @comment)) %>");
<% else %>
  $("#js-table-comment").prepend("<%= j(render('comments/comment', comment: @comment)) %>");
  $("#js-new-comment-body").val('');
<% end %>

#comments/destroy.js.erb
$("#comment-<%= @comment.id %>").remove();

create.js.erbではもしコメントインスタンスがバリデーションに引っかかり、エラーオブジェクトを持っている場合、prependメソッドで指定した要素の子要素の先頭にshared/error_messagesとパーシャルファイルを追加している。エラーオブジェクトがなければ、同じくprependメソッドを使って、コメントのパーシャル部分だけをレンダリングするようにしている。また投稿成功時、コメントフォームvalue属性をvalで空文字を指定してリセットしている。
destroy.js.erbではremoveメソッドで指定した要素を全て削除している

参考文献

Rails で JavaScript を使用する - Railsガイド

【Rails】Ajax(非同期通信)でコメント投稿、削除 - Qiita

jQuery 要素を追加する(before/after/prepend/append) | ITSakura

val(val) - jQuery 日本語リファレンス

ブックマークボタンのajax化

ajaxとは

非同期通信と呼ばれる通信方法、通常、クライアントがリクエストを出したらサーバーからのレスポンスが返ってくるまで他の作業をすることができない同期通信と呼ばれる方法が用いられているが、クライアントからサーバーに対して、一部の情報だけを返すようにリクエストを送ることで、それ以外の部分は変わらないので他の作業ができる

ブックマークボタンをクリック時にajax通信を行うには

#_bookmark.html.erb
<%= 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 %>

#_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 %>

と現在のブックマークボタンの状態でクリックすると

サーバーログ
Processing by BookmarksController#create as HTML

のように通常のブックマーク機能だとHTML形式でリクエストが送信される ここで、ブックマークのリンクヘルパーにremote: trueを指定することでJS形式でリクエストを送信できる

#_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 do %>
   <%= icon 'far', 'star' %>
<% end %>

#_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 do %>
   <%= icon 'fas', 'star' %>
<% end %>

これによりブックマークボタンをクリックすると

サーバーログ
Processing by BookmarksController#create as JS

となり、処理の流れは、bookmarks_controllerのcreate,destroyアクション の通過後はcreate.js.erb,destroy.js.erbファイルに向かうようになる

bookmarks_controller.rbの修正

修正前

#bookmarks_controller.rb
class BookmarksController < ApplicationController
  def create
    @board = Board.find(params[:board_id])
    current_user.bookmark(@board)
    redirect_back fallback_location: root_path, success: t('.success')
  end

  def destroy
    @board = current_user.bookmarks.find(params[:id]).board
    current_user.unbookmark(@board)
    redirect_back fallback_location: root_path, success: t('.success')
  end
end

修正後

#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

とredirect_backの記述を削除

ブックマーク切り替えの処理のJSファイルを作成

HTML形式のリクエストで送信していた時はcreateアクションやdestroyアクションでビューの作成はしていないのでリダイレクト処理でどのページに遷移するか指定していたが、今はremote: trueでJS形式のリクエストで送信しているのでアクション通過後に部分的に更新させるためのJSファイルを作成する

#views/bookmarks/create.js.erb
$("#js-bookmark-button-for-board-<%= @board.id %>").replaceWith("<%= j(render('boards/unbookmark', board: @board)) %>");

#views/bookmarks/destroy.js.erb
$("#js-bookmark-button-for-board-<%= @board.id %>").replaceWith("<%= j(render('boards/bookmark', board: @board)) %>");

それぞれ、replaceWithでboards/unbookmark、boards/bookmarkをrenderして置き換えているが、javascripthは''や""があるとダメなのでエスケープする必要があるのでj(render~とexcape_javascript(renderのaliasを使っている、replaceWithと似たメソッドとしてhtmlメソッドがあるが、htmlにすると、指定している要素(今回はlink_toで生成されるaタグ)に設定したした値をセットする、今回それぞれ、指定している要素はbookamarkファイルとunbookmarkファイル内に含まれていて、それぞれ設定した値はbookmarkファイルとunbookmarkファイルをセットするので、link_toで生成されたaタグにネストされた形になってしまう。htmlは指定した要素に引数で渡した値をセットするのに、replaceWithは指定した要素に引数で渡した値を置き換えるといった違いがある

参考文献

Rails で JavaScript を使用する - Railsガイド

初心者目線でAjaxの説明 - Qiita

【Rails】ブックマーク機能のajax化 - Qiita

replaceWith(content) - jQuery 日本語リファレンス

escape_javascriptメソッドって何ぞや。 - Qiita

ブックマーク機能の追加

アソシエーション

ユーザーは複数の掲示板をブックマークでき、掲示板は複数のユーザーにブックマークされるというようにモデル間で多対多の関係がある時、中間テーブル(今回の場合はブックマーク)を作成してモデル間の情報をやり取りする このテーブルによって、今回ならどのユーザーがどの掲示板と関連づいているのかという情報を管理することで、ユーザーが掲示板をブックマークといった機能を実装できる UserモデルとBoardモデルとBookmarkモデルの関係は以下のようになる User : Bookmark = 1 : 多 Board : Bookmark = 1 : 多

bundle exec rails g model Bookmark user:references board:references

を実行、bookmarkのモデルファイルとマイグレーションファイルを作成

#bookmark.rb
class Bookmark < ApplicationRecord
   belongs_to :user
   belongs_to :board
 
   validates :user_id, uniqueness: { scope: :board_id }
 end

uniqunessヘルパーのscopeオプションにboard_idを指定することで、各掲示板(board_id)別にユーザー(user_id)との関係性を一意にすることができる

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

      #user_idとboard_idカラムにuniqueインデックスを作成して、dbレベルで一意性を保つ
      t.index [:user_id, :board_id], unique: true
      t.timestamps
    end
  end
end

これにより一人のユーザーが同じ投稿を複数回bookmarkできないようにしている

bundle exec rails db:migrate

を実行して中間テーブルは作成完了

#user.rb
class User < ApplicationRecord
  #dependent: :destroyでユーザーが削除された時、そのユーザーに関連する情報を削除するようにしている
   has_many :bookmarks, dependent: :destroy
  #throughオプションでbookmarkテーブルを経由してuserモデルとアソシエーションを作成して、bookmark_boardsは仮のテーブル名なので、sourceオプションで参照するテーブルを指定している
   has_many :bookmark_boards, through: :bookmarks, source: :board
end

ブックマークの処理

ブックマークの処理はモデルに記載すること 理由はコントローラーに記載すると可読性が落ちるため

#user.rb
class User < ApplicationRecord
  #<<演算子でbookmarks.create!(board_id: board.id)と同様の処理を行う
  def bookmark(board)
    bookmark_boards << board
  end

  def unbookmark(board)
    bookmark_boards.destroy(board)
  end
  #ログインしているユーザーに紐づいたブックマークのレコードに引数で渡された掲示板モデルと紐づいたレコードが存在するか。存在すればユーザーはその掲示板をブックマークしていると判定、存在しなければブックマークされていないと判定できる
  def bookmark?(board)
    bookmark_boards.include?(board)
  end
end

ルーティングの設定

今回ブックマーク一覧ページをboards/bookmarksにルーティングを設定したい urlから何を表示しているか推測しやすくするため また、bookmarksコントローラのcreateとdestroyアクションだけルーティングを設定したいので

#routes.rb
resources :boards do
  collection do
    get :bookmarks
  end
resources :bookmarks, only; %i[create destroy]

とcollectionオプションを使うことでboardsリソースの中にbookmarksアクションを追加できる、この時bookmarks_boards_urlやbookmarks_boards_pathといったルーティングヘルパーも使えるようになる。

Bookmarkコントローラ作成

bundle exec rails g controller bookmarks create destroy

を実行してbookmarks_controller.rbを作成して、以下を記述

#bookmarks_controller.rb
class BookmarksController < ApplicationController
  #ブックマークボタンをクリックされた掲示板を取得して、Userモデルのインスタンスメソッドとして作成したbookmarkメソッドの引数に渡してブックマークする
  def create
    @board = Board.find(params[:board_id])
    current_user.bookmark(@board)
    redirect_back fallback_location: root_path, success: t('.success')
  end
  #ログインしているユーザーに関連づいてクリックしたbookmarksモデルに関連づいたboardモデルを取得してunbookmarkメソッドの引数に渡してブックマーク解除をしている
  def destroy
    @board = current_user.bookmarks.find(params[:id]).board
    current_user.unbookmark(@board)
    redirect_back fallback_location: root_path, success: t('.success')
  end
end

redirect_backを使うとユーザーを直前のページに戻すことができる。この時fallback_locationは必ず設定する必要がある。HTTP_REFERER(リンク元のURLが入っているためどこのURLからアクセスしてきたかを知ることができる)がブラウザが必ずしも設定しているとは限らないため

boards_controllerでbookmarksアクションの実装

#boards_controller.rb
class BoardsController < ApplicationController
    def bookmarks
      @bookmark_boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc)
    end
end

ビュー実装

#boards/_board.html.erb
                 中略
<% if current_user.own?(board) %> #ログインしているユーザーが掲示板作成者であるならcrud_menusのパーシャルを表示する、そうでなければ、bookmark_buttonのパーシャルを表示する
  <%= render 'crud_menus', board: board %>
<% else %>
   <%= render 'bookmark_button', board: board  %>
 <% end %>

#boards/_bookmark.html.erb
user.rbにて作成したbookmark?メソッドによってログインしているユーザーが掲示板をブックマークしているか判定、ブックマークしていたら、unbookmarkパーシャルを表示、そうでなければbookmarkパーシャルを表示
<% if current_user.bookmark?(board) %>
   <%= render 'unbookmark', { board: board } %>
<% else %>
   <%= render 'bookmark', { board: board } %>
<% end %>

#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 do %>
   <%= icon 'far', 'star' %>
 <% end %>

#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 %>

#boards/bookmarks.html.erb
                        中略
#ブックマークされた掲示板があればbookmark_boardパーシャルを表示、なければ、ブックマーク中の掲示板がありませんと表示
<% if @bookmark_boards.present? %>
  <%= render @bookmark_boards %>
<% else %>
   <p><%= t('.no_result') %></p>
 <% end %>

参考文献

Active Record の関連付け - Railsガイド

https://railsguides.jp/layouts_and_rendering.html#redirect-to%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B

https://railsguides.jp/routing.html#%E3%82%B3%E3%83%AC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B

https://qiita.com/niwa1903/items/8a0374155cd18adbd7cf

uniqueness: scope を使ったユニーク制約方法の解説 - Qiita

Railsでお気に入り機能の実装方法をまとめてみる(初心者向け) - Qiita

掲示板の編集、削除機能の実装

掲示板編集ページの作成

フォーム部分はパーシャルを用意して、新規作成ページと共有できるようにしておく ので、今回編集ページのビューは以下のようになる

#boards/edit.html.erb
<div class="container">
  <div class="row">
    <div class="col-lg-8 offset-lg-2">
      <h1><%= t('.title') %></h1>
      <%= render 'form', { board: @board } %> #掲示板フォームのパーシャル
    </div>
  </div>
</div>

現在ログインしているユーザーのidと掲示板のuser_idが同じ場合編集、削除アイコンを表示させる。まず、この判定を行うメソッドをuserモデルのインスタンスメソッドとして記述しておく

#user.rb
def own?(object)
    id == object.user_id
end

ビューに直接ifなどを使って判定してもいいが、userモデルに判定のロジックを記載することで、ロジックを見直したりしたいときなどにuserモデルを見るだけで済む。また引数にobjectを受け取るようにすることで、掲示板だけでなく、コメントなどユーザーに他の関連づいたインスタンスの判定も行えるようにしている。 ビューはこのメソッドを使うことで以下のようになる

<% if current_user.own?(board) %>
      <%= link_to edit_board_path(board), id: 'button-edit-#{board.id}' do %>
         <%= icon 'fa', 'pen' %>
      <% end %>
      <%= link_to '#', id: 'button-delete-#{board.id}', method: :delete, data: {confirm: ''} do %>
         <%= icon 'fas', 'trash' %>
      <% end %>
  <% end %>

edit,update,destroyアクションの作成 先にコードを記載すると以下のようになる

  before_action :set_board, only: %i[edit update destroy]
  def edit; end

  def update
    if @board.update(board_params)
      redirect_to board_path(@board), success: t('.success')
    else
      flash.now[:danger] = t('.fail')
      render :edit
    end
  end

  def destroy
    @board.destroy!
    redirect_to boards_path, success: t('.success')
  end

  private

  def set_board
    @board = current_user.boards.find(params[:id])
  end

今回は,edit,update,destroyアクションにおいて、ルーティングのboardオブジェクトのidから現在のユーザーに関連付いたBoardオブジェクトを取得してプロセスが共通なので、コールバック(オブジェクトに対して、特定のアクションを実行した瞬間に呼び出されるメソッドのこと)としてprivateに登録してある。
privateに登録するのは記載したクラスの外から呼び出されて、悪用されるのを防ぐため
before_action :set_board, only: %i[edit update destroy]の記述によりedit,update,destroyアクションの実行前にset_boardアクションが実行される

確認ダイアログの表示

削除アイコンは

<%= link_to board_path(board), id: 'button-delete-#{board.id}', method: :delete, data: {confirm: ‘よろしいですか?’} do %>

と記述method: :deleteによってHTTPにDELETEリクエストを発行してくれるこの時 HTTPはDELETEリクエストを受け取ることができないので,Rails側はJavascriptを使って偽造してくれる。つまりjavascriptがオフになっていると削除のリンクも無効になってしまうということ。また、リンクやフォームにdata-confirm属性を追加することで、確認ダイアログを表示してくれる。confirm: に表示したい文字列を追加する
また、destroyアクションでdestroyメソッドではなくdestroy!メソッドを使っているのはデータを削除するとき失敗する想定(バリデーションなど)に引っかかるなどを今回の仕様では想定していないため、万が一削除の処理に失敗した場合には処理を中断させて例外として扱う必要があるためである。

参考文献

Active Record の基礎 - Railsガイド

Active Record コールバック - Railsガイド

Rails で JavaScript を使用する - Railsガイド