<ウィザードフォーム形式フォームとは>
ユーザーの新規登録画面などで、登録内容ごとにページを分けるフォームのこと。
一つのページにずらっと縦長に全ての項目が表示されるよりも分かりやすく見やすいという特徴がある。
<sessionとは>
ページ遷移するたびにフォームの内容がリセットされてしまうのを防ぐことができるため、ウィザードフォーム形式フォームの実装に不可欠である。
ページごとに入力されたデータを一時的にセッションの中に保存し、最後のページの入力が終了し次第データベースに保存する。
<手順>
1. deviseの導入
2. コントローラの作成
3. ビューファイルの作成
4. ルーティングの設定
5. モデル同士のアソシエーションの定義
6. ビューファイルのマークアップ
7. コントローラの編集(sessionを使ってデータ保持)
6. 情報を一括登録(sessionのデータとともにdbに保存)
7. 自動的にログインさせる
8. バリデーションの追加
9. [ちなみに...] sessionデータの保存先を変更する。(ActionDispatch::Cookies::CookieOverflowエラーの解決方法)
deviseの導入
- Gemfileに
gem 'devise'
を記載。 - その後以下をコンソールで実行
$ bundle install
$ rails g devise:install #deviseを使うのに必要なファイルの生成
$ rails g devise user #deviseでUserモデルの生成
- Usersモデルにカラムを記載して
bundle exec rake db:migrate
してUsersテーブルを生成しておく。 - deviseにnameカラムを追加しておく。(手順は今回は省略。)
- その他にデータを保存したいテーブルがあったら作成しておく。(今回は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になっていることに注意。
get "signup", to: "signup#index"
resources :signup do
collection do
get 'step1'
post 'step2'
post 'step3' #入力が全て完了
get 'complete_signup' #登録完了後
end
end
パスがちょっとおかしくなるが動作するので気にしない。
モデル同士のアソシエーションの定義
今回は全て1対1で組む。
has_manyだとちょっと書き方が変わるらしいので注意。今回は省略。
# 以下を追記
has_one :profile
accepts_nested_attributes_for :profile
# 以下を追記
belongs_to :user
ビューファイルのマークアップ(haml)
- form_forで次のページへのurlパスを指定する。これによってsubmitボタンが押された時に次のページに遷移してくれる。
= form_for @user, url: [次のページのパス] do |f|
- hidden_field_tagを使うとコントローラで現在のステップを引っ張れるのだが結局使わなかった。。。
- 親モデルはform_forを用いて書き、それにネストさせる感じで子モデルはfields_forを用いて書く。
= 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にぶち込むコードを書く。
- private以下でストロングパラメータを設定。子モデル(今回はprofile)をストロングパラメータに入れる場合は
モデル名_attributes: [:id, :カラム名]
を用いる。この:idは親モデルとの関連づけに必須なので忘れずに。 - @userにnewアクションで取ってきた情報を代入。
- buidメソッドを使ってモデル同士を関連づける。
(関連付けのパターンによって書き方が変わるので気をつけること。今回は1対1の場合。参照:Railsモデルの関連付けでbuildを使う時のメソッド名 · GitHub)
<step1>
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>
- コントローラでまず一個前のページで入力した情報をsessionにぶち込む。この際モデルごとに分ける必要がある。
- @user = User.newはいちいち書く必要があるので注意。ページごとにフォームが違うので、逐一入力された情報を取得する。
- profile_attributesにphone_numberカラムを追加
= 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'
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>
- 前回と同じくsessionに情報をぶち込むが。前に使ったのと"別名"のsessionに入れる。()同じ名前だとしまうと値が上書きされて前の値が吹っ飛んでしまうため。)
- merge!メソッドを用いて、同モデルのsessionをひとまとめにする。
- 最後はsessionに情報を入れておく必要はないが、paramsには値を入れておく。
- form_forのurlはcreateアクションに飛ばすためのパスを指定。念のためmethodも指定しておく。
= form_for @user, url: [createアクションへのパス], method: :post do |f|
- sessionにぶっ込んだ情報を引数として渡す。
- createアクション内でまずsessionの値をdbにぶち込む。
- そのあと今回のビューで入力された情報をdbに直接ぶっこむ。
- 新規登録後勝手にログイン状態にさせる。
<条件分岐>dbに情報が保存されたらsessionにユーザーのidをぶっ込みログイン状態を保持させる。(deviseのsign_inアクションを拝借。)その後登録完了ページのパスに飛ばす。
保存に失敗したら最初から登録させ直すためstep1のパスに飛ばす。
= 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 '登録を完了する'
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
バリデーションの追加
各ページごとにバリデーションをかけ、内容に不備がある状態でページ遷移をさせなくする。
- before_actionを用いて、submitボタンが押されてから次のページに遷移する"直前"にバリデーションをかける。
- インスタンス変数に代入させたてからバリデーションをかけたいので一旦ここでまず値をsessionにぶち込んでしまう。(ここでぶっ込んじゃうので次のページのアクション内でぶち込む必要はない。)
- 代入したインスタンス変数にバリデーションをかけ、失敗したらページに留まるようそのページのパスに飛ばす。
- 最後のページはcreateアクション内ですでにバリデーションをかけているのでここではしなくていい。
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
gem 'activerecord-session_store'
session_store.rbを作成
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〜
[Rails]ウィザードフォーム実装でfields_forを使って複数のテーブルに保存する