52
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Rails】インスタンスの状態を保ちながら入力 → 確認画面 → 保存 を実装する方法

Last updated at Posted at 2019-10-04

##実現したいこと
画面をまたいでフォームを入力する際、インスタンスの状態を保ちながら画面遷移したいときがあります。
例えばこんな感じで。

create_user.gif

① 各項目を入力して「確認へ」を押す

② 確認画面で確認したら「送信」を押す

③ 完了画面へリダイレクト

さらに、バリデーションにひっかかったり、戻るボタンを押しても状態を維持したいですよね。
このように、画面をまたいでもインスタンスの状態を維持するやり方について簡単に解説していきます。

##実装手順
アクションにおける処理の流れは以下の通りです。

new(作成フォーム)

confirm(確認画面)

create(作成)

show(完了画面)

今回はフォームのバリデーション、戻るボタンを考慮するため、イメージはこんな感じです。

無題のプレゼンテーション_-_Google_スライド.png

次のアクションへPOSTするたびに、
・invalid(無効)なら画面を変えない
・valid(有効)なら次の画面を表示する
・back(パラメータ)が渡ってきたら前の画面に戻る
という実装をすればいいわけです。(厳密にはアクションごとに違いますがイメージです)
具体的にコードで見ていきましょう。

##Route

config/routes.rb
Rails.application.routes.draw do
  resources :users, only: [:new, :create, :show] do
    collection do
      post :confirm
    end
  end
end

newcreateの流れは今まで通りですが、やはりポイントはconfirmを追加したことであり、newconfirmcreateと挟みこむ感じになります。

フォームではnewconfirmにPOSTするイメージで捉えてください.

##Model

app/models/user.rb
# 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

app/controllers/users_controller.rb
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

app/views/users/new.html.slim
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

app/views/users/confirm.html.slim
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にもちゃんと入力されてるのが確認できました。
RailsTest.png
フォームが見えても邪魔なので、input typehiddenにしておくと良いです。

戻るボタンは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

あとはいつも通り、作成に成功したらリダイレクトしてあげれば完了です。

app/views/users/show.html.slim
h2 ユーザーを作成しました

= render "detail", user: @user
app/views/users/_detail.html.slim
ul
  li
    | First name: #{user.first_name}
  li
    | Last name: #{user.last_name}
  li
    | Email: #{user.email}

##さらに入力画面を増やしたい場合
先ほどの例に加えて、次は性別, 年齢, 電話番号, 住所を入力する画面を加えたいとしましょう。
完成デモ↓
create_user2.gif
このように、どこでどんなリクエストを送ろうと、インスタンスの状態を維持できることがゴールです。

##実装手順

今回はnextアクション(新しい画面)を追加し、アクションの流れは以下のようにします。
new → next(追加) → confirm → create

Controller側のロジックは以下をイメージしてみてください。
無題のプレゼンテーション_-_Google_スライド.png

##Route

config/routes.rb
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

app/models/user.rb
# 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

app/controllers/users_controller.rb
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

app/views/users/new.html.slim
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"

newnextへPOSTするために、URLを直接指定するようにしましょう。

ここでのポイントは、状態管理したい全カラムを送信することです。
これを書くことによって、入力→戻る→次へとしたときにも、入力した値が常に維持されます。
例えば、nextで入力→newに戻る→nextへ遷移としたときに、nextで入力した値が維持されます。
create_user3.gif
これを各アクションでも適応してあげればユーザービリティは向上するでしょう。

###next

app/views/users/next.html.slim
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"

これも先ほど同様、nextconfirmへPOSTするため、URLを直指定しています。

###confirm

app/views/users/confirm.html.slim
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。

app/views/users/_detail.html.slim
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

52
52
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?