Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: マルチステップのフォームやウィザードをgemなしで構築する(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Rails: マルチステップのフォームやウィザードをgemなしで構築する(翻訳)

フォームを簡潔にするとユーザーエクスペリエンス(UX)に好影響を与えられると述べている記事は、何年も前から多数公開されています。

フォームを簡潔にすることで、UXが改善されるのみならず、ユーザーから収集するデータ量も減らせるので、ユーザーとWeb運営側の双方にメリットがあります。

Railsでは、そのために長年wickedというgemが使われてきました。

zombocom/wicked - GitHub

しかし私に言わせれば、この種の機能はわざわざgemにアウトソースするほどのものではありません。さらに重要な点は、機能を手作りすることでコードが外部に依存しなくなって完全に自分たちのものになるので、アプリに合わせていくらでも調整が効くようになることです。

本記事では、アプリに新規ユーザーを登録するためのフォームをマルチステップ形式で作成する方法を解説します。具体的には以下のように動くフォームを作ります。

ようこそ画面以降のどの画面でも、入力フィールドは常に1個だけ表示されています(スキップも可能)。このフォームでは以下を実現したいと思います。

  • ワークスペース名をユーザーに入力してもらう
  • ユースケースをユーザーに入力してもらう(これに応じてダッシュボードに適切なダミーのサンプルテンプレートを表示するのに使える)
  • ユーザーに同僚を招待してもらう
  • 好みのテーマをユーザーに設定してもらう

必要なものが明確かつ実現しやすくなっていて、しかも要点を押さえていますね。

本記事で使うコードは以下のGitHubリポジトリにあります。本記事のコード例は省略されている部分があるので、他の重要な部分についてはぜひこちらのリポジトリでご確認ください。

rails-designer-repos/multistep-form - 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#createredirect_to onboardings_pathを書くことだけです。🎉

他に追加するとよさそうな機能としては、バリデーションエラー時の表示を改善する(あるいは適切なデフォルト値を設定する)、ユーザーがワークスペースやユーザーがオンボーディング処理を完了したかどうかをトラッキングする(これにはrails_vault gemが便利です)、などが考えられます)。

rails-designer/rails_vault - GitHub

以上でフォームは完成です!
すっきりと見栄えの良いオンボーディングフローができあがりました。このセットアップの素晴らしい点は、これらのファイルをコピーして必要な調整を加えるだけで、機能を自分たちの(次の)アプリに簡単に導入できることです。

関連記事

Rails: Action Viewのdom_idヘルパーは実は有能(翻訳)

Rails: ビューをきれいに書くのに便利な知られざるヘルパー10種(翻訳)

Stimulus.JS: 値の変更時にトリガーされるコールバックをコントローラで利用する(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。