はじめに
ウィザード形式は、登録フォームをいくつかのページに分けて実装するやり方です。入力項目がたくさんあるサービスの場合に有効です。
ウィザード形式でのフォーム実装は既に色々な方が記事にしており、実装方法も十人十色です。sessionで値を保持したり、ページ毎にDBに値を格納したり。。
やり方としては数多く存在しますが、今回の記事では、そんなウィザード形式の実装方法の一つをご紹介できればと思います。ウィザード形式の実装をするに当たって、参考になった記事は下記に掲載しています。細かな導入方法(deviseの導入方法やTwilioの登録方法など)については本記事では割愛いたしますので、リンク先の記事をご確認くださいませ。。
開発条件
- Ruby 2.5.1
- Rails 5.2.3
- Haml・Sass記法
- 'devise'のgemを使用
- Twilioの登録(SMS認証実装のため)
実装概要
- 1ページ目でuserテーブルに格納する値を入力してもらう
- 2ページ目でuser_addressテーブルのtelephoneだけ入力してもらう(任意)
- 3ページ目でSMS認証が可能(任意)
- 4ページ目でuser_addressテーブルの残りのカラムを入力してもらう
- 5ページ目で登録完了画面!
DB設計
ユーザ関係のDBは次のとおりです!
今回は2つのDBを用意して、それぞれに値を格納します。
usersテーブル
Column | Type | Options |
---|---|---|
nickname | string | null: false |
string | null: false | |
password | string | null: false |
password_confirmation | string | null: false |
last_name | string | null: false |
first_name | string | null: false |
last_name_kana | string | null: false |
first_name_kana | string | null: false |
birthday | date | null: false |
user_addressesテーブル
Column | Type | Options |
---|---|---|
user_id | references | null: false, foreign_key: true |
last_name | string | null: false |
first_name | string | null: false |
last_name_kana | string | null: false |
first_name_kana | string | null: false |
postal_code | integer | null: false |
prefecture | integer | null: false |
city | string | null: false |
address | string | null: false |
building | string | |
telephone | integer |
実装コード!
完成したコードがこちらです!
routes
ルート設定は以下のとおり。もう少しスマートに書きたい。。
HTTPのメソッドとしては、最初に呼び出されるuser_info
と最後に表示されるuser_complete
はGETで、2,3,4ページはPOSTで通信を行なっています。
Rails.application.routes.draw do
devise_for :users, controllers: {
sessions: 'users/sessions',
registrations: 'signup'
}
resources "signup", only: [:index, :create], path: "/signup" do
collection do
get 'user_info' # 1ページ目
post 'user_tel' # 2ページ目
post 'user_tel_verification' # 3ページ目
post 'user_address' # 4ページ目
get 'user_complete' # 登録完了後のページ
end
end
end
models
モデルも2つ用意しました!
バリデーションはお好みで実装できます。
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_one :user_address
accepts_nested_attributes_for :user_address
end
class UserAddress < ApplicationRecord
belongs_to :user, inverse_of: :user_address
controller
before_action
が多くなってしまいましたが、取り敢えずはこんな感じに実装。
SMSに関する記述はコントローラーに書いてます。session
はかなり苦戦した部分ではありますが、今では(気持ちの一方通行ではありますが)マブダチだと思っています!
class SignupController < ApplicationController
#before_action多すぎ??
before_action :save_user_info_to_session, only: :user_tel
before_action :save_user_tel_to_session, only: :user_tel_verification
before_action :send_sms_code, only: :user_tel_verification
before_action :check_sms_code, only: :user_address
before_action :save_user_address_to_session, only: :create
def user_info #1ページ目(userテーブル)
@user = User.new
@user.build_user_address
end
def save_user_info_to_session #1ページ目登録後に呼び出し、sessionに値を渡す
session[:user_params] = user_params
@user = User.new(session[:user_params]) # インスタンス作成時にsessionを引数として渡す
render '/signup/user_info' unless @user.valid?
end
def user_tel #2ページ目(user_addressテーブル)
@user = User.new
@user.build_user_address
end
def save_user_tel_to_session #2ページ目登録後に呼び出し、sessionに値を渡す
session[:user_address_attributes_after_user_tel] = user_params[:user_address_attributes] # この時点では[:user_address_attributes]にはtelephoneしか格納されていない
@user = User.new(session[:user_params]) # ここでもsessionを渡す
@user.build_user_address(session[:user_address_attributes_after_user_tel]) # インスタンス作成時にsessionを引数として渡す
render '/signup/user_tel' unless @user.valid?
end
def send_sms_code #2ページ目で登録した携帯番号にSMSを飛ばす記述
@user = User.new
@user.build_user_address
send_phone_number = PhonyRails.normalize_number session[:user_address_attributes_after_user_tel][:telephone], country_code:'JP' #入力された携帯番号を変数に渡す
session[:secure_code] = random_number_generator(4) #ランダムな4桁を生成するアクションに飛んだのち、sessionに渡す
begin
client = Twilio::REST::Client.new
result = client.messages.create(
from: ENV["TWILIO_PHONE_NUMBER"], #Twilioで取得した電話番号
to: send_phone_number, # 送り先
body: "認証番号:#{session[:secure_code]}" # 送る内容
)
rescue Twilio::REST::RestError => e #エラー時の処理
@messages = "エラーコード[#{e.code}] :”#{e.message}”"
render '/signup/user_tel'
end
end
def random_number_generator(n) #ランダムな4桁の番号を生成するアクション
''.tap { |s| n.times { s << rand(0..9).to_s } }
end
def user_tel_verification #3ページ目(SMSで送ったコードの確認ページ)
@user = User.new
@user.build_user_address
end
def check_sms_code #3ページ目で入力された認証コードを照らし合わせる
session[:input_code] = user_params[:user_address_attributes] #入力されたコードをsessionに渡す
@user = User.new
@user.build_user_address
redirect_to action: :user_tel_verification unless session[:input_code].present? #値が入力されているか確認
render '/signup/user_tel_verification' and return unless session[:secure_code] == session[:input_code][:telephone] #SMSで送ったコードと実際に入力されたコードの照合
end
def user_address #4ページ目(user_addressテーブル)
@user = User.new
@user.build_user_address
end
def save_user_address_to_session #4ページ目登録後に呼び出し、sessionに値を渡す
session[:user_address_attributes_after_user_address] = user_params[:user_address_attributes]
session[:user_address_attributes_after_user_address].merge!(session[:user_address_attributes_after_user_tel]) # 4ページ目に登録したアドレスと2ページ目に登録した電話番号を結合
@user = User.new
@user.build_user_address
render '/signup/user_address' unless session[:user_address_attributes_after_user_address].present? #本来はここでバリデーションしたいが、上手く行かないため止むを得ずpresent?
end
def create #4ページ目までの登録が完了した時点で、一気にDBに突っ込む
@user = User.new(session[:user_params])
@user.build_user_address(session[:user_address_attributes_after_user_address])
if @user.save
sign_in User.find(@user.id) #ログイン状態を保持する記述
redirect_to user_complete_signup_index_path #登録完了画面にリダイレクト
else
render user_address_signup_index_path # ダメだった場合4ページ目にリダイレクト
end
end
def user_complete #登録完了ページにて、sessionの削除(明示的に消さないとずっと残ってしまうため)
session[:user_params].clear
session[:secure_code].clear
session[:input_code].clear
session[:user_address_attributes_after_user_address].clear
end
private
def user_params #ストロングパラメーターでユーザに関わる情報を保護
params.require(:user).permit(
:nickname,
:email,
:password,
:password_confirmation,
:last_name,
:first_name,
:last_name_kana,
:first_name_kana,
:birthday,
user_address_attributes: [:id, :last_name, :first_name, :last_name_kana, :first_name_kana, :postal_code, :prefecture, :city, :address, :building, :telephone]
)
end
end
views
全てのページの実装コードを記述してしまうと量が膨大になってしまうので省略。
form_for
は非推奨なので、form_with
に書き換えたほうが良いですね。。
そうなると、書き方としては、= form_with model: @user, url: ○○_path, local: true do |f|
となると思います。
.signup-panel
%p.signup-panel__title 住所入力
= form_for @user, url: ○○○○_path, method: :post, html: {class: "inputForm"} do |f|
= f.fields_for :user_address do |p| #user_addressテーブルと紐付ける
.signup-form
.field
= p.label "お名前"
%span.form-require
= p.text_field :last_name, class: "field__input", placeholder: "例)山田"
= p.text_field :first_name, class: "field__input", placeholder: "例)太郎"
# ------------------------------------<中略>----------------------------------------------------
.signup-btn
= f.submit "次へ進む"
ちなみに。。
SMSで送った認証コードを入力してもらうフォームですが、label
やtext_field
に:input_code
と指定することはできません。なので、取り敢えずは下記のように:telephone
を指定してあげるとエラーは起きません。DBには保存せず、sessionに渡してあげるだけなので、このような記載でも問題ないと思います!
.signup-panel
%p.signup-panel__title
電話番号認証
= form_for @user, url: ○○○○_path, method: :post, html: {class: "inputForm"} do |f|
= f.fields_for :user_address do |p|
.signup-form
.field
= p.label :telephone, "認証番号"
%span.form-require
= p.text_field :telephone, placeholder: "認証番号を入力"
終わりに
今回、復習をかねての記事投稿でしたが、アウトプットするとやはり知識の定着度は違いますね!取り敢えずは動くようになりましたが、まだまだリファクタリングが必要な部分もあるかと思いますし、これからも勉強継続は必要だなと思います。文章書くのは苦手ですが、アウトプットもなんとか継続していきたいです笑
参考
ウィザード形式関係
ウィザードフォーム形式フォームの実装 -devise×session×データを複数テーブルに分けて保存 ver.- (rails)
[Rails]ウィザードフォーム実装でfields_forを使って複数のテーブルに保存する