0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails × Hotwire】バリデーションエラー後も file_field の画像を消さずに保持する方法

0
Posted at

はじめに

プログラミング学習中の初心者が書いた記事になります。
画像投稿ができるアプリを開発しています。
間違っているところがあればご指摘いただけると助かります。

概要

フォーム送信時にエラーが発生した場合、renderメソッドでは、同一リクエスト内でテンプレートをレンダリング(描画)するため、すでに入力された値がそのまま再利用されます。しかし、フォーム入力値が再利用されるのはfile_field以外のみで画像は消えてしまいます。

memories_controller.rb
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 でエラーメッセージ、フラッシュメッセージだけ更新

memories_controller.rb
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

new.html.erb

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

まとめ

当初難しく考えすぎていましたが、「全体更新を避けることで、ブラウザの状態を保つ」という逆転の発想で想像よりも単純なコードにすることができました。頭を柔らかくしてコーディングしていきたいものです。

参考

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?