##実現したいこと
画面をまたいでフォームを入力する際、インスタンスの状態を保ちながら画面遷移したいときがあります。
例えばこんな感じで。
① 各項目を入力して「確認へ」を押す
↓
② 確認画面で確認したら「送信」を押す
↓
③ 完了画面へリダイレクト
さらに、バリデーションにひっかかったり、戻るボタンを押しても状態を維持したいですよね。
このように、画面をまたいでもインスタンスの状態を維持するやり方について簡単に解説していきます。
##実装手順
アクションにおける処理の流れは以下の通りです。
new(作成フォーム)
↓
confirm(確認画面)
↓
create(作成)
↓
show(完了画面)
今回はフォームのバリデーション、戻るボタンを考慮するため、イメージはこんな感じです。
次のアクションへPOSTするたびに、
・invalid(無効)なら画面を変えない
・valid(有効)なら次の画面を表示する
・back(パラメータ)が渡ってきたら前の画面に戻る
という実装をすればいいわけです。(厳密にはアクションごとに違いますがイメージです)
具体的にコードで見ていきましょう。
##Route
Rails.application.routes.draw do
resources :users, only: [:new, :create, :show] do
collection do
post :confirm
end
end
end
new
→create
の流れは今まで通りですが、やはりポイントはconfirm
を追加したことであり、new
→confirm
→create
と挟みこむ感じになります。
フォームではnew
→confirm
にPOSTするイメージで捉えてください.
##Model
# Table name: users
#
# id :integer not null, primary key
# first_name :string
# last_name :string
# email :string
# created_at :datetime not null
# updated_at :datetime not null
#
class User < ApplicationRecord
with_options presence: true do
validates :first_name
validates :last_name
validates :email
end
end
今回は便宜上、空のバリデーションのみ設定しました。
##Controller
class UsersController < ApplicationController
def new
@user = User.new
end
def confirm
@user = User.new(user_params)
render :new if @user.invalid?
end
def create
@user = User.new(user_params)
render :new and return if params[:back] || !@user.save
redirect_to @user
end
def show
@user = User.find_by(id: params[:id])
end
private
def user_params
params.require(:user).permit(:first_name, :last_name, :email)
end
end
一番のポイントは、アクションを移動する度にインスタンス情報の入ったパラメーターを渡しているところです。
ここでいうUser.new(user_params)
のとこですね。ここに全てのインスタンス情報が入っているので、確認画面へ橋渡しすれば良いわけです。
そして戻るボタンを押した時の挙動も、params[:back]
で制御しています。back
が渡ってきたら前の画面にrender
すればOK。
View
※ ここではslim
, bootstrap
, simple_form
を使ってます。
slimに関してよくわからない方は、導入から文法までまとめたので、よかったらこちらを参考にしてみてください。
###new
h2 ユーザー新規登録
= simple_form_for @user, url: confirm_users_path(@user) do |f|
= f.input :first_name
= f.input :last_name
= f.input :email
= f.submit "確認画面へ", class: "btn btn-primary"
ポイントは, URL指定でconfirm_users_path
へリクエストを投げていることです。
有効であればconfirm
アクションへ移動して次の確認画面ではUser.new(user_params)
が表示されるので状態が維持されます。
def confirm
@user = User.new(user_params)
render :new if @user.invalid?
end
###confirm
h2 以下の詳細を確認してください
= render "detail", user: @user
= simple_form_for @user do |f|
= f.input :first_name, as: :hidden
= f.input :last_name, as: :hidden
= f.input :email, as: :hidden
= f.submit "送信", class: "btn btn-primary"
= f.submit "戻る", name: :back, class: "btn btn-secondary"
次はconfirm
からcreate
にPOSTします。
前回のnew
アクションから渡ってきたuser_params
を参照してインスタンスを表示しているため、空にならずにちゃんと表示され、フォームのvalue
にもちゃんと入力されてるのが確認できました。
フォームが見えても邪魔なので、input type
はhidden
にしておくと良いです。
戻るボタンはname
でパラメータを指定して、アクション側で存在すれば前の画面にrender
してあげます。
= f.submit "戻る", name: :back, class: "btn btn-secondary"`
def create
@user = User.new(user_params)
render :new and return if params[:back] || !@user.save
redirect_to @user
end
あとはいつも通り、作成に成功したらリダイレクトしてあげれば完了です。
h2 ユーザーを作成しました
= render "detail", user: @user
ul
li
| First name: #{user.first_name}
li
| Last name: #{user.last_name}
li
| Email: #{user.email}
##さらに入力画面を増やしたい場合
先ほどの例に加えて、次は性別, 年齢, 電話番号, 住所を入力する画面を加えたいとしましょう。
完成デモ↓
このように、どこでどんなリクエストを送ろうと、インスタンスの状態を維持できることがゴールです。
##実装手順
今回はnext
アクション(新しい画面)を追加し、アクションの流れは以下のようにします。
new → next(追加) → confirm → create
Controller
側のロジックは以下をイメージしてみてください。
##Route
Rails.application.routes.draw do
resources :users, only: [:new, :create, :show] do
collection do
post :next #追加
post :confirm
end
end
end
入力画面が一つ増えたので、next
を追加してあげましょう。
先ほどと同様、new
アクションからnext
アクションへPOSTしてあげるイメージです。
##Model
# Table name: users
#
# id :integer not null, primary key
# first_name :string
# last_name :string
# email :string
# gender :integer
# age :integer
# phone_number :integer
# address :string
# created_at :datetime not null
# updated_at :datetime not null
#
class User < ApplicationRecord
with_options presence: true do
validates :first_name
validates :last_name
validates :email
end
#追加
with_options on: :confirm do
validates_presence_of :gender
validates_presence_of :age
validates_presence_of :phone_number
validates_presence_of :address
end
enum gender: { man: 0, woman: 1, gay: 2, bisexual: 3, transgender: 4 }
end
on: :confirm
を記述することで、invalid?(context: :confirm)
とした時のみ、カラムの有無を検証できるようにしています。
これは最初のnew
画面のみ、次画面のカラムのバリデーションをスキップするためです。
##Controller
class UsersController < ApplicationController
def new
@user = User.new
end
# 追加
def next
@user = User.new(user_params)
render :new if @user.invalid?
end
# 変更
def confirm
@user = User.new(user_params)
render :new and return if params[:back]
render :next if @user.invalid?(:confirm)
end
# 変更
def create
@user = User.new(user_params)
render :next and return if params[:back]
render :confirm and return if !@user.save
redirect_to @user
end
def show
@user = User.find_by(id: params[:id])
end
private
# 変更
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :age, :gender, :phone_number, :address)
end
end
カラムとアクションが増えただけで、やってること自体は最初に実装したものとほとんど同じなのが分かると思います。
基本的には、渡ってくるパラメーターの状態によって画面遷移を条件分岐しています。
##View
###new
h2 ユーザー新規登録(1)
= simple_form_for @user, url: next_users_path(@user) do |f|
= f.input :first_name
= f.input :last_name
= f.input :email
= f.input :age, as: :hidden
= f.input :gender, as: :hidden
= f.input :phone_number, as: :hidden
= f.input :address, as: :hidden
= f.submit "次へ", class: "btn btn-primary"
new
→next
へPOSTするために、URLを直接指定するようにしましょう。
ここでのポイントは、状態管理したい全カラムを送信することです。
これを書くことによって、入力→戻る→次へとしたときにも、入力した値が常に維持されます。
例えば、next
で入力→new
に戻る→next
へ遷移としたときに、next
で入力した値が維持されます。
これを各アクションでも適応してあげればユーザービリティは向上するでしょう。
###next
h2 ユーザー新規登録(2)
= simple_form_for @user, url: confirm_users_path(@user) do |f|
= f.input :first_name, as: :hidden
= f.input :last_name, as: :hidden
= f.input :email, as: :hidden
= f.input :age
= f.input :gender, collection: User.genders.keys
= f.input :phone_number, as: :tel
= f.input :address
= f.submit "確認画面へ", class: "btn btn-primary"
= f.submit "戻る", name: :back, class: "btn btn-secondary"
これも先ほど同様、next
→confirm
へPOSTするため、URLを直指定しています。
###confirm
h2 以下の詳細を確認してください
= render "detail", user: @user
= simple_form_for @user do |f|
= f.input :first_name, as: :hidden
= f.input :last_name, as: :hidden
= f.input :email, as: :hidden
= f.input :age, as: :hidden
= f.input :gender, as: :hidden
= f.input :phone_number, as: :hidden
= f.input :address, as: :hidden
= f.submit "次へ", class: "btn btn-primary"
= f.submit "戻る", name: :back, class: "btn btn-secondary"
最後はバケツリレーで維持してきたインスタンスからvalue
を送信してあげればOK。
ul
li
| First name: #{user.first_name}
li
| Last name: #{user.last_name}
li
| Email: #{user.email}
li
| Age: #{user.age}
li
| Age: #{user.gender}
li
| PhoneNumber: #{user.phone_number}
li
| Address: #{user.address}
お疲れ様でした。
##まとめ
アクションをまたいでインスタンスの状態を維持するにあたり、色々考えることが多かったです。
初めのころはlink_to
から長ったらしいインスタンスのパラメーターを送ったり、session
で状態を管理したりと割と強引なやり方で実装していました。
しかし今回紹介した、渡ってきたパラメーターからインスタンスを生成し、バケツリレー式に状態を管理するというやり方であれば、比較的簡単に実装できることに気づきました。
長いフォーム画面を分割したいときや、決済フォームを挟んだりするときに使える手法なので、ぜひ参考にしてみてください。
参考記事
https://kossy-web-engineer.hatenablog.com/entry/2018/10/19/063937
https://remonote.jp/rails-confirm-form