はじめに
プログラミング学習中の初心者が書いた記事になります。
画像投稿ができるアプリを開発しています。
間違っているところがあればご指摘いただけると助かります。
概要
フォーム送信時にエラーが発生した場合、renderメソッドでは、同一リクエスト内でテンプレートをレンダリング(描画)するため、すでに入力された値がそのまま再利用されます。しかし、フォーム入力値が再利用されるのはfile_field以外のみで画像は消えてしまいます。
def new
@memory = Memory.new
end
def create
@memory = current_user.memories.build(memory_params)
if @memory.save
# 省略
else
# フォームの入力不備があればnewフォーム画面を再表示
flash.now[:error] = "エラーが発生しました"
render :new, status: :unprocessable_entity
end
end
エラーが起こった場合、ユーザーは画像をもう一度選び直さなくてはならないため地味にストレスです。
この問題を試行錯誤の結果、Turbo Stream を使って 「フォームを再描画しない」 という発想に切り替えたところ、シンプルに解決できたので共有します。
環境
- Rails 7.2
- Ruby 3.3.6
- Hotwire (Turbo + Stimulus)
- Active Storage使用
なぜ画像が消えるのか
文字列の場合
render はリダイレクトではなく、同じリクエストの中で再レンダリングされます。
このとき@memoryオブジェクトにはユーザーが入力した値が保持されたままになっています。
フォームヘルパーは、その入力値を自動でvalue属性に入れてくれる仕組みになっているため、再レンダリング後も入力値が引き継がれます。
<input type="text" name="memory[title]" value="タイトル">
画像(file_field)の場合
file_fieldに入る値はセキュリティ上、ブラウザ側が再設定できないようになっています。
もし再設定できてしまうと、悪意あるユーザーがファイルを勝手に送信できる危険があるためです。
そのため、再レンダリングで<input type="file"> の 選択状態がリセット されます。
解決方法:Turbo Stream でフォームを再描画しない
当初、フォーム送信後のエラーの場合は、hiddenフィールドを使って保持するようコードを変更してみたりしたのですがうまくいかず、行き詰まっていました。
しかし、とある先輩からアドバイスをいただき、
「フォームが再描画されるから画像が消える」のなら、フォームを再描画しなければよい
という方向で考えました。
その結果、「ページ全体」を再レンダリングするのではなく、Turbo Stream を使って
- エラーメッセージを差し込む
- フラッシュメッセージを差し替える
という方法で解決しました。
失敗時に turbo_stream でエラーメッセージ、フラッシュメッセージだけ更新
def new
@memory = Memory.new
end
def create
@memory = current_user.memories.build(memory_params)
if @memory.save
# 省略
else
# フォームの入力不備があればnewフォーム画面を再表示
flash.now[:error] = "エラーが発生しました"
render_form_failure(:new)
end
end
private
# バリデーション/画像処理失敗時のレスポンス。
# turbo_stream で error メッセージと flash を差し替えることで form 全体の DOM
# (ファイル選択状態・プレビュー・テキスト入力値) を保持し、画像再選択を不要にする。
def render_form_failure(action)
respond_to do |format|
format.turbo_stream do
render turbo_stream: [
turbo_stream.update("error_explanation_container",
partial: "shared/error_messages",
locals: { object: @memory }),
turbo_stream.replace("flash_messages", partial: "shared/flash_message")
], status: :unprocessable_entity
end
format.html { render action, status: :unprocessable_entity }
end
end
view
<%= form_with model: @memory do |f| %>
<%# バリデーション失敗時にturbo_streamで中身だけ差し替えるためid付きコンテナで囲む %>
<div id="error_explanation_container">
<%= render 'shared/error_messages', object: f.object %>
</div>
<%# 入力フィールドなど 省略 %>
<% end %>
バリデーションのエラー文が差し込まれるように上記の部分をdivで囲みました。
フラッシュメッセージはもともとapplication.html.erbに配置してあります。初期描画では空の状態であるため上書きという形で対応しています。
| ターゲット | 中身 | パーシャル |
|---|---|---|
| error_explanation_container | フィールド単位のバリデーションエラー(例: 「タイトルを入力してください」) | shared/_error_messages |
| flash_messages | 総括メッセージ(例: 「エラーが発生しました」) | shared/_flash_message |
まとめ
当初難しく考えすぎていましたが、「全体更新を避けることで、ブラウザの状態を保つ」という逆転の発想で想像よりも単純なコードにすることができました。頭を柔らかくしてコーディングしていきたいものです。
参考