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

前回に続き、ajax
今回はコメント投稿、削除機能をajax化していきます!
(コメント編集機能も実装しました)

手順

1 . コメントコントローラを修正
2 . コメント作成、削除処理をajax
3 . コメント作成、削除時の動的レンダリング処理を追加

1 . コメントコントローラを修正

Image from Gyazo

・上記createアクションに記載していた下記を削除し、コメント削除処理を追加。

if @comment.save
    redirect_to board_path(@comment.board_id), success: t('defaults.message.created', item: Comment.model_name.human)
  else
    redirect_to board_path(@comment.board_id), danger: t('defaults.message.not_created', item: Comment.model_name.human)
  end

・定義したdestroyアクションをルーティングに追加します。

Image from Gyazo

2 . コメント作成、削除処理をajax

form_withlocal: trueを削除して非同期処理にします。
(form_withはデフォルトでremote: trueの設定になっているため記述しなくても良い)

Image from Gyazo

・コメントの削除ボタンlink_toremote: trueを追加します。

Image from Gyazo

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

・コメントの追加に成功した場合、追加したコメントをコメント一覧に追加する処理をjavascriptで実装します。
・また、コメントの追加に失敗した場合は、エラーメッセージを表示するようにします。

・app/views/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 %>

・コメントの追加と同様に、コメントの削除した時にコメントリストから対象のコメントを取り除きます。

・app/views/comments/destroy.js.erb

$("tr#comment-<%= @comment.id %>").remove();
✅総括

ajax化を実装する流れは頭に入ってきましたが、
手順3のコメント作成、削除時の動的レンダリング処理の実装での、
Javascriptの記法に前回に引き続き慣れないので、Javascriptの記法さえ頭に入れられればスイスイコードが書けそう。
Railsでは今後もJavascriptも使うことがたくさんありそうなので、ajax化する時にまた復習しながら進めていきたいです!

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

Ajaxとは

Ajaxとは、Webブラウザ上で非同期通信を行い、ページ全体の再読み込み無しにページを更新する方法のことです。

同期通信

同期通信では、クライアントはwebページ全体の情報(HTMLとそれに紐づくcss,js,imageなどのアセット)をサーバーから受け取って、ページを一から作り直します。
例えばページの一部を変更するだけなのに、他の部分も組み立て直すってことはその分ページの表示に時間がかかっちゃいます。(サーバー側の処理を待つことになる)

しかも、このリクエスト〜レスポンスの処理を行っている間は、他の処理を行わずにサーバーからレスポンスが返ってくるのを待ち続ける必要があります(よくあるのが画面が真っ白になって何もできない状態)。

そこでAjaxのような非同期通信を使用すれば、ページ遷移無しに、高速で更新処理を行い、尚且つ、リクエスト〜レスポンスの処理を行っている間も他の処理が行えます。

非同期通信の方法は2種類

この便利なAjaxによる非同期通信を行う方法としては、
remote:true形式
ajax関数を使った形式

今回はremote: true形式を使ってajax化していきます。

手順

1 . ブックマークコントローラを修正
2 . ブックマークボタンをajax
3 . ブックマークボタンの切り替え処理を追加

1 . ブックマークコントローラを修正

redirect先の指定コメントアウト

Image from Gyazo

2 . ブックマークボタンをajax

ブックマークボタンのコードを記載している
app/views/boards/_bookmark.html.erb

app/views/boards/_unbookmark.html.erb


<%= link_to 略 %>
remote: trueを追加。

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

3 . ブックマークボタンの切り替え処理を追加

上記の
id: "js-bookmark-button-for-board-#{board.id}"
を使って、下記viewファイルを作成。

# 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)) %>");

replaceWithは要素を置換するメソッド。

$(置換対象).replaceWith(置換後の要素)
パーシャルを読み込んで、置換している

j()はescape_javascriptエイリアス

🟢総括

今回の課題を通して、Ajaxとは何かが分かりました。
自分が日頃、身近で使っているアプリでもよく搭載されているので、
次の課題を通してAjaxについてもっと理解を深めていきたいと思いました。
また、Javascriptの記法に対しても理解を深めていきたいです。

⭐️ブックマーク機能の追加

モデルのアソシエーション

Image from Gyazo

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

validates :user_id, uniqueness: { scope: :board_id }
この記載によって、
「1ユーザー」が「1つの掲示板」に対して「1ブックマーク」という範囲を限定した一意チェックができる

Image from Gyazo

・app/model/user.rb

has_many :bookmark_boards, through: :bookmarks, source: :board

ユーザーモデルは複数のブックマークされた掲示板(bookmark_boards)を持っている。
throughオプションでbookmarkテーブルを経由してuserモデルとアソシエーションを作成。
bookmark_boardsは仮のテーブル名なので、sourceオプションで参照するテーブルを指定している

has many :throughオプションで関連付けすると何がいいのか
ユーザーがお気に入りした掲示板を直接取得することができるようになる!
ユーザーがお気に入りした掲示板の一覧を取得するときに使う。

app/controller/boards_controller
Image from Gyazo

ルーティングの設定
Image from Gyazo

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

ほぉ、、、collectionオプションって便利🤔✨

コントローラー、もろもろの設定の流れ
・bookmarks_controllerを生成する
・bookmarkする処理はモデルに定義する
・自分が作った掲示板かを判定するメソッドをモデルに定義する(定義済)
・bookmarkしてるかを判定するメソッドをモデルに定義する
・bookmarks_controllerのcreateアクション、destroyアクションを定義する
・bookmarksアクションを定義する

ブックマークの処理

ブックマークの処理はモデルに記載するらしい!
理由はコントローラーに記載すると可読性が落ちるため 上記、補足※ブックマークのメソッドはrailsの7つのアクションと違う名前で定義するから
Image from Gyazo

🟢総括
今回はブックマーク(お気に入り機能)の実装でしたが、ブックマーク機能はモデルにメソッドの定義をしたり、今までのUser、Board、Commentモデルに加えての実装だったので、各モデルとのアソシエーションやルーティング関係が特に難しかったです。
これから課題をこなすにあたって、includeだったり、pathなどのヘルパーメソッドについても、これから深く勉強していかないとダメだなと感じました。

⭐️中間テスト アウトプット

git clone

git clone クローンしたいurlでclone。
cloneしたらcdコマンドでclone先のディレクトリに移動して作業開始。

bundle exec rspec specでバグの内容を1つずつ確認しながらバグを修正していく。

ちなみに今回のバグ内容は下記。

rspec ./spec/system/blogs_spec.rb:18 # Blogs blog一覧から詳細ページにアクセスした場合 blogの詳細ページが表示されること  
rspec ./spec/system/blogs_spec.rb:29 # Blogs blog詳細ページでコメントした場合 blogの詳細ページにコメントが表示されること  
rspec ./spec/system/blogs_spec.rb:44 # Blogs blog詳細ページでコメントを削除した場合 blogの詳細ページからコメントが削除されること  
rspec ./spec/system/blogs_spec.rb:62 # Blogs blog詳細ページで編集画面へのリンクをクリックした場合 blogの編集ページが表示されること  
rspec ./spec/system/blogs_spec.rb:72 # Blogs blog編集ページにアクセスした場合 blogの編集用フォームが表示されること  
rspec ./spec/system/blogs_spec.rb:80 # Blogs blog編集ページにアクセスした場合 blogの編集用フォームに編集前のblog情報が表示されること  
rspec ./spec/system/blogs_spec.rb:96 # Blogs blog新規作成ページにアクセスした場合 blogの新規作成ページが正しく表示されていること  
rspec ./spec/system/blogs_spec.rb:103 # Blogs blog新規作成ページにアクセスした場合 blogの新規作成ができること  
rspec ./spec/system/blogs_spec.rb:112 # Blogs blog新規作成ページにアクセスした場合 blogの新規作成でcontentも正しく作成できること  
rspec ./spec/system/blogs_spec.rb:124 # Blogs blog一覧ページでblogを削除しようとした場合 blogが一覧ページから削除されること

今回はサクサク進められたので、自分の手が止まってしまったバグだけ復習していきます。

rspec ./spec/system/blogs_spec.rb:112 # Blogs blog新規作成ページにアクセスした場合 blogの新規作成でcontentも正しく作成できること

上記エラーでは、まず
1 . blogを作成した時にサーバーのログでparameterにどこまで送った情報が入っているかを確認しました。
結果、
titleはparameterに入っていて、
contentだけ入っていない状態だったので、
blogを作成して保存する動作である
BlogsControllercreateアクションを確認。

Image from Gyazo

2 . 上記で@blogがどこまで持ってこられているかをbinding.pryを使って検証。
すると、contentの値がnilだったので、

3 . @blog = Blog.new(blog_params)(blog_params)の内容を確認。
permitの引数に:contentがなかったので追加。

def blog_params
    params.require(:blog).permit(:title, :content)
end

ただ、contentにどんな文字を入力しても0になって出力されるので、この0はどこから来てるんだ??と疑問🤔
もしかしたらデータベースが怪しい?と感じたので、db/schema.rbを確認。
Image from Gyazo

すると、blogsテーブルのcontentのデータ型が、integerになっている!
なので、マイグレーションファイルを新規作成してcontentのデータ型を変えることに。
$ rails g migration ChangeDatatypeカラム名Ofテーブル名

$ rails g migration ChangeDatatypeContentOfBlogs
カラム名、テーブル名の頭文字は大文字

・生成されたファイルを編集

change_column :テーブル名, :カラム名, :データ型
Image from Gyazo

最後に、rails db:migrateした後しっかりrails db:migrate:statusしてマイグレーションファイルがしっかりupされているか確認して、contentが文字列を表示してくれたことで解決!✨

🟢中間テスト総括
解説動画なしでほぼほぼ進められたので今後もこの調子で頑張る!
エラーログと
binding.pryを使って、
どこまで変数を持ってこられているかを調査することで
問題の切り分けをし、 バグを1つずつ解決していけるんだな、
と今回の課題で再認識出来ました。
Railsチュートリアルの時は、エラーが起きた時にあれこれコードを書いて自分が書いたコードがどれか分からなくなり、沼にハマってしまっていたので、今後はそうならないように問題の切り分けをしっかりと行いながら開発を進めていきたいです!💪

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

・Userモデルに記載していた

def own?(object)
  id == object.user_id
end

を利用して、自分が作成した掲示板のみ、編集、削除ボタンを表示させるように実装。

・app/views/boards/show
Image from Gyazo

・app/views/boards/_board.html.erb
Image from Gyazo

※重要

boards_controller.rb

# Bad
  @board = Board.find(params[:id])
  ※ この記載では、URL入力時のIDを変えると自分以外が作成した掲示板を対象にできてしまう。
    このアプリは他の人の掲示板を見られる仕様のため、showアクションではBoardから取得させても問題ない。
    しかし、editやupdate, destroyアクションでは、他人の作成した掲示板を変更できないように実装。

# Good
  @board = current_user.boards.find(params[:id])
  ※ current_user.boardsによって、対象のユーザーに関連する掲示板の集合が取得できる。
def destroy
    @board.destroy!

!を使っている理由は、削除処理は「必ず成功するもの」だから。
save save! は、処理が失敗したときの挙動が違います。 前者はfalse を返し、後者は例外を返します。

例えば、掲示板作成でタイトルを入力漏れし、falseが帰ってきたら、エラー表示箇所を訂正してまた作成を試みます。掲示板作成は「失敗する可能性がある」処理です。

一方、削除の処理は失敗する余地がない処理です。この処理が失敗したときは、意図的に処理を止めてデバッグが必要になります。

🟢総括

今回の課題で、

・モデルにメソッドを定義することで条件変更時にモデルメソッド1箇所の変更で済む保守性が良くな流ことが分かった。

・破壊的メソッド!の使い方をよく理解できた!🔥

⭐️三項演算子について補足

・解答例ではapplication_helper.rbにて、

page_title.empty? ? base_title : page_title + " | " + base_title

上記のように条件式(.empty?) ? trueの処理 : falseの処理と言う記載を使って1行で済ませている。

⭐️タイトルを動的に出力する

・動的に出力とは?

「状況に合わせて選択できたりする柔軟性」

🟢実装方法

1 . app/views/layouts/application.html.erbのheadタグ内にyield文を記載。

<title><%= page_title(yield(:title)) %></title>

2 . app/helpers/application_helper.rbでpege_titleメソッドを定義。

Image from Gyazo

page_titleの引数にはデフォルト値として空文字を指定。 これにより引数が渡されなかったとしても空文字(””)がpage_titleに代入され、メソッド内の演算で使用される。

そして、三項演算子によって、引数が渡されていなければ「RUNTEQ BORAD APP(base_title)」、引数が渡されていれば「引数の文字(page_title) |RUNTEQ BORAD APP」を返す。

三項演算子によって、if文の

if page_title.empty?
  base_title
else
  page_title + " | " + base_title
end

page_title.empty? ? base_title : page_title + " | " + base_title

このように一行で記載できる。

3 . あとはそれぞれのビューで、

<% content_for(:title, '渡したい文字列') %>
上記を記載するだけ! これにより、'渡したい文字列'が共通レイアウトのページにあるyeildに渡され、page_titleメソッドの引数が'渡したい文字列'となり、成果物のようにタイトルが「「Access | AAA 電機」(ページ内容|渡したい文字列 )」などとなる。

☀️動的であるメリット

タイトルを変更する際、タイトル新しく変更しなければなりません。

"ヘルパーメソッドのbase_titleに入れる文字を変える"だけで、content_forが使われる他の全ページのタイトルを変えられる。

app/helpers/application_helper.rb

module ApplicationHelper

  def page_title(page_title = '')
    # 変更箇所 -----------
    base_title = 'BBB 電機'
    # --------------------

    page_title.empty? ? base_title : page_title + " | " + base_title
  end
end