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

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

![create_user.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242611/2b355b8d-e907-8059-ebba-debc5974ca89.gif)


**① 各項目を入力して「確認へ」を押す**
↓
**② 確認画面で確認したら「送信」を押す**
↓
**③ 完了画面へリダイレクト**


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


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

new(作成フォーム)
↓
confirm(確認画面)
↓
create(作成)
↓
show(完了画面)

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

![無題のプレゼンテーション_-_Google_スライド.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242611/87f81275-f6c7-fcd6-1b3e-165a7a404fe2.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
```
`new`→`create`の流れは今まで通りですが、やはりポイントは`confirm`を追加したことであり、`new`→`confirm`→`create`と挟みこむ感じになります。

フォームでは`new`→`confirm`に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に関してよくわからない方は、導入から文法までまとめたので、よかったら[こちら](https://qiita.com/ngron/items/c03e68642c2ab77e7283)を参考にしてみてください。
###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)`が表示されるので状態が維持されます。

```rb

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242611/f0fc890c-efb9-a0bf-0a4f-0e3f269c8ab9.png)
フォームが見えても邪魔なので、`input type`は`hidden`にしておくと良いです。

戻るボタンは`name`でパラメータを指定して、アクション側で存在すれば前の画面に`render`してあげます。

```rb

= f.submit "戻る", name: :back, class: "btn btn-secondary"`
```

```rb

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242611/db25369c-12fa-5e21-ed56-76d2189b3af7.gif)
このように、どこでどんなリクエストを送ろうと、インスタンスの状態を維持できることがゴールです。


##実装手順

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

`Controller`側のロジックは以下をイメージしてみてください。
![無題のプレゼンテーション_-_Google_スライド.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242611/bf1b7a7a-830c-044b-152c-ca7c40dbc1c1.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"
```
`new`→`next`へPOSTするために、URLを直接指定するようにしましょう。

ここでのポイントは、**状態管理したい全カラムを送信すること**です。
これを書くことによって、入力→戻る→次へとしたときにも、入力した値が常に維持されます。
例えば、`next`で入力→`new`に戻る→`next`へ遷移としたときに、`next`で入力した値が維持されます。
![create_user3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242611/016b4d0a-9c2d-8711-018d-2b34655700d1.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"

```
これも先ほど同様、`next`→`confirm`へ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