LoginSignup
3
2
記事投稿キャンペーン 「Rails強化月間」

[rails7]turbo streamsでコメント投稿機能を実装する

Last updated at Posted at 2023-11-08

はじめに

個人開発でアプリケーション作成をするにあたり、turbo streamを用いた実装をしたことがなかったため、コメント投稿機能に組み込んでみようと思いました。

備忘録も兼ねて、実装内容を残していこうと思います。

開発環境

  • ruby 3.2.0
  • Rails 7.0.7.2
  • MySQL

完成形イメージ

コメント投稿・編集

コメント投稿・編集

コメント削除

コメント削除

私の場合、planモデルに対してcommentを複数投稿できるように実装を進めています。
ER図は以下のようなイメージです。
ER図

また、コメント投稿する動線としては、planの詳細ページ→コメント一覧ページ→コメント投稿という流れを取っています。

1. 同期通信でコメント投稿機能を実装する。

ルーティング・コントローラー・ビューをそれぞれ以下のように実装します。

routes.rb
Rails.application.routes.draw do
  # plansコントローラーにcommentsコントローラーをネストする。
  resources :plans do
    resources :comments, only: [:index, :create, :edit, :destroy]
  end
end
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_plan, only: [:index, :create]
  
  def index
    @comment = Comment.new
  end

  def create
    @comment = Comment.new(comment_params)
    if @comment.save
        redirect_to plan_comments_path
    else
       render :index
    end

  end

  private
  def comment_params
    params.require(:comment).permit(:comment).merge(user_id: current_user.id, plan_id: params[:plan_id])
  end

  def set_plan
    @plan = Plan.find(params[:plan_id])
  end

end

app/views/comments/index.html.erb
<div class="text-center mx-auto py-6 sm:py-8 lg:py-12 w-1/2">
  <h2>コメント一覧</h2>

  <%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
</div>
app/views/comments/_form.html.erb
<div id="new_comment">
  <%= form_with model:[plan, comment],local:true, remote: true do |f|%>
    <%= render "shared/error_messages", resource: f.object %>
    <%= f.text_field :comment %>
    <%= f.submit "投稿" %>
  <% end %>
</div>

2. turbo_streamを用いた実装に変更する。

1.コントローラーの記述を変更。
respond_toを用いて、turbo streamsのリクエクトに対するレスポンスを作成します。

turbo streamsに対するレスポンスはformat.turbo_streamと記述することで設定ができます。

app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_plan, only: [:index, :create]
  before_action :set_comments, only: [:index, :create]

  def index
    @comment = Comment.new
  end

  def create
    @comment = Comment.new(comment_params)

      respond_to do |format|
        if @comment.save
          # 入力フォームのリセットのために変数を再定義
          @comment = Comment.new
          format.html { redirect_to plan_comments_path }
          # turbo streamsに関するレスポンスを指定。
          format.turbo_stream
        else
          format.html { render :index, status: :unprocessable_entity }
        end
      end
  end

  private
  def comment_params
    params.require(:comment).permit(:comment).merge(user_id: current_user.id, plan_id: params[:plan_id])
  end

  def set_plan
    @plan = Plan.find(params[:plan_id])
  end

  def set_comments
    @comments = @plan.comments.includes(:user)
  end
end

2.ビューの変更。
コントローラーで変更したformat.turbo_streamの記述によって、レスポンスではxxxx.turbo_stream.erb形式のビューが返されるようになります。

アクション名に対応したビューが返るため、今回はcreate.turbo_stream.erbファイルを作成します。

app/views/comments/create.turbo_stream.erb
<h2>コメント一覧</h2>

<!-- id="new_comment"の要素を、ブロックで囲んだ内容と置き換えする --!>
<%= turbo_stream.replace "new_comment" do %>
  <%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
<% end %>

<!-- id="comments"の要素に対して、ブロックで囲んだ内容を差し込む --!>
<%= turbo_stream.prepend "comments" do %>
  <%= render @comments %>
<% end %>

<%= render @comments %><%= render partial: "comment", collection: @comments %>は同義。

こちらのファイル内では、turbo_streamのメソッドを用いて、ビューの更新をしていきます。

turbo_stream.replace
更新をかけたいターゲット要素を含めて更新する。
turbo_streamタグで囲んでいる要素を、id="new_comment"を付与された要素と置き換えて更新をかける。(今回は_form.html.erb内にid="new_comment"を持ったdivタグを用意)
こうすることで、コメント投稿が完了した後にフォームがリセットされて、再度新規投稿ができるようになる。

turbo_stream.prepend
→更新したいターゲット要素の先頭に要素を追加する。
ターゲット要素(index.html.erbファイル内のid="comments")の先頭にブロック内の<%= render @comments %>差し込むことで、非同期通信でコメント投稿ができる。

なお、部分テンプレートとして1投稿分のコメントを描写しているビューは以下になります。
turbo_frame_tag を使用することによって、”comment”というIDを持ったTurbo Frameを作成できます。このturbo Frame内あるコンテンツが非同期的に更新されるように実装が可能です。

app/views/comments/_comment.html.erb
<%= turbo_frame_tag dom_id(comment) do %>
  <div class="comment">
    <div class="chat chat-start">
      <div class="chat-header">
        <%= comment.user.nickname %>
        <time class="text-xs opacity-50"><%= l comment.created_at %></time>
      </div>
      <div class="chat-bubble">
        <%= comment.comment %>
        <div class="flex justify-end text-xs mt-1">
          <%= link_to '削除', plan_comment_path(comment.plan, comment), data: { turbo_method: :delete} %>
          <%= link_to '編集',edit_plan_comment_path(comment.plan, comment), class:"ml-2" %>
        </div>
      </div>
    </div>
  </div>
<% end %>

ここまでの実装で、以下のような非同期通信でのコメント投稿が完了します。
挙動確認

3. コメント編集機能の実装。

1.編集用のコントローラーの作成

コントローラーにeditアクションを追加します。
createアクションの時と同様に、respond_toを用いて、turbo_streamのテンプレートをレスポンスとして返します。

app/controllers/comments_controller.rb
  def edit
    @comment = Comment.find(params[:id])
  end

  def update
    @comment = Comment.find(params[:id])
    respond_to do |format|
      if @comment.update(comment_params)
        @comment = Comment.new
        format.html { redirect_to plan_comments_path }
        format.turbo_stream
      else
        format.html { render :index, status: :unprocessable_entity }
      end
    end
  end

2.ビューファイルの修正

①編集ページを描写させるための記述はこちら。
ポイントは置き換えたい部分をturbo_frame_tagで囲むこと。
また、入力フォームを準備する必要があったので、以下のように記述しました。

edit.html.erb
<%= turbo_frame_tag dom_id(@comment) do %>
  <div class="comment">
    <%= form_with model:[@plan, @comment],local:true, remote: true do |f|%>
      <div class="chat chat-start">
        <div class="chat-header">
          <%= @comment.user.nickname %>
          <time class="text-xs opacity-50"><%= l @comment.updated_at %></time>
        </div>
        <div class="chat-bubble">
          # こちらに入力欄を追加。入力欄に記入された値をもとに、更新をupdateアクションでしていきます。
          <%= f.text_field :comment, class:"input input-bordered w-full bg-gray-600" %>
          <div class="flex justify-end text-xs mt-1">
            <%= link_to 'キャンセル', plan_comments_path(@plan) %>
            <%= f.submit "更新", class:"ml-2" %>
          </div>
        </div>
      </div>
    <% end %>
  </div>
<% end %>

②更新後のページを描写させるための記述はこちら。
createアクションと同じく、update.turbo_stream.erbファイルを作成してcreateと同じ記述をしています。

update.turbo_stream.erb
<%= turbo_stream.prepend "comments" do %>
  <%= render @comments %>
<% end %>

<%= turbo_stream.replace "new_comment" do %>
  <%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
<% end %>

ここまでの実装で、コメント編集機能は完了です!
挙動確認

4.コメント削除機能の実装

1. 削除用のコントローラーの作成

編集と同じく、respond_toを使用してdestroy.turbo_stream.erbを表示させる記述をします。
コメントを削除した後、reset_formを呼び出しているところがポイントです。
reset_formがない場合、コメント入力欄に@commentの値が残ってしまうので、フォームのリセットをするために記述をします。
(editやcreateでも同じ記述をしていたので、メソッド化をしました)

app/controllers/comments_controller.rb
  def destroy
    respond_to do |format|
      if @comment.destroy
        reset_form
        @comments = @plan.comments.includes(:user)
        format.html { redirect_to plan_comments_path }
        format.turbo_stream
      else
        format.html { render :index, status: :unprocessable_entity }
      end
    end
  end

  ~省略~
  private
  def reset_form
    @comment = Comment.new
  end

2. ビューファイルの作成

削除後に表示させるdetroy.turbo_stream.erbを作成。
ポイントは、turbo_stream.updateメソッドを使用すること。

app/views/comments/destroy.turbo_stream.erb
<%= turbo_stream.replace "new_comment" do %>
  <%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
<% end %>

<!-- turbo_stream.updateを使用して、更新をかける。 --> 
<%= turbo_stream.update "comments" do %>
  <%= render @comments %>
<% end %>

edit/updateアクションの場合は、編集をするターゲットも含めて更新をしたかったため、replaceを使用しました。
一方で、destroyのアクションの場合は削除したターゲットのみを更新(削除)するため、updateを使用します。

ここまでの実装で、以下のような非同期通信でのコメント投稿が完了します。
挙動確認

まとめ

今回はじめてturbo_streamを用いて投稿や編集の実装にチャレンジしてみました。
体感としては理屈さえ押さえてしまえば、javascriptを書くよりも簡単に非同期で投稿・編集・削除の実装が出来た気がします!

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2