🦓

[Rails]コメント投稿、削除機能のajax化11/20

2023/06/30に公開

はじめに

コメント投稿、削除機能のajax化を実装していきます。

環境:

Rails 6.1.7.3
ruby 3.0.0

Ajaxとは

Ajax(Asynchronous JavaScript and XML)は、Webページの一部を非同期に更新するための技術です。Ajaxを使用することで、ページの再読み込みを行わずにデータの送受信や部分的な画面の更新が可能となります。
https://railsguides.jp/v6.1/working_with_javascript_in_rails.html

Railsにおけるajax通信

Railsにおいてajax通信を実装する場合、主に二つの方法があります。

  1. remote: trueを指定する方法
  2. JSファイルに任意のタイミングでajax処理を発火させるように記述を施す方法

remote: true

remote: true は、Ruby on Rails のビューヘルパーメソッドであり、AJAX リクエストを行う際に使用されます。このヘルパーメソッドを使用すると、フォームやリンクなどの要素をクリックしたり送信したりした際に、非同期的なリクエストが行われます。

具体的には、remote: true を使用すると、以下のようなことが実現できます:

  1. ページのリロードなしでデータの送信や取得ができる。
  2. レスポンスを JavaScript で処理して、動的なコンテンツの更新や表示ができる。
<%= form_with url: some_path, remote: true do |form| %>
  <!-- フォームのフィールドやボタンなどのコード -->
<% end %>
<%= link_to 'Click me', some_path, remote: true %>

今までのページ遷移では、ブラウザはページ全体のHTMLをサーバから取得し、続いてそのHTMLが要求するアセットすべてをサーバから取得して、ページを組み立てます。
Ajaxを使うと、ページの一部だけをサーバから非同期で取得し、コントローラーのアクションに対応するJavaScriptファイルで処理され、レスポンスビューを作成されます。

https://qiita.com/hisamura333/items/e3ea6ae549eb09b7efb9

コメント投稿のajax化

コメントした時ににページをリロードせずJSで新規コメントをフォームの直下に表示させます。
コントローラーを編集しアクショのビューをJSにします。
ビューへ変数の値を渡すためにインスタンス変数にします。

app/controllers/comments_controller.rb
class CommentsController < ApplicationController
    def create
        @comment = Current.user.comments.build(comment_params)
        @article = Article.find(params[:article_id])
        @comments = @article.comments.includes(:user).order(created_at: :desc)

      respond_to do |format|
        if @comment.save
          format.js
          format.html { redirect_to article_path(comment.article), flash: { success: t('defaults.message.created', item: Comment.model_name.human) } }
        else
          format.html { redirect_to article_path(comment.article), flash: { danger: t('defaults.message.not_created', item: Comment.model_name.human) } }
          format.js
        end
      end
    end
end

respond_toとは

respond_toは、Railsコントローラー内で使用されるメソッドです。このメソッドは、リクエストの種類や形式に応じて異なるレスポンスを返すために使用されます。
通常、formatブロックと組み合わせて使用されます。

local: falseオプション

rails6ではform_withで同期通信がデフォルト(local: true)になったため、local: falseを指定する必要があります。

app/views/comments/_form.html.erb
<%= form_with model: [article, comment], local: false, id: 'new_comment' do |form| %>

フォームが非同期送信になりました。

コンソールでrequest.formatを実行してフォーマットがjavascriptであることを確認します。

(byebug) request.format
#<Mime::Type:0x00007f887bac99e0 
@synonyms=["application/javascript", 
"application/x-javascript"], 
@symbol=:js, 
@string="text/javascript", 
@hash=-116090781805038615>

 TRANSACTION (0.7ms)  begin transaction
  ↳ app/controllers/comments_controller.rb:9:in `block in create'
  Article Load (1.0ms)  SELECT "articles".* FROM "articles" WHERE "articles"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/comments_controller.rb:9:in `block in create'
  Comment Create (3.1ms)  INSERT INTO "comments" ("body", "article_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["body", "hello?"], ["article_id", 1], ["user_id", 1], ["created_at", "2023-06-30 10:03:04.906103"], ["updated_at", "2023-06-30 10:03:04.906103"]]
  ↳ app/controllers/comments_controller.rb:9:in `block in create'
  TRANSACTION (1.3ms)  commit transaction
  ↳ app/controllers/comments_controller.rb:9:in `block in create'
*** No sourcefile available for <internal:kernel>
(byebug) next
# jsを実行される
  Rendering comments/create.js.erb
  Rendered comments/_comment.html.erb (Duration: 39.6ms | Allocations: 403517)
  Rendered comments/create.js.erb (Duration: 57.9ms | Allocations: 602137)

jsにてビューを作成する

comments#createの処理後、create.js.erbにてコメントの部分テンプレートの再描画を行うように実装していきます。
JSにてhtml要素を取得するためにIDを付けました。

app/views/comments/create.js.erb
(function() {
<%# 既に表示されているエラーメッセージがあった場合は削除する %>
document.querySelector('.error_messages')?.remove();
<% if @comment.errors.any? %>
  <%# エラーがある、処理失敗時にはエラーメッセージのパーシャルを表示させる %>
  let commentErrors = '<%= j(render('shared/error_messages', object: @comment)) %>';
  let newCommentForm = document.getElementById('new_comment');
  newCommentForm.insertAdjacentHTML('afterbegin', commentErrors);
<% else %>
  let commentTemplate = '<%= j(render('comments/comment', article: @article, comment: @comment)) %>';
  let commentContainer = document.getElementById('js-comment-container');
  <%# エラーがない、処理成功時には作成されたコメント内容をHTML要素として追加する %>
  commentContainer.insertAdjacentHTML('afterbegin', commentTemplate);
  <%# コメント入力フォームのテキストは表示する必要がないので、空文字に置き換えて内容をクリアする %>
  let newCommentBody = document.getElementById('js-new-comment-body');
  newCommentBody.value = '';
<% end %>
})();

Image from Gyazo

コメント削除のajax化

コメントを削除した時にJSを発火させコメントビューをページから消します、remote: trueを使用しサーバーの処理から離れます。

routes.rbを編集する

config/routes.rb
  resources :articles do
    resources :comments, only: %i[create destroy]
  end

destroyアクションを追加する

コントローラーにてdestroyアクションを追加します。

app/controllers/comments_controller.rb
class CommentsController < ApplicationController
    def destroy
      @comment = Current.user.comments.find(params[:id])
      @article = Article.find(params[:article_id])
      respond_to do |format|
        @comment.destroy
          format.js
          format.html { redirect_to article_path(@article), flash: { success: t('defaults.message.destroyed', item: Comment.model_name.human) } }
      end
    end
end

削除用パスを追加する

article_comment DELETE /articles/:article_id/comments/:id(.:format)                                                      comments#destroy
app/views/comments/_comment.html.erb
<li class="list-inline-item">
            <%= link_to article_comment_path(@article, comment), 
            class: 'js-delete-comment-button', 
            method: :delete, 
            id: "button-delete-#{comment.id}", 
            data: { confirm: t('defaults.message.delete_confirm') }, 
            remote: true do %>
              <%= icon 'fas', 'trash' %>
            <% end %>
</li>

destroy.js.erbを作成する

app/views/comments/destroy.erb
var commentElement = document.getElementById('comment-<%= @comment.id %>');
if (commentElement) {
commentElement.parentNode.removeChild(commentElement);
}

処理の流れを確認します。

Started DELETE "/articles/1/comments/137" for ::1 at 2023-06-30 19:04:49 +0900
Processing by CommentsController#destroy as JS
  Parameters: {"article_id"=>"1", "id"=>"137"}
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/application_controller.rb:9:in `set_current_user'
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."user_id" = ? AND "comments"."id" = ? LIMIT ?  [["user_id", 1], ["id", 137], ["LIMIT", 1]]
  ↳ app/controllers/comments_controller.rb:19:in `destroy'
  Article Load (0.1ms)  SELECT "articles".* FROM "articles" WHERE "articles"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/comments_controller.rb:20:in `destroy'
  TRANSACTION (0.0ms)  begin transaction
  ↳ app/controllers/comments_controller.rb:22:in `block in destroy'
  Comment Destroy (0.7ms)  DELETE FROM "comments" WHERE "comments"."id" = ?  [["id", 137]]
  ↳ app/controllers/comments_controller.rb:22:in `block in destroy'
  TRANSACTION (0.5ms)  commit transaction
  ↳ app/controllers/comments_controller.rb:22:in `block in destroy'
  # jsを実行される
  Rendering comments/destroy.js.erb
  Rendered comments/destroy.js.erb (Duration: 0.7ms | Allocations: 140)
Completed 200 OK in 21ms (Views: 4.0ms | ActiveRecord: 1.5ms | Allocations: 6599)

終わりに

AJAX難しいかったです。
流れを掴んだらコメント編集機能も挑戦したいと思います。
https://qiita.com/t-yama-3/items/7148d25b17e70ceb48d6
https://www.sejuku.net/blog/28967
https://pikawaka.com/rails/remote-true

Discussion