RailsでRESTを崩さずに確認画面を実装してみる

久々に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などでよろしくお願いいたします)

動作のおさらい

  1. new ビューから <%= f.hidden_field :submited %> の値が “” 空文字で送信される
  2. 空文字を受け取った @user はバリデーションで submited の検証に失敗する
  3. after_validation によって、submited 以外にエラーがない場合、submited に “1” を代入する
  4. new テンプレートのレンダリングへの条件分岐に入る
  5. new がレンダリングされるが、<%= f.hidden_field :submited %> には “1” が入っている状態になる
  6. submited == “1” の場合は確認用のフォームを表示する
  7. 送信ボタンに仕込んだ confirmed=”1″ が送信されると、submited と confirmed の両方に “1” が入るのでバリデーションが成功し、save される
  8. confirm=”” が送信された場合、バリデーションに失敗して、newテンプレートのレンダリングへの条件分岐に入る
  9. after_validation によって、confirmed と submited には nil が代入される
  10. 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 めっちゃ便利ですね。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする