Rails: マルチステップのフォームやウィザードをgemなしで構築する(翻訳)
フォームを簡潔にするとユーザーエクスペリエンス(UX)に好影響を与えられると述べている記事は、何年も前から多数公開されています。
- Best Practices For Mobile Form Design — Smashing Magazine
- LukeW | Web Form Design: Filling in the Blanks -a Web design & usability book by Luke Wroblewski
- 7 Basic Best Practices for Buttons :: UXmatters
- Placeholders in Form Fields Are Harmful - NN/G
フォームを簡潔にすることで、UXが改善されるのみならず、ユーザーから収集するデータ量も減らせるので、ユーザーとWeb運営側の双方にメリットがあります。
Railsでは、そのために長年wickedというgemが使われてきました。
しかし私に言わせれば、この種の機能はわざわざgemにアウトソースするほどのものではありません。さらに重要な点は、機能を手作りすることでコードが外部に依存しなくなって完全に自分たちのものになるので、アプリに合わせていくらでも調整が効くようになることです。
本記事では、アプリに新規ユーザーを登録するためのフォームをマルチステップ形式で作成する方法を解説します。具体的には以下のように動くフォームを作ります。
ようこそ画面以降のどの画面でも、入力フィールドは常に1個だけ表示されています(スキップも可能)。このフォームでは以下を実現したいと思います。
- ワークスペース名をユーザーに入力してもらう
- ユースケースをユーザーに入力してもらう(これに応じてダッシュボードに適切なダミーのサンプルテンプレートを表示するのに使える)
- ユーザーに同僚を招待してもらう
- 好みのテーマをユーザーに設定してもらう
必要なものが明確かつ実現しやすくなっていて、しかも要点を押さえていますね。
本記事で使うコードは以下のGitHubリポジトリにあります。本記事のコード例は省略されている部分があるので、他の重要な部分についてはぜひこちらのリポジトリでご確認ください。
Railsアプリは既に設定済みとします。このサンプルではTailwind CSSを使っていますが、Tailwind CSSは必須ではありません。
それではルーティングと基本的なコントローラを設定するところから始めましょう。
# config/routes.rb
resource :onboardings, path: "get-started", only: %w[show create]
# app/controllers/onboardings_controller.rb
class OnboardingsController < ApplicationController
def show
@onboarding = Onboarding.new
end
def create
Onboarding.new(onboarding_params).save
redirect_to root_path
end
end
ユーザーが必要としているのは、アプリに参加するためのオンボーディング処理だけなので、ここではこの単一リソースパターンが有効です。/get-started
というパスはユーザーにとってもわかりやすいURLになります。
🔗 データを保存・リダイレクトするためのForm Object
Form Objectは、ActiveModel::Model
モジュールをinclude
している他は普通のPORO(Plain Old Ruby Object)で、データベーステーブルに直接対応付けられていない複雑なフォームを構築するのに最適です。
# app/models/onboarding.rb
class Onboarding
include ActiveModel::Model
include ActiveModel::Attributes
include Onboarding::Steps
attribute :workspace_name, :string
attribute :use_case, :string
attribute :coworker_emails, :string
attribute :theme, :string
def save
return false if invalid?
ActiveRecord::Base.transaction do
workspace = create_workspace
add_usecase_to workspace
add_invitees_to workspace
add_theme_preference
end
end
private
def create_workspace
puts "Creating workspace: #{workspace_name}"
end
def add_usecase_to(workspace)
puts "Set Workspace template for use case: #{use_case}"
end
# ...
end
ActiveModel::Model
モジュールは、「バリデーション」「フォームヘルパー」といったRailsモデルのさまざまな機能を、データベーステーブル抜きで提供します。同モジュールの個別のメソッドが、データ処理の特定の部分を担当しているので、これらを活用することでコードを整理できます。
当たり前ですが、メソッドの処理結果を出力しただけで魔法のように動くわけではありません!😅
🔗 Onboading
クラスをconcernで拡張する
以下のStep
クラスによって、プロパティにドット.
記法でアクセス可能になり、dom_id
などの便利なRailsヘルパーとも調和します。
# app/models/onboarding/step.rb
class Onboarding::Step
include ActiveModel::Model
attr_accessor :id, :title, :description, :fields
def to_param = id
end
以下のSteps
モジュールは、ユーザーがアプリに参加するためのオンボーディング処理の全ステップを定義します。
# app/models/onboarding/steps.rb
module Onboarding::Steps
def steps
data.map { Onboarding::Step.new(it) }
end
private
def data
[
{
id: "welcome",
title: "Welcome to my app 🎉",
description: "In just a few steps we'll get your workspace up and running",
fields: []
},
{
id: "workspace",
title: "Workspace Name",
fields: [
{
name: :workspace_name,
label: "Workspace Name",
type: :text_field,
placeholder: "My Awesome Workspace"
}
]
}
# ...
]
end
end
この方法にしておくことで、ビューのコードを変更せずにステップを変更したり、新しいステップを追加する作業が簡単になります。
🔗 入力フィールドごとにパーシャルを作成する
ビューのコードを整理するため、フィールドタイプごとに専用のパーシャルを用意します。
<!-- app/views/onboardings/fields/_text_field.html.erb -->
<%= form.text_field field[:name], class: "w-full px-3 py-1 border border-gray-300 rounded-sm", placeholder: field[:placeholder] %>
<!-- app/views/onboardings/fields/_select.html.erb -->
<%= form.select field[:name], field[:options], { include_blank: field[:include_blank] }, { class: "w-full px-3 py-1 border border-gray-300 rounded-sm" } %>
🔗 すべてをまとめる
以上のすべての結果を、以下のメインのビューに集約します。
<!-- app/views/onboardings/show.html.erb (partial) -->
<main data-controller="onboarding">
<!-- 現在のステップの表示 -->
<%= form_with model: @onboarding do |form| %>
<% @onboarding.steps.each_with_index do |step, index| %>
<div
id="<%= dom_id(step) %>"
class="<%= class_names("grid place-items-center h-dvh max-w-3xl mx-auto", { hidden: index.zero? }) %>"
data-onboarding-target="step"
>
<div class="grid gap-3 text-center">
<h2 class="text-2xl font-semibold text-gray-900">
<%= step.title %>
</h2>
<!-- フィールドとボタンを表示 -->
</div>
</div>
<% end %>
<% end %>
</main>
このビューで使われているTailwindのh-dvh
クラスは、ビューポートの動的な高さを100%に設定することで、個別のステップを画面いっぱいに広げるのに使われています。
また、class_names
ヘルパーの使われ方にもご注目ください(詳しくは過去記事を参照)。
このフォームで利用しているOnboading
モデルは、フィールドを自動的に適切な属性に対応付けてくれます。さらに、この@onboarding.steps.each_with_index
というAPIの素晴らしさにもご注目ください(concernsありがとう!🧑🍳)。
🔗 ステップナビゲーション用のStimulusコントローラ
続いて、ステップナビゲーションと進捗インジケーターを管理するための小さなStimulusコントローラを作成しましょう。
// app/javascript/controllers/onboarding_controller.js
export default class extends Controller {
static targets = ["step", "indicator"]
static values = { step: { type: Number, default: 0 } }
stepValueChanged() {
this.#updateVisibleStep()
this.#updateIndicators()
}
continue(event) {
const currentStepId = event.currentTarget.dataset.step
const currentIndex = this.stepTargets.findIndex(step =>
step.id === `onboarding_step_${currentStepId}`
)
this.stepValue = currentIndex + 1
}
#updateVisibleStep() {
this.stepTargets.forEach((step, index) => {
const invisible = index !== this.stepValue
step.classList.toggle("hidden", invisible)
})
}
}
上のコードのstepValueChanged
コールバックは、ステップで値が変更されるたびに自動的に実行され、UIを更新します。Stimulusの「何かが変更されると呼び出されるコールバック」機能は過去記事でも取り上げたことがあり、イベントハンドリングを手動で行わずに済みます。
また、上のコードでは#
をプレフィックスすることでメソッドをprivateにできる機能を使っていますが、これについては私の近著『JavaScript for Rails Developer』で詳しく取り上げています。
ここまでできあがれば、残る作業はSignupsController#create
にredirect_to onboardings_path
を書くことだけです。🎉
他に追加するとよさそうな機能としては、バリデーションエラー時の表示を改善する(あるいは適切なデフォルト値を設定する)、ユーザーがワークスペースやユーザーがオンボーディング処理を完了したかどうかをトラッキングする(これにはrails_vault gemが便利です)、などが考えられます)。
以上でフォームは完成です!
すっきりと見栄えの良いオンボーディングフローができあがりました。このセットアップの素晴らしい点は、これらのファイルをコピーして必要な調整を加えるだけで、機能を自分たちの(次の)アプリに簡単に導入できることです。
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。