Rails Jqueryを使ってマルチステップフォームの実装
オリジナルアプリを作る過程でマルチステップフォーム(複数登録画面)を作ってみました。
細かい部分はまだ終わってないですがこういう流れなのかという感じで参考にしてみてください。
## 環境
- Mac
- Rails 5.2.3
今回のポイント
- reformというgemを使って、登録画面ごとにvalidationを行うこと
- Jqueryを使って隠しているビュー部分を出していきながら登録を進めていく方法
reformの使い方の詳細は省くので以下の記事や公式ドキュメントを参考にしてみてください。
- https://qiita.com/akichim21/items/afe961ea752f894b389b
- https://qiita.com/subaru-shoji/items/e548f10a871938bedd06
- http://trailblazer.to/gems/reform/index.html
まずはビューの部分
.registration-container
.registration-header
%h3
registration
.registration-view-part
= form_for @user, html: {class: 'registration-form clearfix'} do |f|
.first-step-registration
.field
.field-label
= f.label '名前(名)'
.field-input#first_name_input
= f.text_field :first_name, id: 'first_name'
.field
.field-label
= f.label '名前(性)'
.field-input#family_name_input
= f.text_field :family_name, id: 'family_name'
.field
.field-label
= f.label 'ニックネーム'
.field-input#nickname_input
= f.text_field :nickname, id: 'nickname'
.field
.field-label
= f.label '電話番号'
.field-input#phone_number_input
= f.password_field :phone_number, id: 'phone_number'
.btn#first-step-btn
next
.second-step-registration
.field
.field-label
= f.label '性別'
.field-input
= f.select :sex, { '男の子': 1, '女の子': 2 }
.field
.field-label
= f.label 'パスワード'
.field-input#password_input
= f.password_field :password, id: 'password'
.field
.field-label
= f.label 'パスワード(確認)'
.field-input#password_confirmation_input
= f.password_field :password_confirmation, id: 'password_confirmation'
.btn#second-step-btn
next
.third-step-registration
.field
.field-label
= f.label '学年'
.field-input#grade
= f.select :grade, {'大学1年': 1, '大学2年': 2,'大学3年': 3,'大学4年': 4,'修士1年': 5,'修士2年': 6}
.field
.field-label
= f.label '大学'
.field-input#university_input
= f.text_field :university, id: 'university'
.field
.field-label
= f.label 'Eメールアドレス'
.field-input#email_input
= f.email_field :email, id: 'email'
= f.submit 'create account', class: 'registration-submit-btn'
header {
width: 100%;
height: 100px;
line-height: 100px;
background: skyblue;
h1 {
color: #f8f8f8;
font-size: 50px;
text-align: center;
}
}
.registration-container {
width: 500px;
height: 500px;
margin: 30px auto;
.registration-header {
width: 200px;
margin: 0 auto;
height: 30px;
line-height: 30px;
text-align: center;
h3 {
font-size: 30px;
}
}
.registration-view-part {
width: 480px;
height: 550px;
margin: 30px auto;
overflow: hidden;
border: 1px solid black;
.registration-form {
width: 2000px;
height: 440px;
.clearfix::after {
content: "";
display: block;
clear: both;
}
.first-step-registration {
border-box: box-sizing;
width: 400px;
height: 500px;
padding: 40px;
float: left;
.field {
width: 400px;
height: 70px;
margin: 0 30px 30px 30px;
.field-label {
margin-bottom: 10px;
}
.field-input {
width: 80%;
input {
width: 90%;
height: 30px;
padding: 5px 10px;
border-radius: 5px;
border: 1px solid black;
}
select {
width: 100px;
height: 40px;
}
}
p {
color: red;
}
}
.btn {
width: 100px;
height: 40px;
margin: 50px auto 30px auto;
display: block;
border-radius: 3px;
font-size: 20px;
border: 1px solid black;
text-align: center;
line-height: 40px;
cursor: pointer;
}
}
.second-step-registration,
.third-step-registration {
border-box: box-sizing;
width: 400px;
height: 400px;
padding: 40px;
float: left;
.field {
width: 400px;
height: 100px;
margin: 0 30px 30px 30px;
.field-label {
margin-bottom: 10px;
}
.field-input {
width: 80%;
input {
width: 90%;
height: 30px;
padding: 5px 10px;
border-radius: 5px;
border: 1px solid black;
}
select {
width: 100px;
height: 40px;
}
}
p {
color: red;
}
}
.btn {
width: 100px;
height: 40px;
margin: 0 auto;
display: block;
border-radius: 3px;
font-size: 20px;
border: 1px solid black;
text-align: center;
line-height: 40px;
cursor: pointer;
}
.registration-submit-btn {
margin: 0 auto;
display: block;
border-radius: 3px;
font-size: 20px;
border: 1px solid black;
text-align: center;
line-height: 40px;
padding: 5px;
}
}
}
}
}
$(function() {
function check_first_name(first_name) {
var result = first_name.match(/.+/)
if(result != null) {
return true;
} else {
$('#first_name_input').after('<p>名前を入力してください</p>')
}
}
function check_family_name(family_name) {
var result = family_name.match(/.+/)
if(result != null) {
return true;
} else {
$('#family_name_input').after('<p>名字を入力してください</p>')
}
}
function check_nickname(nickname) {
var result = nickname.match(/.+/)
if(result != null) {
return true;
} else {
$('#nickname_input').after('<p>ニックネームを入力してください</p>')
}
}
function check_phone_number(phone_number) {
var result = phone_number.match(/.+/)
if(result != null) {
return true;
} else {
$('#phone_number_input').after('<p>電話番号を入力してください</p>')
}
}
function check_password(password) {
var result = password.match(/.+/)
if(result != null) {
return true;
} else {
$('#password_input').after('<p>パスワードを入力してください</p>');
}
}
function check_password_confirmation(password_confirmation) {
var result = password_confirmation.match(/.+/);
if(result != null) {
return true;
} else {
$('#password_confirmation_input').after('<p>パスワード(確認)を入力してください</p>')
}
}
function check_university(university) {
var result = university.match(/.+/);
if(result != null) {
return true;
} else {
$('#university_input').after('<p>大学名を入力してください</p>');
return false;
}
}
function alert_first_step_error(message) {
if(message["nickname"] && message["phone_number"]) {
$('#nickname_input').after("<p>" + message["nickname"] + "</p>")
$('#phone_number_input').after("<p>" + message["phone_number"] + "</p>")
} else if(message["nickname"]) {
$('#nickname').after("<p>" + message["nickname"] + "</p>")
} else if(message["phobe_number"]) {
$('#phobe_number_input').after("<p>" + message["phobe_number"] + "</p>")
}
}
function alert_second_step_error(message) {
if(message["password_confirmation"]) {
$('#password_confirmation').after("<p>" + message["password_confirmation"] + "</p>")
}
}
function alert_third_step_error(message) {
if(message["email"]) {
$('#email_input').after("<p>" + message["email"] + "</p>")
}
}
$('#first-step-btn').on('click', function() {
var first_name = $('#first_name').val();
var family_name = $('#family_name').val();
var nickname = $('#nickname').val();
var phone_number = $('#phone_number').val();
var btn = $(this)
$('.first-step-registration > .field > p').remove();
var first_name_result = check_first_name(first_name)
var family_name_result = check_family_name(family_name)
var nickname_result = check_nickname(nickname)
var phone_number_result = check_phone_number(phone_number)
if(first_name_result == true && family_name_result == true && nickname_result == true && phone_number_result == true ) {
$.ajax({
type: 'POST',
url: '/first_step',
data: { registrations: { first_name: first_name, family_name: family_name, nickname: nickname, phone_number: phone_number} },
dataType: 'json'
})
.done(function(message) {
if(message.length != 0) {
$('#nickname_input > p').remove();
$('#phone_number_input > p').remove();
alert_first_step_error(message)
} else {
$('.registration-form').css({
'transform': 'translate(-480px)',
'transition-duration': '1s'
})
}
})
}
})
$('#second-step-btn').on('click', function() {
var sex = $('#user_sex').val()
var password = $('#password').val()
var password_confirmation = $('#password_confirmation').val()
var btn = $(this)
$('.first-step-registration > .field > p').remove();
var password_result = check_password(password)
var password_confirmation_result = check_password_confirmation(password_confirmation)
if(password_result == true && password_confirmation_result == true) {
$.ajax({
type: 'POST',
url: '/second_step',
data: { registrations: { sex: sex, password: password, password_confirmation: password_confirmation } },
dataType: 'json'
})
.done(function(message) {
if(message.length != 0) {
alert_second_step_error(message)
} else {
$('.registration-form').css({
'transform': 'translate(-960px)',
'transition-duration': '1s'
})
}
})
}
})
$('#new_user').on('submit', function(e) {
e.preventDefault();
var formData = new FormData(this)
var url = $(this).attr('action');
var university = $('#university').val();
var university_result = check_university(university)
if(university_result == false) {
return;
}
$.ajax({
type: 'POST',
url: url,
data: formData,
dataType: 'json',
processData: false,
contentType: false
})
.done(function(message) {
if(message.length != 0) {
console.log('good');
alert_third_step_error(message)
$('.registration-submit-btn').prop('disabled', false);
} else {
location.href = '/users';
}
})
})
});
cssで最初の登録画面以外は隠してあります。次の登録画面へ行くボタンを各画面に配置してクリックしたらajaxでコントローラーにリクエストを送ってvalidationを行ってエラーがなければ次の登録画面に移動するという手順です。
class RegistrationsController < ApplicationController
def first_step
@form = Registrations::FirstStepUsecase.new(first_step_params).execute
if @form.errors.empty?
render json: []
else
render 'first_step_errors', formats: 'json', handlders: 'jbuilder'
end
end
def second_step
@form = Registrations::SecondStepUsecase.new(second_step_params).execute
if @form.errors.empty?
render json: []
else
render 'second_step_errors', formats: 'json', handlers: 'jbuilder'
end
end
private
def first_step_params
params.require(:registrations).permit(:first_name, :family_name, :nickname, :phone_number)
end
def second_step_params
params.require(:registrations).permit(:sex, :password, :password_confirmation)
end
end
ここでは自分でUsecaseクラスを作ってvalidation等の処理はそっちに任せています。(実際にvalidationを行うのはreformです)
module Registrations
class FirstStepUsecase
attr_reader :param, :form
def initialize(param)
@param = param
@form = Registrations::FirstStepForm.new(User.new)
end
def execute
return form unless form.validate(param)
form
end
end
end
usecaseクラスはこんな感じです。登録画面の数だけusecaseクラスを作ります。
次はreformクラスの作成です。
require "reform/form/validation/unique_validator"
module Registrations
class FirstStepForm < Reform::Form
property :first_name
property :family_name
property :nickname
property :phone_number
validates :first_name, presence: true
validates :family_name, presence: true
validates :nickname, presence: true, unique: true
validates :phone_number, presence: true, unique: true
end
end
簡単に説明しておくとここにvalitionを行いたいモデルのpropertyと
どんなvalidation行いたいかを書いておくことでvalidationすることができます。
先ほどのusecaseクラスに戻ります。
module Registrations
class FirstStepUsecase
attr_reader :param, :form
def initialize(param)
@param = param
@form = Registrations::FirstStepForm.new(User.new)
end
def executeif @form.errors.messages[:nickname].present?
json.nickname "このニックネームは使用されています。"
end
if @form.errors.messages[:phone_number].present?
json.phone_number "この電話番号は使用されてます"
end
return form unless form.validate(param)
form
end
end
end
executeメソッド内のform.validate(param)の部分でバリデーションを行ってエラーがあればfalseを返しコントローラー内で@form.errorsが利用できます。エラーがなければ普通にformを返します。
あとはエラー文があればjbuilderなどでエラー文をjsonで返して、JS側でエラー文をビューに表示する流れになります。
if @form.errors.messages[:nickname].present?
json.nickname "このニックネームは使用されています。"
end
if @form.errors.messages[:phone_number].present?
json.phone_number "この電話番号は使用されてます"
end
function check_first_name(first_name) {
var result = first_name.match(/.+/)
if(result != null) {
return true;
} else {
$('#first_name_input').after('<p>名前を入力してください</p>')
}
}
function check_family_name(family_name) {
var result = family_name.match(/.+/)
if(result != null) {
return true;
} else {
$('#family_name_input').after('<p>名字を入力してください</p>')
}
}
function check_phone_number(phone_number) {
var result = phone_number.match(/.+/)
if(result != null) {
return true;
} else {
$('#phone_number_input').after('<p>電話番号を入力してください</p>')
}
}
function alert_first_step_error(message) {
if(message["nickname"] && message["phone_number"]) {
$('#nickname_input').after("<p>" + message["nickname"] + "</p>")
$('#phone_number_input').after("<p>" + message["phone_number"] + "</p>")
} else if(message["nickname"]) {
$('#nickname').after("<p>" + message["nickname"] + "</p>")
} else if(message["phobe_number"]) {
$('#phobe_number_input').after("<p>" + message["phobe_number"] + "</p>")
}
}
$('#first-step-btn').on('click', function() {
var first_name = $('#first_name').val();
var family_name = $('#family_name').val();
var nickname = $('#nickname').val();
var phone_number = $('#phone_number').val();
var btn = $(this)
$('.first-step-registration > .field > p').remove();
var first_name_result = check_first_name(first_name)
var family_name_result = check_family_name(family_name)
var nickname_result = check_nickname(nickname)
var phone_number_result = check_phone_number(phone_number)
if(first_name_result == true && family_name_result == true && nickname_result == true && phone_number_result == true ) {
$.ajax({
type: 'POST',
url: '/first_step',
data: { registrations: { first_name: first_name, family_name: family_name, nickname: nickname, phone_number: phone_number} },
dataType: 'json'
})
.done(function(message) {
if(message.length != 0) {
$('#nickname_input > p').remove();
$('#phone_number_input > p').remove();
alert_first_step_error(message)
} else {
$('.registration-form').css({
'transform': 'translate(-480px)',
'transition-duration': '1s'
})
}
})
}
})
他の画面も同じ流れなので説明は省きますが最後の画面でユーザーを作る際にajaxでコントローラーにリクエストを送ることになるのですが, redirect_toでページ遷移ができないことに注意してください。
class UsersController < ApplicationController
def index
end
def new
@user = User.new
end
def create
@form = Registrations::ThirdStepUsecase.new(third_step_params).execute
if @form.errors.empty?
@user = User.create(user_params)
binding.pry
if @user.save
log_in @user
render json: []
else
render 'new'
end
else
render 'registrations/third_step_errors', formats: 'json', handlers: 'jbuilder'
end
end
private
def third_step_params
params.require(:user).permit(:grade, :university, :email)
end
def user_params
params.require(:user).permit(:first_name, :family_name, :nickname,
:password, :password_confirmation, :grade, :university, :phone_number,
:email, :sex
)
end
end
例えばcreateメソッド内にredirect_to を記述してもページ遷移できません。
なのでJS内でページ遷移ができるようにしてあげてください。
$('#new_user').on('submit', function(e) {
e.preventDefault();
var formData = new FormData(this)
var url = $(this).attr('action');
var university = $('#university').val();
var university_result = check_university(university)
if(university_result == false) {
return;
}
$.ajax({
type: 'POST',
url: url,
data: formData,
dataType: 'json',
processData: false,
contentType: false
})
.done(function(message) {
if(message.length != 0) {
alert_third_step_error(message)
$('.registration-submit-btn').prop('disabled', false);
} else {
location.href = '/users';
}
})
})
});
location.href = '/users'; の部分ですね。
以上 RailsとJqueryを使ったマルチステップフォームの実装をざっくりと説明してみました。
わからないところ、もっとこうした方がいいところなどがあれば教えてください。
よろしくお願いします!