久々にRailsでのお仕事をすることになったので、リハビリがてら弄っている今日このごろ。
タイトル通り、実際Webアプリを作り始めるとフォームの入力確認画面って必要になります。
安直な実装パターン
今までやったことのある実装パターンはこんな感じ。
users/confirm
ルートを作っていったん引き渡す- JavaScriptで確認UIを作る
お察しの通り、confirm
ルートを作るのは何か間違っている感じがするし、JSを使わずになんとかならないかとも思って調べたところ、賢い方法を書いている方がいました。
参考サイト
追記:(この方法だけでは、モデルインスタンスの操作に不都合がありましたので、後述したサンプルにて対策をしています)
「Rails 確認画面」で検索するといくつかエントリが引っかかりますが、この実装が一番賢くシンプルだと思います。
ぱっと見ただけだとわかりにくいですが、要するにバリデーションが通るまでnewテンプレートを返し続けているだけです。
:confirmed
の値をafter_validation
でセットすることでビューを切り替えています。
この際、hidden
で引き継がれた入力値と:confirmed
と:finished
がコントローラーへ送信されることでようやくvalid
な状態になり、save
が通るというわけです。
入力画面へ戻る際は、戻るボタンが:finished
の値が無いのでvalid
にならず、入力画面が表示されるという仕組みです。
再入力させる、と考えてしまうとnewアクションへ戻すという発想になりますが、その場合どうやって入力済みの値を再セットするかが課題になります。
この方法では、「戻る」=「入力者がvalidな状態ではないと判断した」だからバリデーションエラーと同じ扱いをする、というところにポイントがあると思います。
Rails4を使っていますが、acceptance
を使っているだけなので、Rails5でもRails3でも同じ発想で実装できると思います。
問題発生
しかし、使ってみると問題が出てきました。
user
インスタンスがvalid?
を呼ぶたびにtrue/false
を交互に返してくる挙動です。
例えば、画面を経由しない、モデルインスタンスだけで操作する場合、confirmed, finished
の値は使わず(確認フローを使わず)にvalid?
を通したかったのですが、after_validation
によってconfirmed
の値に""
が代入されてしまうので、confirmed
がnil
である状態からvalid?
が実行されるたびにtrue/false
が繰り返されてしまうのです。
a = Article.new(title: "One more thing.")
a.valid? # => true
# ここでafter_validationによってconfirmedに""が入る
a.valid? # => false
# confirmedに""が入っているのでfalseになるがafter_validationによってconfirmedに"1"が入る
a.valid? # => true
# confirmedに"1"が入っているのでtrueになるがafter_validationによってconfirmedに""が入る
# 以下繰り返し…
問題に対応したサンプル
自分なりに対策をしたサンプルをつくりました。
リポジトリを公開しておきますので、よければ参考にしてみてください。
(より良いアドバイスがあればPRなどでよろしくお願いいたします)
動作のおさらい
バリデーションの実行から保存されるまでの動作をまとめてみます。
new
ビューから<%= f.hidden_field :submited %>
の値が""
空文字で送信される- 空文字を受け取った
@user
はバリデーションでsubmited
の検証に失敗する after_validation
によって、submited
以外にエラーがない場合、submited
に"1"
を代入するnew
テンプレートのレンダリングへの条件分岐に入るnew
がレンダリングされるが、<%= f.hidden_field :submited %>
には"1"
が入っている状態になるsubmited == "1"
の場合は確認用のフォームを表示する- 送信ボタンに仕込んだ
confirmed="1"
が送信されると、submited
とconfirmed
の両方に"1"
が入るのでバリデーションが成功し、save
される confirm=""
が送信された場合、バリデーションに失敗して、new
テンプレートのレンダリングへの条件分岐に入るafter_validation
によって、confirmed
とsubmited
にはnil
が代入されるnew
がレンダリングされ、confirm
は""(空文字)
となり、それ以外の属性は入力済みの状態になっている
acceptance の動作
acceptance
で指定された属性名は、値を代入される(nil以外になる)ことでvalid?
に反応するので、user.submited
に値がnil
の場合には、バリデーションに失敗しません。
clear_confirming_errors
でエラーを消すことで、他の属性のエラーメッセージと混ざって表示をさせないようにしています。
(ちょっと気になるのは <%= f.button "作成", name: "#{f.object_name}[confirmed]", value: "1" %>
の部分ですが、htmlだけで実現できているので目をつぶります…)
コンセプト
この方法は、あくまで画面からsubmited
へ""
が渡されて動き出す実装となっていて、仮にsubmited
とconfirmed
の値をセットせずにvalid
なパラメタをPOST
すれば確認フローを無視してcreate
を成功させることが出来ます。
(これについてはテストコードにも書いています)
確認フローはあくまで人間が失敗しないようにするためのものであり、直接モデルの操作をするような場合には不要(むしろ不都合が多い)と考えたためです。
もし、そういった場合でも制御が必要であるならば、この方法では解決出来ないので、別の見方の設計が必要だと思われます。
余談
利用している gem や実装によっては属性名が衝突し、予期しない動作になる可能性もあるので、実装してみて、どうも様子がおかしいようであれば、任意で属性名を変更してみてください。
また、Dockerfile を同梱してみました。
Dockerが使える環境であれば、Readme のようにコマンドを実行するだけですぐに立ち上がります。
最初はよくわかんないことだらけで取っつきにくいですが、Docker めっちゃ便利ですね。