🐈

[Rails,JavaScript]非同期処理・コメント機能

に公開4

同期処理・非同期処理とは

同期処理

順番に処理を行う。(処理完了まで待つ必要がある)
一つの処理が終わるまで次に進まない。

同期処理 = 行列に並んでレジを1つずつ通る

非同期処理

他が完了するのを待たず、前の処理が完了する前に次の処理を行う。
並行して処理されるため、非同期の方がすべての処理を完了するのが速い。
(しかし、操作が複雑になりがち)

非同期処理 = セルフレジでそれぞれ同時に処理

たとえば:

  • 「いいね」ボタンを押して数だけ変わる(ページはそのまま)
  • コメントを投稿してリストが更新される(リロードなし)
    このように、ユーザー体験を向上させるための通信方式

非同期通信とAjaxの違いは?

非同期通信を行う技術(=手段)や仕組みのひとつ。技術の名前。

復習・Ajaxってなんだっけ?

コメント機能を非同期処理にする方法

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: trueAjaxリクエストにする。

<%= 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のネイティブなフォームリセット関数 → 中の入力を全部消す!

https://developer.mozilla.org/ja/docs/Web/API/HTMLFormElement/reset

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をオフにする必要がある。

https://zenn.dev/ganmo3/articles/d234583bdc876f


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

参考文献

https://qiita.com/okamoto_ryo/items/d8d9476490a27ad17f71
https://qiita.com/sho0211/items/28faafcd1840c5948107
https://zenn.dev/airiswim/articles/e55d25a597bf3d
https://zenn.dev/airiswim/articles/2b8a49512614da


「コールバック関数」など、JavaScript関係の用語も調べられてないので、この辺も勉強したい!
JavaScript難しすぎる!!!!

Discussion

junerjuner

コメントフォームを投稿後にクリア(リセット)するためのJavaScript。

規定値に復元なのでhtml構築時の初期値を入れた際の値になることに注意が必要です。

https://developer.mozilla.org/ja/docs/Web/API/HTMLFormElement/reset

EliEli

ご指摘ありがとうございます!
簡単にですが追記させていただきました。
HTMLFormElement.reset() メソッドは、値を必ず空にさせるためのものではなく、あくまで既定値を復元……つまり、HTMLで設定させた初期値があればそっちに戻ってしまう…ってことですね。

junerjuner

そうです!そうです!(運用方法によっては空にならない可能性があります。

EliEli

いつもありがとうございます、励みになります!🙇