はじめに
個人開発でアプリケーション作成をするにあたり、turbo streamを用いた実装をしたことがなかったため、コメント投稿機能に組み込んでみようと思いました。
備忘録も兼ねて、実装内容を残していこうと思います。
開発環境
- ruby 3.2.0
- Rails 7.0.7.2
- MySQL
完成形イメージ
コメント投稿・編集
コメント削除
私の場合、planモデルに対してcommentを複数投稿できるように実装を進めています。
ER図は以下のようなイメージです。
また、コメント投稿する動線としては、planの詳細ページ→コメント一覧ページ→コメント投稿という流れを取っています。
1. 同期通信でコメント投稿機能を実装する。
ルーティング・コントローラー・ビューをそれぞれ以下のように実装します。
Rails.application.routes.draw do
# plansコントローラーにcommentsコントローラーをネストする。
resources :plans do
resources :comments, only: [:index, :create, :edit, :destroy]
end
end
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
<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>
<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
と記述することで設定ができます。
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ファイルを作成します。
<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内あるコンテンツが非同期的に更新されるように実装が可能です。
<%= 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のテンプレートをレスポンスとして返します。
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
で囲むこと。
また、入力フォームを準備する必要があったので、以下のように記述しました。
<%= 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と同じ記述をしています。
<%= 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でも同じ記述をしていたので、メソッド化をしました)
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
メソッドを使用すること。
<%= 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を書くよりも簡単に非同期で投稿・編集・削除の実装が出来た気がします!