Railsでウィザードフォームを作ってみた


はじめに

閲覧いただきありがとうございます。
よわよわエンジニアです。

僕みたいなよわよわエンジニアでもわかるように記載していきたいです。

今回のテーマと環境

Railsでウィザードフォームを実装しました。

Rails 5.2.4
Ruby 2.5.1
gem 'devise'

デバイスのインストールとモデル作成まではいってると仮定します。

ウィザードフォームとは?

入力項目が多い際にページ遷移しながら
1ページごとに入力された情報を保持して、全ての入力が終わってから最後にその情報を送信し、
DBに保存するフォームのこと。

要するに入力項目が100個あったとして、
1ページで100個入力するよりも、1ページ10個x10ページ作っちゃおうよというものです。

↓例えばこういうの

パッとみた時に
入力項目が限られているので認識しやすいが、何ページもあって萎えちゃうっていうデメリットもある。

ウィザードの動きの原理

通常なら1ページに入力したフォームの内容は、2ページ目にalink_toでページ遷移すると、
入力内容が消えてしまいますが

sessionという物を使い、入力内容を保持したままで
formsubmitを押すことでページ遷移し、全ての入力が終わった段階でcreateを走らせてDBに内容を保存します。

いざ実装

デバイスとは関係ないコントローラーを作る

まずはコントローラーを作ります。
signupなど、わかりやすい名前で作っちゃいましょう。

$rails g controller signup

ルーティングの設定

前述のとおり今回実装するウィザードは、
sessionといもので情報を保持し、次のページへ進まなければいけません。

今回はcollection doで必要な数だけページのルーティングをしましょう。

routes.rb
resources :signup do
    collection do
      get 'page1'
      get 'page2'
      get 'page3' # ここで全て入力完了。このページの送信ボタンを押した後にcreateが走る
      get 'done' # 登録完了後のページ
    end
  end

フォームを作る

各ページ入力したいフォームを作ります。
この時に、先ほど設定したルーティングの数だけページを新規作成しましょう。
その際のURLの送信先に注意!!

page1.html.haml
= form_for @user, url: page2_signup_path, method: :get, html: {class: 'form__box'} do |f|
  = f.text_field :nickname, placeholder: '例) メルカリ太郎'
  = f.email_field :email, placeholder: 'PC・携帯どちらでも可'
  = f.password_field :password, placeholder: '6文字以上'
  = f.password_field :password_confirmation, placeholder: '6文字以上'
  = f.submit #これでpage2へ進む。link_toなどで遷移すると情報が消えちゃうので注意 
page2.html.haml
= form_for @user, url: page3_signup_path, method: :get, html: {class: 'form__box'} do |f|
  = f.text_field :last_name, placeholder: '例)鬼頭'
  = f.text_field :first_name, placeholder: '例)権蔵'
  = f.text_field :last_name_kana, placeholder: '例)オニガシラ'
  = f.text_field :first_name_kana, placeholder: '例)ゴンゾウ'
  = f.submit #これでpage3へ進む。link_toなどで遷移すると情報が消えちゃうので注意 

form_for @モデル名, url: 次のページのアクションが動くprefix, do |f|
と記載してください。(クラス名はお好みで)

session登場

Railsにはsessionというものがあります。
キーバリュー形式で情報を保管できるよー。ってやつです。
保管方法は
session[:キー] = 値で保管できます。以下サンプル。

session[:name] = "田中"
#この場合、:name キーに value "田中" を保管できる。
#binding.pryなどで保管できているか確認するときは session[:name] と入力すると内容が表示される。

このsessionで入力したものを保持しながら、formのsubmitで次のページへ送り、保持した状態でcreateかけると保存できます。

コントローラーの設定

signup_controller.rb

class SignupController < ApplicationController
  # 各アクションごとに新規インスタンスを作成します←ここ重要
 # 各アクションごとに、遷移元のページのデータをsessionに保管していきます

  def page1
    @user = User.new # 新規インスタンス作成
  end

  def page2
    # step1で入力された値をsessionに保存
    session[:nickname] = user_params[:nickname]
    session[:email] = user_params[:email]
    session[:password] = user_params[:password]
    session[:password_confirmation] = user_params[:password_confirmation]
    @user = User.new # 新規インスタンス作成
  end

  def page3
    # step2で入力された値をsessionに保存
    session[:last_name] = user_params[:last_name]
    session[:first_name] = user_params[:first_name]
    session[:last_name_kana] = user_params[:last_name_kana]
    session[:first_name_kana] = user_params[:first_name_kana]
    @user = User.new # 新規インスタンス作成
  end

~省略。遷移ページ分繰り返す~

private
 # 受け取り許可するキーを設定します
  def user_params
    params.require(:user).permit(
      :nickname, 
      :email, 
      :password, 
      :password_confirmation, 
      :last_name, 
      :first_name, 
      :last_name_kana, 
      :first_name_kana, 
      ~省略。各pageで入力し、受け取る値を設定した分だけ受け取れるようにしておく~
  )
  end

ポイントは、
page1で入力した情報を、page2アクションが呼び出されるときにsession代入すること。

(理由)

signup_controller.rb
   x間違い
  def page1
    session[:nickname] = user_params[:nickname]
    session[:email] = user_params[:email]
    session[:password] = user_params[:password]
    session[:password_confirmation] = user_params[:password_confirmation]
    @user = User.new # 新規インスタンス作成
  end

上記のようにpage1が呼び出されるアクションで代入してしまうと、
page1を開いた時にはフォームに情報が入力されていないのに「何を代入するんじゃ、userの情報なんでないぞ小僧」と叱られちゃいます。

また、新規新スタンスを作るのを忘れずに。

createを走らせる方法

$rails routes

を実行して、createアクションが走るパスを探してください。

signup_index GET      /signup(.:format)                  signup#index
             POST     /signup(.:format)                  signup#create

こんな感じででてくるかと思われます。
今回ですと、signup_indexのPOSTの横にsignup#createがあるので
パスとしてはsignup_index method: :postですね。

page3.html.haml
= form_for @user, url: signup_index_path, method: :post, html: {class: 'form__box'} do |f|
  ~省略~
  = f.submit #これでcreateが走る。 

page1,page2との違いはurlとその直後のmethodです。
ここに先ほど探してきたパスを入れていますね。その結果createが動きます。

createの設定

singnup_controller.rb
def create
    @user = User.new(
      nickname: session[:nickname], # sessionに保存された値をインスタンスに渡す
      email: session[:email],
      password: session[:password],
      password_confirmation: session[:password_confirmation],
      last_name: session[:last_name], 
      first_name: session[:first_name], 
      last_name_kana: session[:last_name_kana], 
      first_name_kana: session[:first_name_kana], 
      ~省略~
    )
    if @user.save
     # ログインするための情報を保管
      session[:id] = @user.id
     #データベースに保存できた場合どこに飛ぶか?
      redirect_to done_signup_index_path
    else
     #データベースに保存ができなかった場合どこに飛ぶか?
      render '/signup/registration'
    end
  end

今まで保持してきた値をインスタンスに渡してあげます。
なので、ここはsessionの数だけ記載が必要です。
これにより、今までsession君が保持していたものをインスタンスに渡し、全てのパラメーターをもったインスタンスが誕生し、クリエイトが走ることにより、DBに登録できるという流れです。

自動ログイン

singnup_controller.rb
def done
    sign_in User.find(session[:id]) unless user_signed_in?
end

最後に、デバイスのsign_inメソッドに モデル名.findで保存したデータのidを用いてサインインさせます。
これで、doneアクションが呼び出されて自動的にサインインさせることができます。

さいごに

いかがだったでしょうか?
この通りに設定していけば動くはずです。

ただ、現在の問題点としては、
①1つのモデルでしかフォームを作れない。
②バリデーションエラーでもページ遷移できてしまう。

これに関してはまた近いうちに追記しようと思いますが、
「今知りたいんだよ!このクソ雑魚エンジニアが!」って人は下記記事を参照してもらえればわかりやすいと思います。

■他テーブルの情報も一括保存 + バリデーション
https://qiita.com/NT90957869/items/614842934feff9812203

むしろこの記事だけ見てもいいかも・・・・(根も葉もない)

以上、ご覧いただきありがとうございました。