ウィザードフォーム形式フォームの実装 -devise×session×データを複数テーブルに分けて保存 ver.- (rails)

# ＜ウィザードフォーム形式フォームとは＞
ユーザーの新規登録画面などで、登録内容ごとにページを分けるフォームのこと。
一つのページにずらっと縦長に全ての項目が表示されるよりも分かりやすく見やすいという特徴がある。

# ＜sessionとは＞
ページ遷移するたびにフォームの内容がリセットされてしまうのを防ぐことができるため、ウィザードフォーム形式フォームの実装に不可欠である。
ページごとに入力されたデータを一時的にセッションの中に保存し、最後のページの入力が終了し次第データベースに保存する。

# ＜手順＞

1.　deviseの導入
2.　コントローラの作成
3.　ビューファイルの作成
4.　ルーティングの設定
5.　モデル同士のアソシエーションの定義
6.　ビューファイルのマークアップ
7.　コントローラの編集（sessionを使ってデータ保持）
6.　情報を一括登録（sessionのデータとともにdbに保存）
7.　自動的にログインさせる
8.　バリデーションの追加
9.　[ちなみに...]　sessionデータの保存先を変更する。（ActionDispatch::Cookies::CookieOverflowエラーの解決方法）


## deviseの導入
1. Gemfileに ```gem 'devise'```
を記載。
2. その後以下をコンソールで実行
 
```:ターミナル
$ bundle install
$ rails g devise:install #deviseを使うのに必要なファイルの生成
$ rails g devise user #deviseでUserモデルの生成
```

3. Usersモデルにカラムを記載して```bundle exec rake db:migrate```してUsersテーブルを生成しておく。
4. deviseにnameカラムを追加しておく。（手順は今回は省略。)
5. その他にデータを保存したいテーブルがあったら作成しておく。（今回はUsersテーブルに加えProfilesテーブルを併用する。）

## コントローラの作成
コントローラを作成する。（今回はsignupコントローラで作成。）

```:ターミナル
$ bundle exec rails g controller signup
```

## ビューファイルの作成

signup#index (登録方法選択ページ) ＊step1にリンク飛ばすだけなので今回は省略
step1 (会員情報入力ページ)　⇨UsersテーブルとProfilesテーブルに保存したい
step2 (sms認証ページ)　⇨Profilesテーブルに保存したい
step3 (住所入力ページ)　⇨Profilesテーブルに保存したい
complete_signup (登録完了報告ページ)

## ルーティングの設定
collectionの中身のアクションがget, post, post, getになっていることに注意。

```ruby:routes.rb

get "signup", to: "signup#index"
resources :signup do
  collection do
    get 'step1'
    post 'step2'
    post 'step3'  #入力が全て完了
    get 'complete_signup'  #登録完了後
  end
end
```
パスがちょっとおかしくなるが動作するので気にしない。

## モデル同士のアソシエーションの定義
今回は全て１対１で組む。
has_manyだとちょっと書き方が変わるらしいので注意。今回は省略。

```ruby:models/user.rb

# 以下を追記
  has_one :profile
  accepts_nested_attributes_for :profile
  
```
```ruby:models/profile.rb

# 以下を追記
  belongs_to :user
  
```

## ビューファイルのマークアップ（haml）

1. form_forで次のページへのurlパスを指定する。これによってsubmitボタンが押された時に次のページに遷移してくれる。
`= form_for @user, url: [次のページのパス] do |f|`
2. hidden_field_tagを使うとコントローラで現在のステップを引っ張れるのだが結局使わなかった。。。
3. 親モデルはform_forを用いて書き、それにネストさせる感じで子モデルはfields_forを用いて書く。

```ruby:step1.html.haml

= form_for @user, url: step2_signup_index_path do |f|
  = hidden_field_tag :current_step, 'step1'

    = f.label :nickname, "ニックネーム"
    = f.text_field :nickname

    = f.label :email, "メールアドレス"
    = f.text_field :email

    = f.label :password, "パスワード"
    = f.password_field :password

    = f.label :password, "パスワード(確認)"
    = f.password_field :password_confirmation

  # ここからprofileテーブルに入れる。fields_forを使う。form_forにきちんとネストできているか確認。
  = f.fields_for :profile do |p|

    = p.label :family_name_kanji, "お名前(全角)"
    = p.text_field :family_name_kanji
    = p.text_field :first_name_kanji

    = p.label :family_name_kana, "お名前カナ(全角)"
    = p.text_field :family_name_kana
    = p.text_field :first_name_kana
    
    = p.label :birthday, "生年月日"
      = p.select :birth_year, #省略
      = p.select :birth_month, #省略
      = p.select :birth_day, #省略

  = f.submit '次へ進む'

```
 最後はsubmitボタンで締める。


## コントローラの編集
コントローラでフォームに入力された情報をsessionにぶち込むコードを書く。
1. private以下でストロングパラメータを設定。子モデル（今回はprofile）をストロングパラメータに入れる場合は `モデル名_attributes: [:id, :カラム名]` を用いる。この:idは親モデルとの関連づけに必須なので忘れずに。
2. @userにnewアクションで取ってきた情報を代入。
3. buidメソッドを使ってモデル同士を関連づける。
（関連付けのパターンによって書き方が変わるので気をつけること。今回は１対１の場合。参照：[Railsモデルの関連付けでbuildを使う時のメソッド名 · GitHub](https://gist.github.com/seak0503/7291012e7e99fc8aca80)）

＜step1＞

```ruby:signup_controller.rb


  def step1
    @user = User.new
    @user.build_profile #userモデルとprofileモデルの関連付け。
  end

  
  private
  def user_params
    params.require(:user).permit(
      :nickname,
      :email,
      :password, 
      :password_confirmation, 
      profile_attributes: [:id, :family_name_kanji, :first_name_kanji, :family_name_kana, :first_name_kana, :birth_year, :birth_month, :birth_day]
    )
  end
end

```

## これを遷移ページ分繰り返す

＜step2＞
1. コントローラでまず一個前のページで入力した情報をsessionにぶち込む。この際モデルごとに分ける必要がある。
2. @user = User.newはいちいち書く必要があるので注意。ページごとにフォームが違うので、逐一入力された情報を取得する。
3. profile_attributesにphone_numberカラムを追加

```ruby:step2.html.haml

= form_for @user, url: step3_signup_index_path do |f|
  = hidden_field_tag :current_step, 'step2'
  = f.fields_for :profile do |p|

    = p.label :phone_number, "携帯電話の番号"
    = p.text_field :phone_number

  = f.submit 'SMSを送信する', class: 'btn-default btn-red'
```
```ruby:signup_controller.rb


  def step2
    session[:user_params] = user_params  #userモデルの値をぶっこむ。
    session[:profile_attributes_after_step1] = user_params[:profile_attributes]  # profileモデルの値をぶっこむ。
    @user = User.new
    @user.build_profile
  end
   
 
  private
  def user_params
    params.require(:user).permit(
      :nickname,
      :email,
      :password, 
      :password_confirmation,
      profile_attributes: [:id, :family_name_kanji, :first_name_kanji, :family_name_kana, :first_name_kana, :birth_year, :birth_month, :birth_day, :phone_number]
    )
  end
  
```

＜step3＞
1. 前回と同じくsessionに情報をぶち込むが。前に使ったのと"別名"のsessionに入れる。()同じ名前だとしまうと値が上書きされて前の値が吹っ飛んでしまうため。)
2. merge!メソッドを用いて、同モデルのsessionをひとまとめにする。
4. 最後はsessionに情報を入れておく必要はないが、paramsには値を入れておく。
5. form_forのurlはcreateアクションに飛ばすためのパスを指定。念のためmethodも指定しておく。`= form_for @user, url: [createアクションへのパス], method: :post do |f|`
6. sessionにぶっ込んだ情報を引数として渡す。
7. createアクション内でまずsessionの値をdbにぶち込む。
8. そのあと今回のビューで入力された情報をdbに直接ぶっこむ。
9.  新規登録後勝手にログイン状態にさせる。
＜条件分岐＞dbに情報が保存されたらsessionにユーザーのidをぶっ込みログイン状態を保持させる。（deviseのsign_inアクションを拝借。）その後登録完了ページのパスに飛ばす。
保存に失敗したら最初から登録させ直すためstep1のパスに飛ばす。

```ruby:step3.html.haml

= form_for @user, url: complete_signup_signup_index_path, method: :post do |f|
  = hidden_field_tag :current_step, 'step3'
  = f.fields_for :profile do |p|
 
    = p.label :postal_code, "郵便番号"
    = p.text_field :postal_code

    = p.label :prefectures, "都道府県"
    = p.collection_select :prefectures, #省略

    = p.label :city, "市区町村"
    = p.text_field :city
   
    = p.label :address1, "番地"
    = p.text_field :address1

    = p.label :address2, "建物名"
    = p.text_field :address2

    = p.label :phone_number, "電話番号"
    = p.text_field :phone_number

  = f.submit '登録を完了する'
```

```ruby:signup_controller.rb

  def step3
    session[:profile_attributes_after_step2] = user_params[:profile_attributes]  # step2で入力された情報をsessionにぶっこむ。
    session[:profile_attributes_after_step2].merge!(session[:profile_attributes_after_step1])  # step2のsessionにstep1のsessionの中身を合わせる。
    @user = User.new
    @user.build_profile
  end
  
  def create
    @user = User.new(session[:user_params])  # ここでuserモデルのsessionを引数で渡す。
    @user.build_profile(session[:profile_attributes_after_step1])  # ここでprofileモデルのsessionを引数で渡す。
    @user.build_profile(user_params[:profile_attributes])  # 今回のビューで入力された情報を代入。
    if @user.save
      session[:id] = @user.id  #　ここでidをsessionに入れることでログイン状態に持っていける。
      redirect_to complete_signup_signup_index_path
    else
      render '/signup/step1'
    end
  end
  
  
  private
  def user_params
    params.require(:user).permit(
      :nickname,
      :email,
      :password, 
      :password_confirmation, 
      profile_attributes: [:id, :family_name_kanji, :first_name_kanji, :family_name_kana, :first_name_kana, :birth_year, :birth_month, :birth_day, :phone_number, :postal_code, :prefectures, :city, :address1, :address2]
    )
  end
  
```

## バリデーションの追加
各ページごとにバリデーションをかけ、内容に不備がある状態でページ遷移をさせなくする。
1. before_actionを用いて、submitボタンが押されてから次のページに遷移する"直前"にバリデーションをかける。
2. インスタンス変数に代入させたてからバリデーションをかけたいので一旦ここでまず値をsessionにぶち込んでしまう。（ここでぶっ込んじゃうので次のページのアクション内でぶち込む必要はない。）
3. 代入したインスタンス変数にバリデーションをかけ、失敗したらページに留まるようそのページのパスに飛ばす。
4. 最後のページはcreateアクション内ですでにバリデーションをかけているのでここではしなくていい。

```ruby:signup_controller.rb

  before_action :save_step1_to_session, only: :step2
  before_action :save_step2_to_session, only: :step3

  def step1
    @user = User.new
    @user.build_profile
  end
  # 以下バリデーション
  def save_step1_to_session
    session[:user_params] = user_params
    session[:profile_attributes_after_step1] = user_params[:profile_attributes]
    @user = User.new(session[:user_params])
    @user.build_profile(session[:profile_attributes_after_step1])
    render '/signup/step1' unless @user.valid?
  end 


  def step2
    @user = User.new
    @user.build_profile
  end
  # 以下バリデーション
  def save_step2_to_session
    session[:profile_attributes_after_step2] = user_params[:profile_attributes]
    session[:profile_attributes_after_step2].merge!(session[:profile_attributes_after_step1])
    @user = User.new
    @user.build_profile(session[:profile_attributes_after_step2])
    render '/signup/step2' unless session[:profile_attributes_after_step2][:phone_number].present?
  end


  def step3
    @user = User.new
    @user.build_profile
  end
  

  def create
    @user = User.new(session[:user_params])
    @user.build_profile(session[:profile_attributes_after_step2]) 
    @user.build_profile(user_params[:profile_attributes])
    if @user.save
      session[:id] = @user.id
      redirect_to complete_signup_signup_index_path
    else
      render '/signup/step1'
    end
  end
  
  private
  def user_params
    params.require(:user).permit(
      :nickname,
      :email,
      :password, 
      :password_confirmation, 
      profile_attributes: [:id, :family_name_kanji, :first_name_kanji, :family_name_kana, :first_name_kana, :birth_year, :birth_month, :birth_day, :postal_code, :prefectures, :city, :address1, :address2, :phone_number]
    )
  end
```

## 終了！！！
お疲れ様でした！

# ちなみに...
sessionというのは情報を一時的にcookieに保存しているのだが、ある一定量を超えるとActionDispatch::Cookies::CookieOverflow エラー
を起こしてしまう。
その対処法としてsessionデータの保存先をActiveRecordStoreに保存するお方法があるので紹介する。

## gemの追加
以下を記載してbundke install

```ruby:Gemfile

gem 'activerecord-session_store'
```

## session_store.rbを作成

config/initializersいかにsession_store.rbを手動で作成し、中身を以下のように記載。

```ruby:config/initializers/session_store.rb

Rails.application.config.session_store :active_record_store, key: '_lesson_session'
```

ターミナルで以下を実行（もしかしたらしなくてもいいかも。）

```:ターミナル
$ rails g active_record:session_migration
$ rake db:migrate
```
するとdbにsessionsテーブルが出現し、sessionデータがトークン化されたものが入っている。これは1行のみで、sessionが終了したら消える仕組みになっている。

#以上！！！
初Quiita投稿。
拙い文章でしたが、お付き合いありがとうございました。
誰かの役にたちますことを。
アドバイス、修正点、質問等お気軽に是非お願いします！

#参考にさせていただいた記事
[Railsウィザード形式フォームで新規登録〜Devise+session〜](https://qiita.com/ATORA1992/items/40fc543742a6df5a17c1)
[[Rails]ウィザードフォーム実装でfields_forを使って複数のテーブルに保存する](https://qiita.com/NT90957869/items/614842934feff9812203)