はじめに
プログラミングスクールRUNTEQ卒業生のおかかチーズと申します。(@Yoo_pr)
RUNTEQアドベントカレンダー2024🎄 24日目を担当致します。
ポートフォリオで、私が5年ほど続けている競技「パワーリフティング」の試技結果が記録・管理ができるサービスを制作しました。
サービス名: PowerLifter's Log
GitHub: https://github.com/okaka-c/goodlifterlog
サービスでの工夫ポイントは、20項目に及ぶ入力フォームをステップ入力フォーム形式に変更し、ユーザビリティを向上させたことです。
本記事ではRuby on RailsのAction Viewテンプレート ERBビューテンプレート
を用いた
ステップ入力フォームの実装方法と、画面設計をする際にUI/UX設計面で考えた事をまとめました。
記事を読むうえでの注意点
Ruby on Rails、画面のデザインやUI/UX設計については経験が浅く、一部おかしな記述があるかもしれません。
そのためもし間違いなどがありましたらご指摘いただけると幸いです。よろしくお願いします。
環境
カテゴリ | 技術 |
---|---|
PC | MacBookAir M2 |
開発環境 | Docker |
フロントエンド | TailwindCSS, daisyUI |
バックエンド | Ruby 3.2.2 / Ruby on Rails 7.0.8.1 |
データベース | PostgreSQL |
この記事でわかること
- ステップ入力フォームとは何か概要説明
- 作ったサービスに採用することになった経緯
- ステップ入力フォームの
UI/UX設計
で意識するポイント - Ruby on Railsの以下の機能を使ったステップ入力フォームの実装方法
-
session
メソッド ActiveModel::Modelモジュール
ActiveModel::Validationsモジュール
-
ステップ入力フォームとは
入力フォームを1ページにすべて表示するのではなく、いくつかのステップに分割して順番に表示する形式です。
複数の入力項目を1ページにまとめるとフォームが長くなり、ユーザーにとって見にくく、入力の心理的ハードルが上がることがあります。
例えば、以下のイメージ画像の左側には、氏名、メールアドレス、パスワード、住所情報、クレジットカード情報を1ページにまとめた会員登録フォームがあります。
これを画像のように「ユーザー情報」「住所情報」「クレジットカード情報」の3つに分け、それぞれをステップごとに順番に入力する形式にしたものがステップ入力フォームです。
詳しい内容やメリットについては、以下の参考サイトで詳しく解説されていますので、ぜひご覧ください。本記事では詳細な説明を割愛します。
作ったサービスに採用することになった経緯
20項目の入力フォームを1ページにまとめた場合の問題点
パワーリフティングとは、スクワット、ベンチプレス、デッドリフトの3種目の挙上重量の合計値で順位を決めます。
各種目とも3回の試技に挑戦でき、試技ごとに審判が成功か失敗か判定をします。
試技結果の入力項目に必要なものは以下の通りです。
- 検量体重:試合開始前に公式に測定された体重
- 各種目の試技重量値:第一試技〜第三試技で挙上を試みた重量
- 各試技の判定結果:成功または失敗の判定
- 振り返り用コメント:試技や大会全体についてのメモ
これらの入力フォームを、サービスのMVPリリース時点で1ページにまとめていました。
それが以下の画像になります。
※【before】MVPリリース時点の入力フォーム
-
実際の使用シーンを想定すると、視線が散りやすく、入力途中の負担が大きい
スマホでの使用を想定すると、ユーザーはサービスの入力画面と試技結果が記載された手書きメモや大会結果表を交互に見ながら情報を入力します。
スマホの小さな画面では、フォーム全体をスクロールしながら入力と確認を行う必要があり、負担が増加します。
さらに、入力途中で「どこまで入力したか」が分かりにくくなり、完了までのストレスが大きくなる可能性があります。
問題点を解決するためにステップ入力フォーム形式を採用
画面遷移数が多くなる欠点はありますが、採用すると以下のメリットがありますので採用することにしました。
-
1ページあたりの入力項目を減らすことで負担軽減
- 1画面に表示される項目が限定されるため、スクロールを最小限にすることができます。
- ステップごとに進行状況が明確になるため、入力の途中で「どこまで入力したか分からない」という状況を防げます。
ページの分割方法や、画面設計面については後述しますが ステップ入力フォーム化後のafter
は以下画像のようになりました。
※【after】ステップ入力フォーム採用後
概要(ステップ入力の全体像とゴール)
本記事でこれから解説するステップ入力フォームで受け取るデータは、competition_records
テーブルに保存されます。
このテーブルは、各種試技結果や反省コメントなど、パワーリフティング競技の記録を管理するために使用されます。
competition_recordsテーブル用の入力フォームを5ステップに分割します。
- 検量体重入力フォーム
- スクワット試技結果入力フォーム
- ベンチプレス試技結果入力フォーム
- デッドリフト試技結果入力フォーム
- コメント試技結果入力フォーム
各ステップに対応するテーブルのカラムは以下の通りです。
ステップ構成と対応カラム
ステップ | 対応カラム |
---|---|
1.検量体重入力フォーム | - weight ("検量体重") |
2.スクワット試技結果入力フォーム | - squat_first_attempt ("第1試技重量") - squat_second_attempt ("第2試技重量") - squat_third_attempt ("第3試技重量") - squat_first_attempt_result ("第1試技判定結果") - squat_second_attempt_result ("第2試技判定結果") - squat_third_attempt_result ("第3試技判定結果") |
3.ベンチプレス試技結果入力フォーム | - benchpress_first_attempt ("第1試技重量") - benchpress_second_attempt ("第2試技重量") - benchpress_third_attempt ("第3試技重量") - benchpress_first_attempt_result ("第1試技判定結果") - benchpress_second_attempt_result ("第2試技判定結果") - benchpress_third_attempt_result ("第3試技判定結果") |
4.デッドリフト試技結果入力フォーム | - deadlift_first_attempt ("第1試技重量") - deadlift_second_attempt ("第2試技重量") - deadlift_third_attempt ("第3試技重量") - deadlift_first_attempt_result ("第1試技判定結果") - deadlift_second_attempt_result ("第2試技判定結果") - deadlift_third_attempt_result ("第3試技判定結果") |
5.コメント入力フォーム | - comment ("反省点コメント") |
実装条件
バリデーションと画面遷移
各フォームでは入力データのバリデーションを実行し、成功すれば次のフォームへ遷移します。バリデーションに失敗した場合は、ユーザーに再入力を促します。
入力データをデータベースへ保存するタイミング
各フォームの段階ではデータベースに保存せず、最後のステップが完了した時点で全てのデータを一括してデータベースに保存します。
おおまかな流れ
1. 検量体重入力フォーム
- ユーザーが入力後、「次の種目」へボタンを押す
- 入力したデータのバリデーションを実行
- 成功:スクワット入力フォームにリダイレクト
- 失敗:検量体重入力フォームに戻り再入力させる
2. スクワット試技結果入力フォーム
↓ ※ステップ1と同様の処理
3. ベンチプレス試技結果入力フォーム
↓ ※ステップ1と同様の処理
4. デッドリフト試技結果入力フォーム
↓ ※ステップ1と同様の処理
5. 振り返りコメント入力フォーム
- 入力したデータのバリデーションを実行
- 成功:ステップ1~5で取得したデータをデータベースへ一括保存
実装概要
概要で記載されたゴール・条件に向けて、おおまかな実装手順を以下に示します。
参考記事: ステップフォームの使い方
1. ステップ入力フォームの画面計画
2. UIのデザイン設計
4. 【Rails側】モデルの作成
5. 【Rails側】コントローラの作成
6. 【Rails側】ルーティングの設定
7. 【Rails側】ビューファイルの作成
実装詳細
※ 6.【Rails側】ルーティングの設定と7.【Rails側】ビューファイルの作成は詳細な実装説明は割愛致します。
1. ステップ入力フォームの画面計画
複数の入力項目をグループ分けし、ステップ数を決めます。
competition_records
テーブル用の入力フォームを5ステップに分割しました。
- 検量体重入力フォーム
- スクワット試技結果入力フォーム
- ベンチプレス試技結果入力フォーム
- デッドリフト試技結果入力フォーム
- 振り返りコメント入力フォーム
2. UIのデザイン設計
各ステップの画面設計をします。
意識したこと
-
⭐️完了までのステップと進捗状況を表示
ステップ入力フォームには重要だと思います。あとどれくらいか?今どこの入力か?という情報が一目でわかるので、ユーザーの心理的な負担が軽減されるようです。
参考記事: 8. 完了までのステップを表示 -
選択肢が少ない場合はラジオボタンにする
入力項目が多いのでいかにクリック数を減らすかを意識しています。試技判定結果(未試行or成功or失敗)を選択させる箇所はラジオボタン形式にし、ユーザービリティ向上を狙いました。
参考記事: ドロップダウンを多用しない -
バリデーションエラー表示は入力欄の近くに表示
管理ツールなので、データの整合性を担保するために一つの入力項目に対しバリデーションを何種類か定義しています。
何がどの様に間違っているかユーザーにわかりやすく伝えるために、エラーメッセージはページの上部に1箇所にまとめるのではなく、
入力フォームの近くに表示させました。
参考記事: エラー表示は入力欄の近くに表示
4. 【Rails側】モデルの作成
なぜモデルクラスを作成するか
各ステップの入力フォーム毎にデータのバリデーションを行いますが、この時点ではデータベースに保存はしません。
最後のステップまで正常に完了したあとに、今までのステップのデータを一括してデータベースに保存します。
そのため、各ステップの入力フォームは直接データベースに紐づいていません。
そこで、form_with
ヘルパーの:model
オプションを利用して、Active Record
以外のオブジェクトでもフォームを生成できるようにするために、ActiveModel::Model
をインクルードしたクラスを作成しました。
また、Active Record
のようなバリデーション機能を使いたいため、ActiveModel::Validations
もインクルードしました。
https://railsguides.jp/active_model_basics.html
各ステップ用のモデルを準備する
app/models/
配下にrecord
ディレクトリを作成。各ステップ入力フォームに対応したモデルはrecord
ディレクトリ配下に配置。
- ディレクトリ構成
app/models ├── record ├── weigh_in.rb # 検量体重入力フォーム用 ├── squat.rb # スクワット試技結果入力フォーム用 ├── bench_press.rb # ベンチプレス試技結果入力フォーム用 ├── deadlift.rb # デッドリフト試技結果入力フォーム用 └── comment.rb # 振り返りコメント入力フォーム用
各モデルのコード例
app/models/record/weigh_in.rb
module Record
class WeighIn
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :weight, :competition_id
validates :weight, presence: true, numericality: true
validate :unique_competition_id
private
def unique_competition_id
return unless CompetitionRecord.exists?(competition_id:)
errors.add(:competition_id, 'の試技結果はすでに存在しています')
end
end
end
app/models/record/squat.rb
※benchpress.rb, deadlift.rbはsquat.rbとコードが同じなので割愛します。
module Record
class Squat
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :squat_first_attempt,
:squat_second_attempt,
:squat_third_attempt,
:squat_first_attempt_result,
:squat_second_attempt_result,
:squat_third_attempt_result
# 初期化メソッド
def initialize(attributes = {})
super
@squat_first_attempt ||= nil
@squat_second_attempt ||= nil
@squat_third_attempt ||= nil
@squat_first_attempt_result ||= 'not_attempted'
@squat_second_attempt_result ||= 'not_attempted'
@squat_third_attempt_result ||= 'not_attempted'
end
# スクワット各試技のバリテーション設定
validates :squat_first_attempt, numericality: { allow_nil: true },
if: :should_validate_squat_first_attempt_numericality?
validates :squat_second_attempt, numericality: { allow_nil: true },
if: :should_validate_squat_second_attempt_numericality?
validates :squat_third_attempt, numericality: { allow_nil: true },
if: :should_validate_squat_third_attempt_numericality?
# スクワット試技判定結果のバリテーション設定
# 空は禁止
with_options presence: true do
validates :squat_first_attempt_result
validates :squat_second_attempt_result
validates :squat_third_attempt_result
end
# 重量の入力があったら成功か、失敗か選択させる
validate :squat_first_attempt_is_not_be_not_attempted, unless: :should_validate_squat_first_attempt_numericality?
validate :squat_second_attempt_is_not_be_not_attempted, unless: :should_validate_squat_second_attempt_numericality?
validate :squat_third_attempt_is_not_be_not_attempted, unless: :should_validate_squat_third_attempt_numericality?
# 成功か失敗選択時に重量が入力されていない
# スクワット
validate :squat_first_attempt_is_not_be_blank
validate :squat_second_attempt_is_not_be_blank
validate :squat_third_attempt_is_not_be_blank
private
# 重量の入力フォームに文字列が入力されていないか?
# 第一試技
def should_validate_squat_first_attempt_numericality?
squat_first_attempt.present? && Float(squat_first_attempt, exception: false).nil?
end
# 第二試技
def should_validate_squat_second_attempt_numericality?
squat_second_attempt.present? && Float(squat_second_attempt, exception: false).nil?
end
# 第三試技
def should_validate_squat_third_attempt_numericality?
squat_third_attempt.present? && Float(squat_third_attempt, exception: false).nil?
end
# カスタムバリデータ スクワット判定結果
# 第一試技
def squat_first_attempt_is_not_be_not_attempted
if squat_first_attempt.present? && Float(squat_first_attempt,
exception: false) >= 0 && squat_first_attempt_result == 'not_attempted'
errors.add(:squat_first_attempt_result, 'は成功か失敗かを選んでください')
end
end
# 第一試技
def squat_second_attempt_is_not_be_not_attempted
if squat_second_attempt.present? && Float(squat_second_attempt,
exception: false) >= 0 && squat_second_attempt_result == 'not_attempted'
errors.add(:squat_second_attempt_result, 'は成功か失敗かを選んでください')
end
end
# 第二試技
def squat_third_attempt_is_not_be_not_attempted
if squat_third_attempt.present? && Float(squat_third_attempt,
exception: false) >= 0 && squat_third_attempt_result == 'not_attempted'
errors.add(:squat_third_attempt_result, 'は成功か失敗かを選んでください')
end
end
# カスタムバリデータ 成功か失敗選択時に重量が入力されていない
# 第一試技
def squat_first_attempt_is_not_be_blank
if (squat_first_attempt_result == 'success' ||
squat_first_attempt_result == 'failure') &&
squat_first_attempt.blank?
errors.add(:squat_first_attempt, 'は成功か失敗を選択したときは重量を入力して下さい。')
end
end
# 第二試技
def squat_second_attempt_is_not_be_blank
if (squat_second_attempt_result == 'success' ||
squat_second_attempt_result == 'failure') &&
squat_second_attempt.blank?
errors.add(:squat_second_attempt, 'は成功か失敗を選択したときは重量を入力して下さい。')
end
end
# 第三試技
def squat_third_attempt_is_not_be_blank
if (squat_third_attempt_result == 'success' ||
squat_third_attempt_result == 'failure') &&
squat_third_attempt.blank?
errors.add(:squat_third_attempt, 'は成功か失敗を選択したときは重量を入力して下さい。')
end
end
end
end
app/models/record/comment.rb
module Record
class Comment
include ActiveModel::Model
include ActiveModel::Attributes
attribute :comment, :string, default: nil
end
end
5. 【Rails側】コントローラの作成
各モデルに対応したコントローラ準備する
app/controllers/
配下にrecord
ディレクトリを作成。各ステップ入力フォームに対応したコントローラはrecord
ディレクトリ配下に配置。
- ディレクトリ構成
app/controllers ├── record ├── weigh_ins_controller.rb # 検量体重入力フォーム用 ├── squats_controller.rb # スクワット試技結果入力フォーム用 ├── bench_presses_controller.rb # ベンチプレス試技結果入力フォーム用 ├── deadlifts_controller.rb # デッドリフト試技結果入力フォーム用 └── comments_controller.rb # 振り返りコメント入力フォーム用
各コントローラのコード例
-
1ステップ目
-
new
アクション
Record::WeighIn
クラスのインスタンスを生成し、新規検量体重入力フォームをレンダリング -
create
アクション
フォームから送信されたデータ(weigh_in_params
)を使って、Record::WeighIn
クラスのインスタンスを生成.- バリデーション実行
- バリデーション成功
-
session
に:record
キーを新規追加し、ユーザーが入力したデータを一時保存します。 - 次のステップ(スクワット試技結果入力フォーム)にリダイレクトします。
-
- バリデーション失敗
- 新規検量体重入力フォームを再表示し、バリデーションエラーメッセージを表示します。
- バリデーション成功
- バリデーション実行
コード例
app/controllers/record/weigh_ins_controller.rb
module Record class WeighInsController < ApplicationController def new @weigh_in = Record::WeighIn.new end def create @competition = current_user.competitions.find(params[:competition_id]) @weigh_in = Record::WeighIn.new(weigh_in_params) if @weigh_in.valid? session[:record] = { competition_id: @weigh_in.competition_id, weight: @weigh_in.weight } redirect_to new_competition_squat_path, success: t('.success') else flash.now[:danger] = t('.danger') render :new, status: :unprocessable_entity end end private def weigh_in_params params.require(:record_weigh_in).permit(:weight).merge(competition_id: params[:competition_id]) end end end
-
sessionメソッドを使う理由
HTTPはステートレスプロトコルであり、リクエストごとに状態が維持されません。
そのため、create アクションでバリデーション成功後に次のステップ入力フォームにリダイレクトすると、
フォームから送信されたデータは失われてしまいます。
1ステップ目から最後のステップまで受け取ったデータを保持するために、session を使用してデータを保存します。
-
2ステップ目
-
new
アクション
Record::Squat
クラスのインスタンスを生成し、新規スクワット入力フォームをレンダリング -
create
アクション
フォームから送信されたデータ(squat_params
)を使って、Record::Squat
クラスのインスタンスを生成.- バリデーション実行
- バリデーション成功
- 既存のセッションデータ
session[:record]
に試技結果入力データを追加します。 - 次のステップ(ベンチプレス試技結果入力フォーム)にリダイレクトします。
- 既存のセッションデータ
- バリデーション失敗
- 新規スクワット入力フォームを再表示し、バリデーションエラーメッセージを表示します。
- バリデーション成功
- バリデーション実行
コード例
app/controllers/record/squats_controller.rb
module Record class SquatsController < ApplicationController def new @squat = Record::Squat.new end def create @competition = current_user.competitions.find(params[:competition_id]) @squat = Record::Squat.new(squat_params) if @squat.valid? session[:record].merge!({ squat_first_attempt: @squat.squat_first_attempt, squat_second_attempt: @squat.squat_second_attempt, squat_third_attempt: @squat.squat_third_attempt, squat_first_attempt_result: @squat.squat_first_attempt_result, squat_second_attempt_result: @squat.squat_second_attempt_result, squat_third_attempt_result: @squat.squat_third_attempt_result }) redirect_to new_competition_bench_presse_path, success: t('.success') else flash.now[:danger] = t('.danger') render :new, status: :unprocessable_entity end end private def squat_params params.require(:record_squat).permit( :squat_first_attempt, :squat_first_attempt_result, :squat_second_attempt, :squat_second_attempt_result, :squat_third_attempt, :squat_third_attempt_result ) end end end
-
-
3ステップ目〜4ステップ目
※スクワット入力ステップと同じ流れなので割愛致します。
-
5ステップ目
-
new
アクション
Record::Comment
クラスのインスタンスを生成し、新規コメント入力フォームをレンダリング -
create
アクション
フォームから送信されたデータ(comment_params
)を使って、Record::Comment
クラスのインスタンスを生成.- バリデーション実行
- バリデーション成功
- 既存のセッションデータ
session[:record]
にコメント入力データを追加します。 -
competition_records
テーブルに、ステップ入力フォーム1~5で保存したセッションデータsession[:record]
を一括挿入します。
- 既存のセッションデータ
- バリデーション失敗
- 新規コメント入力フォームを再表示し、バリデーションエラーメッセージを表示します。
- バリデーション成功
- バリデーション実行
コード例
app/controllers/record/squats_controller.rb
module Record class CommentsController < ApplicationController def new @comment = Record::Comment.new end def create @competition = current_user.competitions.find(params[:competition_id]) @comment = Record::Comment.new(comment_params) if @comment.valid? session[:record].merge!({ comment: @comment.comment }) @competition_record_params = session[:record] # ここでcompetition_recordsテーブルに一括保存します CompetitionRecord.create!(@competition_record_params) # 一括保存ができたらsession[:record]の値を消します。 session.delete(:record) redirect_to competition_path(@competition), success: t('.success') else flash.now[:danger] = t('.danger') render :new, status: :unprocessable_entity end end private def comment_params params.require(:record_comment).permit(:comment) end end end
-
おわりに
以上、ステップ入力フォームの実装方法と、画面設計におけるUI/UX設計で考慮したポイントについてまとめました。
更なるユーザビリティ向上のために、最後の確認画面や戻るボタンの実装もしていきたいです。
入力項目が多い場合でも、ユーザービリティを向上させるためにステップ入力フォームを導入したいと考えている方のお役に立てれば幸いです。
参考文献
ステップ入力フォームとは?メリットとコンバージョン率をアップさせる理由
ステップフォームの利用とそのメリット
Railsガイド ActiveModelの基礎