たとえば商品の購入や、お店の予約など、入力→確認→完了という遷移をさせたい場面は多いと思います。確認画面には「入力画面に戻る」と「送信する」の2つの遷移が必要ですが、実現するためには入力値を送信まで保存しておかなければならないという点が難しいところです。
以下ふたつの方法で検討したので、メリット・デメリット比較してご紹介します。
- 確認画面をフォームにして、hidden項目を持っておく
- 入力値をセッションに保存する
開発環境
Ruby 2.6.5
Rails 6.0.3.2
サンプルの仕様
以下のような予約フォームを例に、ふたつの方法で実装してみます。

↓

↓

モデルの実装
当予約フォームはReserveモデルと対応しています。migrationとモデルを以下の通りに作成しました。
class CreateReserve < ActiveRecord::Migration[6.0]
def change
create_table :reserves do |t|
t.string :reserve_date
t.string :name
t.string :note
t.timestamps
end
end
end
class Reserve < ApplicationRecord
end
routesの実装
routesは以下の通りです。backというアクションを作成するところがポイントです。
get 'reserve/new' # 入力画面
post 'reserve/confirm' # 確認画面
post 'reserve/back' # 確認画面から「入力画面に戻る」をクリックしたとき
post 'reserve/complete' # 完了画面
ビューの実装
ビューは以下3つを作成します。
- app/views/reserve/new.html.erb
- app/views/reserve/confirm.html.erb
- app/views/reserve/complete.html.erb
入力画面の実装はシンプルなフォームになっています。
<h1>予約フォーム</h1>
<ol class="nav">
<li class="current">1.入力</li>
<li>2.確認</li>
<li>3.完了</li>
</ol>
<%= form_for @reserve, method: :post, url: reserve_confirm_path do |form| %>
<dl>
<dt>希望日</dt>
<dd><%= form.text_field :reserve_date %></dd>
<dt>名前</dt>
<dd><%= form.text_field :name %></dd>
<dt>備考</dt>
<dd><%= form.text_field :note %></dd>
</dl>
<%= form.submit '確認画面へ' %>
<% end %>
完了画面の実装はただのHTMLです。
<h1>予約フォーム</h1>
<ol class="nav">
<li>1.入力</li>
<li>2.確認</li>
<li class="current">3.完了</li>
</ol>
<p>ご予約ありがとうございました。</p>
ここまでサンプルの共通部分の実装をご紹介しました。確認画面のビューの実装や、コントローラの実装ですが、2種類の方法を以下にご紹介します。
確認画面をフォームにして、hidden項目を持っておく方法
こちらは理解しやすい方法で、項目の少ないフォームならオススメの実装です。
ビューの実装
確認画面のビューの実装は以下の通りです。
<h1>予約フォーム</h1>
<ol class="nav">
<li>1.入力</li>
<li class="current">2.確認</li>
<li>3.完了</li>
</ol>
<dl>
<dt>希望日</dt>
<dd><%= @reserve.reserve_date %></dd>
<dt>名前</dt>
<dd><%= @reserve.name %></dd>
<dt>備考</dt>
<dd><%= @reserve.note %></dd>
</dl>
<%= form_for @reserve, method: :post, url: reserve_back_path do |form| %>
<%= form.hidden_field :reserve_date %>
<%= form.hidden_field :name %>
<%= form.hidden_field :note %>
<%= form.submit '入力画面に戻る' %>
<% end %>
<%= form_for @reserve, method: :post, url: reserve_complete_path do |form| %>
<%= form.hidden_field :reserve_date %>
<%= form.hidden_field :name %>
<%= form.hidden_field :note %>
<%= form.submit '送信する' %>
<% end %>
18〜23行目をご確認ください。
<%= form_for @reserve, method: :post, url: reserve_back_path do |form| %>
<%= form.hidden_field :reserve_date %>
<%= form.hidden_field :name %>
<%= form.hidden_field :note %>
<%= form.submit '入力画面に戻る' %>
<% end %>
「入力画面に戻る」ボタンとhidden項目だけのフォームになっています。ボタンをクリックするとbackアクションが呼び出され、@reserve変数を設定した上でnew画面を表示します。
同様に25〜30行目も見ていきます。
<%= form_for @reserve, method: :post, url: reserve_complete_path do |form| %>
<%= form.hidden_field :reserve_date %>
<%= form.hidden_field :name %>
<%= form.hidden_field :note %>
<%= form.submit '送信する' %>
<% end %>
「送信する」ボタンとhidden項目だけのフォームになっています。ボタンをクリックするとcompleteアクションが呼び出され、reservesテーブルに保存したあと、complete画面を表示します。
このように、hidden項目に値を保存しておくことで、確認画面を挟んだとしても値を保持することができ、入力→確認→入力に戻る→確認→送信 といった操作が可能になります。
コントローラの実装
コントローラは以下の通りです。
class ReserveController < ApplicationController
before_action :permit_params, except: :new
def new
@reserve = Reserve.new
end
def back
@reserve = Reserve.new(@attr)
render :new
end
def confirm
@reserve = Reserve.new(@attr)
if @reserve.invalid?
render :new
end
end
def complete
Reserve.create!(@attr)
end
private
def permit_params
@attr = params.require('reserve').permit(:id, :reserve_date, :name, :note)
end
end
ポイントはbackアクションです。
def back
@reserve = Reserve.new(@attr)
render :new
end
@attr変数には、hidden項目に保持していた入力値が入っています。その値でReserveモデルを生成し、render :new で入力画面を表示させています。
モデルのバリデーションはconfirmアクションで行っています。入力チェックは、入力画面→確認画面の遷移のタイミングで行いたいからです。invalid?メソッドは、バリデーションエラーの場合にtrueになります。エラーがあった場合には入力画面に戻します。
メリット、デメリット
シンプルでわかりやすい方法ですが、hiddenフィールドを項目の数だけ記述しなければならず、しかも「入力画面に戻る」ボタンと「送信する」ボタンの2箇所に必要です。たとえばフォームにひとつ項目が追加になったとすると、入力画面と、確認画面で合わせて3箇所に修正が必要です。hiddenフィールドの部分のみpartialに切り出すなどしても、キレイなコードとは言えないでしょう。保守性の悪さが難点です。
入力値をセッションに保存する方法
セッションを利用する方法では、hiddenを利用するときのデメリットが解消できます。
ビューの実装
確認画面のビューの実装は以下の通りです。
<h1>予約フォーム</h1>
<ol class="nav">
<li>1.入力</li>
<li class="current">2.確認</li>
<li>3.完了</li>
</ol>
<dl>
<dt>希望日</dt>
<dd><%= @reserve.reserve_date %></dd>
<dt>名前</dt>
<dd><%= @reserve.name %></dd>
<dt>備考</dt>
<dd><%= @reserve.note %></dd>
</dl>
<%= form_for @reserve, method: :post, url: reserve_back_path do |form| %>
<%= form.submit '入力画面に戻る' %>
<% end %>
<%= form_for @reserve, method: :post, url: reserve_complete_path do |form| %>
<%= form.submit '送信する' %>
<% end %>
18〜24行目では、hidden項目は持たずに、submitボタンだけになっています。
コントローラの実装
コントローラは以下の通りです。
class ReserveController < ApplicationController
before_action :permit_params, only: :confirm
def new
session.delete(:reserve)
@reserve = Reserve.new
end
def back
@reserve = Reserve.new(session[:reserve])
session.delete(:reserve)
render :new
end
def confirm
@reserve = Reserve.new(@attr)
session[:reserve] = @reserve
if @reserve.invalid?
render :new
end
end
def complete
Reserve.create!(session[:reserve])
session.delete(:reserve)
end
private
def permit_params
@attr = params.require('reserve').permit(:id, :reserve_date, :name, :note)
end
end
Railsでセッションを使う方法はとても簡単です。
セッションに登録するときは、session[:reserve] = @reserve
セッションから取り出すときは、session[:reserve]
と書きます。
confirmアクションでセッションに登録し、backアクションとcompleteアクションではセッションの内容を利用しています。こうすることでhiddenに項目を持たなくても入力値の一時的な保存が可能になります。
メリット、デメリット
メリットはコードの重複がなくなり保守性が上がることです。
デメリットは、セッションには期限や容量上限があることです。デメリットでもないのですが、セッションタイムアウトやセッション削除の処理などが必要になり、少し面倒ということです。
Railsには3つのセッション方式があり、どれを選択するかによって変わります。
- CookieStore(クッキー方式)
- Redis(インメモリ方式)
- ActiveRecord(DB方式)
詳細はこちら
Railsのセッション管理には何が最適か – Qiita
デフォルトではCookieStoreになっていますが、Cookieに保存できる容量は小さいため、実運用には向きません。RedisかActiveRecordを選択することになると思います。
応用:resourcesと組み合わせた方法
ここからは応用編ですが、筆者としてはこちらが本題。
Railsのルーティングにresourcesというのがあります。特定のモデルに対して一覧、詳細、作成、編集、削除というよくあるCRUD (Create/Read/Update/Delete) 操作が1行で定義できる便利ルーティングです。
↓こう書くと
resources :photos
以下の7つのルーティングが生成されます。※Railsガイドから抜粋

このresourcesに確認画面も入れたいという要件です。つまり、
- 一覧
- 詳細
- 新規作成→確認→登録
- 編集→確認→登録
という操作が必要というものでした。こんな要件あまりないので誰の参考にもならないかもしれませんが…。具体的にはメール配信機能で、確認画面はメールのプレビュー画面に相当しています。一覧はメールの配信履歴です。編集ができるのはメール未配信のときのみ、という仕様です。
customer_mailsというリソースに対して以下のようなroutesを定義しました。
resources :customer_mails, except: [:create, :update] do
member do
post :confirm, action: :confirm, as: :edit_confirm
post :edit, action: :back, as: :edit_back
post :register, action: :register, as: :edit_register
end
collection do
post :confirm, action: :confirm, as: :new_confirm
post :new, action: :back, as: :new_back
post :register, action: :register, as: :new_register
end
end
少し冗長になってしまったのでresourcesを使う意味もなかったかもしれません…。こういうときのベストプラクティスを教えてほしいです。
まとめ
確認画面を作る方法を3つご紹介しました。ネットで他の記事も検索したのですがhiddenで実装しているものが多かったので、保守しやすく実運用で使える方法を模索した結果を記事にしました。参考になれば幸いです。