[Rails,JavaScript]非同期処理・コメント機能
同期処理・非同期処理とは
同期処理
順番に処理を行う。(処理完了まで待つ必要がある)
一つの処理が終わるまで次に進まない。
同期処理 = 行列に並んでレジを1つずつ通る
非同期処理
他が完了するのを待たず、前の処理が完了する前に次の処理を行う。
並行して処理されるため、非同期の方がすべての処理を完了するのが速い。
(しかし、操作が複雑になりがち)
非同期処理 = セルフレジでそれぞれ同時に処理
たとえば:
- 「いいね」ボタンを押して数だけ変わる(ページはそのまま)
- コメントを投稿してリストが更新される(リロードなし)
このように、ユーザー体験を向上させるための通信方式。
非同期通信とAjaxの違いは?
非同期通信を行う技術(=手段)や仕組みのひとつ。技術の名前。
復習・Ajaxってなんだっけ?
Ajax = Asynchronous JavaScript and XML
コメント機能を非同期処理にする方法
1. コメント機能を部分テンプレート化する
public/tasks/show.html.erb
<div id="task-comments">
<%= render 'public/task_comments/comments', task: @task, task_comment: TaskComment.new %>
</div>
下記は今回作成したコメント用の部分テンプレート。
public/task_comments/_comments.html.erb
<div class="index-results">
<div class="index-card">
<div class="index-header">
<ul>
<b>コメント件数:<%= @task.comments.count %></b>
<hr size="10">
<p></p>
<!-- コメント表示 -->
<% @task.comments.each do |task_comment| %>
<li class="comment">
<div class="comment-container">
<div class="comment-image">
<%= link_to user_path(task_comment.user) do %>
<% if task_comment.user.image.attached? %>
<%= image_tag task_comment.user.image, alt: "#{task_comment.user.name}のプロフィール画像", class: "profile-image" %>
<% else %>
<%= image_tag "no_image.jpg", alt: "デフォルトプロフィール画像", class: "profile-image" %>
<% end %>
<% end %>
</div>
<div class="comment-content">
<p class="comment-text"><%= task_comment.comment %></p>
<span class="task-meta">
<%= task_comment.user.name %>
<%= task_comment.created_at.in_time_zone('Tokyo').strftime('%m月%d日 %H:%M') %>
<% if task_comment.user == current_user %>
<%= link_to "削除", task_task_comment_path(task_comment.task, task_comment), method: :delete, remote: true, "data-confirm" => "本当に削除しますか?" %>
<% end %>
</span>
</div>
<br>
</div>
</li>
<hr size="10">
<% end %>
<!-- コメント投稿フォーム -->
<%= form_with model: [@task, @task_comment], remote: true, data: { turbo: false }, id: "new_task_comment" do |f| %>
<%= f.text_area :comment, placeholder: "コメントを入力", class: "form-input-comment" %>
<%= button_tag(type: 'submit', class: 'submit-btn') do %>
<i class="fa-solid fa-paper-plane"></i>
<% end %>
<% end %>
</ul>
</div>
</div>
</div>
2. コメント投稿フォームを非同期化
remote: true
でAjaxリクエストにする。
<%= form_with model: [@task, @task_comment], remote: true do |f| %>
<%= f.text_area :comment, placeholder: "コメントを入力" %>
<%= f.submit "送信" %>
<% end %>
remote: true
とは
form_with や link_to に付けると、ページ遷移せずに非同期でリクエストを送るようになる。
(Ajaxリクエストに変換する)
ブラウザがHTMLを再読み込みせず、JavaScriptで裏側で通信するため画面が滑らかに動く。
local: false
で書くことも可能。
<%= form_with model: [@task, @task_comment], local: false do |f| %>
3. コントローラー側で format.js を指定
def create
@task = Task.find(params[:task_id])
@comment = current_user.task_comments.new(task_comment_params)
@comment.task_id = @task.id
@comment.save
respond_to do |format|
format.js # → create.js.erbを探す
end
end
respond_to do |format|
とは
リクエストの形式に応じて適切なレスポンスを返す。
下記は、同じアクションでも、受け取るリクエストの形式に応じて処理を切り替える。
respond_to do |format|
format.html # HTMLリクエストが来たらHTMLを返す
format.js # JS(Ajax)リクエストが来たらcreate.js.erbを探す
format.json { render json: @comment }
end
今回は下記の書き方。
このアクションが .js 形式
(Ajax)で呼ばれた場合、create.js.erb
を探して実行する。
respond_to do |format|
format.js
end
この remote: true
によってフォームが JavaScript形式(.js)で送信される。
<%= form_with model: [@task, @task_comment], remote: true do |f| %>
4. JavaScriptファイルを作成する
app/views/public/task_comments/create.js.erb
を作成する
$("#task-comments").html("<%= j render 'public/task_comments/comments', task: @task, task_comment: TaskComment.new %>");
$("#new_task_comment")[0].reset(); // フォームリセット
$("#new_task_comment")[0].reset();
とは
コメントフォームを投稿後にクリア(リセット)するためのJavaScript。
$("#new_task_comment")
jQueryで idが new_task_comment の要素(フォーム)を取得
[0]
jQueryオブジェクトの 最初のDOM要素(純粋なHTMLのform要素)を取得
.reset()
JavaScriptのネイティブなフォームリセット関数 → 中の入力を全部消す!
HTMLFormElement.reset() メソッドは、フォーム要素の既定値を復元
規定値に復元なので、html構築時の初期値を入れた際の値になる
とコメントいただきました。ありがとうございます🙇
今回は、HTMLで初期値は空なので、空の状態に戻されるが
下記のように初期値が設定されていると、入力欄の中は "はじめまして!" に戻るので使い方に注意。
(.reset()は、空にしてくれるものではない)
<%= form_with model: [@task, @task_comment], remote: true do |f| %>
<%= f.text_area :comment, value: "はじめまして!", placeholder: "コメントを入力" %>
<%= f.submit "送信" %>
<% end %>
この書き方だと、初期値あり。
コメントを変更して送信。
送信ボタンを押したコメントは投稿で来てるが、入力欄には初期値が復活。
DOMとは
DOM(Document Object Model・ドキュメントオブジェクトモデル)とは
HTML・XMLドキュメントなどのマークアップ言語で書かれた文書を
JavaScriptなどのスクリプトからアクセス・操作できるようにするためのAPI。
要は「HTML・CSSを簡単に操作できる」
画像・文字などをHTMLで書かれたものを、オブジェクトとして表現して、アニメーションをつけて表示させたりなどの操作ができる。
JavaScriptだと10行ほど書かなきゃいけないことを、jQueryだと3行とか短めで済む。
https://zenn.dev/eliri/articles/e73d74c4aaba95#dom(document-object-model・ドキュメントオブジェクトモデル)%E3%81%A8%E3%81%AF
task: @task, task_comment: TaskComment.new
はなぜ書くか?
パーシャルで渡す変数を明示的にする必要がある。
パーシャル(部分テンプレート)とは?
Railsの render
でパーシャル(部分テンプレート)を呼び出すときには、
そのパーシャルの中で使う変数を渡す必要がある。
tasks/show.html.erb
下記は、部分テンプレートpublic/task_comments/comments
にタスクとコメントのデータを渡してる。
<div id="task-comments">
<%= render 'public/task_comments/comments', task: @task, task_comment: TaskComment.new %>
</div>
create.js.erb
これを create.js.erb に書いてるのは、非同期でコメントを再表示するため。
<%= render 'public/task_comments/comments', task: @task, task_comment: TaskComment.new %>
今回起きたエラー
1. ActionController::UnknownFormat
format.js
を指定しているのに、HTMLとして送信されている。
解決策
Turboが有効なRails 6.1+ の場合は turbo無効化する必要あり。
<%= form_with model: [@task, @task_comment], remote: true, data: { turbo: false } do |f| %>
Turbo は、Rails 6.1以降に デフォルトで導入されたページ遷移高速化ライブラリ。
Rails 6.1以降のデフォルトで Turbo(旧Turbolinks)が有効になっている。
Rails 6.1 以降で rails new をすると、自動的に Turboがセットアップされるようになっている。
Gemfile に以下があれば Turboが有効な状態。
gem 'turbo-rails'
Turboは .js.erb を実行できないので、Turboをオフにする必要がある。
2. undefined method 'model_name' for nil:NilClass
JSでコメント欄をリレンダリングする際、@task_comment を渡していないのでフォームが生成できない。
解決策
create.js.erb や destroy.js.erb に以下のように TaskComment.new を渡す:
$("#task-comments").html("<%= j render 'public/task_comments/comment', task: @task, task_comment: TaskComment.new %>");
_comment.html.erb
は下記の通り。
このとき task_comment
が 渡されていなければ nil なのでエラーになってしまう。
<%= form_with model: [task, task_comment], remote: true do |f| %>
参考文献
- 【JavaScript】非同期処理を完全に理解する(初心者向け)Asynchronous JavaScript Course For Beginners
- コールバック関数とは何か?【超入門編/JavaScript/プログラミング/jQuery/Ajax】
- 非同期処理とは何か?【超入門編/JavaScript/プログラミング】
「コールバック関数」など、JavaScript関係の用語も調べられてないので、この辺も勉強したい!
JavaScript難しすぎる!!!!
Discussion
規定値に復元なのでhtml構築時の初期値を入れた際の値になることに注意が必要です。
ご指摘ありがとうございます!
簡単にですが追記させていただきました。
HTMLFormElement.reset() メソッドは、値を必ず空にさせるためのものではなく、あくまで既定値を復元……つまり、HTMLで設定させた初期値があればそっちに戻ってしまう…ってことですね。
そうです!そうです!(運用方法によっては空にならない可能性があります。
いつもありがとうございます、励みになります!🙇