久々に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の値に""が代入されてしまうので、confirmednilである状態から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"が送信されると、submitedconfirmedの両方に"1"が入るのでバリデーションが成功し、saveされる
  8. confirm=""が送信された場合、バリデーションに失敗して、newテンプレートのレンダリングへの条件分岐に入る
  9. after_validationによって、confirmedsubmitedには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""が渡されて動き出す実装となっていて、仮にsubmitedconfirmedの値をセットせずにvalidなパラメタをPOSTすれば確認フローを無視してcreateを成功させることが出来ます。

(これについてはテストコードにも書いています)

確認フローはあくまで人間が失敗しないようにするためのものであり、直接モデルの操作をするような場合には不要(むしろ不都合が多い)と考えたためです。

もし、そういった場合でも制御が必要であるならば、この方法では解決出来ないので、別の見方の設計が必要だと思われます。

余談

利用しているgemや実装によっては属性名が衝突し、予期しない動作になる可能性もあるので、実装してみて、どうも様子がおかしいようであれば、任意で属性名を変更してみてください。

また、Dockerfileを同梱してみました。

Dockerが使える環境であれば、Readme のようにコマンドを実行するだけですぐに立ち上がります。

最初はよくわかんないことだらけで取っつきにくいですが、Docker めっちゃ便利ですね。

Ruby on Rails 4 アプリケーションプログラミング
Ruby on Rails 4 アプリケーションプログラミング
作者: 山田 祥寛
出版社/メーカー: 技術評論社
発売日: 2014-04-11
売上順位: 41133
プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化
プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化
作者: 阿佐 志保
出版社/メーカー: 翔泳社
発売日: 2015-11-20
売上順位: 132718
RESTful Webサービス
RESTful Webサービス
作者: Leonard Richardson
出版社/メーカー: オライリー・ジャパン
発売日: 2007-12-21
売上順位: 247813