Simple Form

Railsアプリケーション向けの強力で柔軟なフォームビルダー

概要

Simple Formは、Ruby on Railsアプリケーション向けの強力なフォームビルダーです。フォーム作成を簡素化し、バリデーションの統合、CSSフレームワークとの連携、カスタマイズ可能なコンポーネントなど、豊富な機能を提供します。

主な特徴

  • 簡潔な構文: 少ないコードで複雑なフォームを作成
  • 自動バリデーション統合: Railsモデルのバリデーションを自動的に反映
  • CSSフレームワーク対応: Bootstrap、Foundation、Tailwind CSSなどと簡単に統合
  • 国際化対応: 多言語対応フォームの簡単な実装
  • カスタマイズ可能: 独自のフォームコンポーネントを作成可能
  • アクセシビリティ: WAI-ARIA属性の自動生成

インストール

Gemfileへの追加

gem 'simple_form'

バンドルインストール

bundle install

初期設定

# 基本的なインストール
rails generate simple_form:install

# Bootstrapを使用する場合
rails generate simple_form:install --bootstrap

# Foundation使用する場合
rails generate simple_form:install --foundation

基本的な使用方法

シンプルなフォームの作成

<%= simple_form_for @user do |f| %>
  <%= f.input :name %>
  <%= f.input :email %>
  <%= f.input :password %>
  <%= f.input :birthday, as: :date %>
  <%= f.button :submit %>
<% end %>

入力タイプの指定

<%= simple_form_for @article do |f| %>
  <%= f.input :title %>
  <%= f.input :content, as: :text %>
  <%= f.input :published, as: :boolean %>
  <%= f.input :category, collection: ['News', 'Blog', 'Tutorial'] %>
  <%= f.input :tags, as: :check_boxes, collection: Tag.all %>
<% end %>

バリデーション統合

モデルバリデーションの自動反映

# app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true, length: { minimum: 2, maximum: 50 }
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :age, numericality: { greater_than_or_equal_to: 18 }
end
<!-- バリデーションエラーが自動的に表示される -->
<%= simple_form_for @user do |f| %>
  <%= f.input :name, hint: '2文字以上50文字以下' %>
  <%= f.input :email %>
  <%= f.input :age, hint: '18歳以上である必要があります' %>
  <%= f.button :submit %>
<% end %>

カスタムバリデーションメッセージ

<%= f.input :username, 
    error: 'ユーザー名は既に使用されています',
    hint: '英数字とアンダースコアのみ使用可能' %>

フォームビルダーとヘルパー

利用可能な入力タイプ

<%= simple_form_for @product do |f| %>
  <!-- テキスト入力 -->
  <%= f.input :name %>
  
  <!-- テキストエリア -->
  <%= f.input :description, as: :text %>
  
  <!-- 数値入力 -->
  <%= f.input :price, as: :numeric %>
  
  <!-- セレクトボックス -->
  <%= f.input :category, collection: Category.all %>
  
  <!-- ラジオボタン -->
  <%= f.input :status, as: :radio_buttons, 
      collection: ['draft', 'published', 'archived'] %>
  
  <!-- チェックボックス(複数選択) -->
  <%= f.input :features, as: :check_boxes, 
      collection: Feature.all %>
  
  <!-- 日付選択 -->
  <%= f.input :release_date, as: :date %>
  
  <!-- ファイルアップロード -->
  <%= f.input :image, as: :file %>
  
  <!-- カラーピッカー -->
  <%= f.input :color, as: :color %>
  
  <!-- 範囲入力 -->
  <%= f.input :rating, as: :range, input_html: { min: 1, max: 5 } %>
<% end %>

アソシエーションの処理

<%= simple_form_for @post do |f| %>
  <%= f.input :title %>
  
  <!-- belongs_to アソシエーション -->
  <%= f.association :author %>
  
  <!-- has_many アソシエーション -->
  <%= f.association :categories, as: :check_boxes %>
  
  <!-- カスタムラベルとスコープ -->
  <%= f.association :tags, 
      label_method: :name,
      value_method: :id,
      include_blank: false,
      collection: Tag.active %>
<% end %>

カスタムフォームコンポーネント

カスタム入力の作成

# app/inputs/currency_input.rb
class CurrencyInput < SimpleForm::Inputs::Base
  def input(wrapper_options)
    merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
    
    template.content_tag(:div, class: 'input-group') do
      template.content_tag(:span, '¥', class: 'input-group-text') +
      @builder.text_field(attribute_name, merged_input_options)
    end
  end
end
<!-- カスタム入力の使用 -->
<%= f.input :price, as: :currency %>

カスタムラッパーの定義

# config/initializers/simple_form_custom.rb
SimpleForm.setup do |config|
  config.wrappers :custom_wrapper, tag: 'div', class: 'form-group' do |b|
    b.use :html5
    b.use :placeholder
    b.optional :maxlength
    b.optional :pattern
    b.optional :min_max
    b.optional :readonly
    
    b.use :label, class: 'form-label'
    b.use :input, class: 'form-control', error_class: 'is-invalid'
    b.use :error, wrap_with: { tag: 'div', class: 'invalid-feedback' }
    b.use :hint,  wrap_with: { tag: 'small', class: 'form-text text-muted' }
  end
end

Bootstrap統合

Bootstrap 5の設定

# config/initializers/simple_form_bootstrap.rb
SimpleForm.setup do |config|
  # Bootstrap 5の水平フォーム設定
  config.wrappers :horizontal_form, class: 'row mb-3' do |b|
    b.use :html5
    b.use :placeholder
    b.optional :maxlength
    b.optional :minlength
    b.optional :pattern
    b.optional :min_max
    b.optional :readonly
    
    b.use :label, class: 'col-sm-3 col-form-label'
    b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba|
      ba.use :input, class: 'form-control', error_class: 'is-invalid'
      ba.use :full_error, wrap_with: { class: 'invalid-feedback' }
      ba.use :hint, wrap_with: { class: 'form-text' }
    end
  end
end

Bootstrap形式のフォーム

<%= simple_form_for @user, wrapper: :horizontal_form do |f| %>
  <div class="card">
    <div class="card-body">
      <%= f.input :name %>
      <%= f.input :email %>
      <%= f.input :bio, as: :text, input_html: { rows: 4 } %>
      
      <div class="form-check form-switch">
        <%= f.input :newsletter, as: :boolean, 
            wrapper: :custom,
            input_html: { class: 'form-check-input' },
            label_html: { class: 'form-check-label' } %>
      </div>
    </div>
    
    <div class="card-footer">
      <%= f.button :submit, class: 'btn btn-primary' %>
      <%= link_to 'キャンセル', users_path, class: 'btn btn-secondary' %>
    </div>
  </div>
<% end %>

国際化(i18n)

ロケールファイルの設定

# config/locales/simple_form.ja.yml
ja:
  simple_form:
    "yes": 'はい'
    "no": 'いいえ'
    required:
      text: '必須'
      mark: '*'
    error_notification:
      default_message: "エラーを確認してください:"
    labels:
      user:
        name: '名前'
        email: 'メールアドレス'
        password: 'パスワード'
        remember_me: 'ログイン状態を保持'
    hints:
      user:
        name: 'フルネームを入力してください'
        email: '有効なメールアドレスを入力してください'
        password: '8文字以上で入力してください'
    placeholders:
      user:
        name: '例: 山田太郎'
        email: '例: [email protected]'

動的な国際化

<%= simple_form_for @user do |f| %>
  <%= f.input :name, label: t('activerecord.attributes.user.name') %>
  <%= f.input :role, 
      collection: User.roles.keys.map { |role| [t("user.roles.#{role}"), role] } %>
<% end %>

実践的な例

ユーザー登録フォーム

<div class="container">
  <div class="row justify-content-center">
    <div class="col-md-6">
      <h2>新規登録</h2>
      
      <%= simple_form_for @user, url: registration_path do |f| %>
        <%= f.error_notification %>
        <%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>
        
        <div class="form-inputs">
          <%= f.input :name, required: true, autofocus: true %>
          <%= f.input :email, required: true %>
          <%= f.input :password, required: true, hint: ("最低#{@minimum_password_length}文字" if @minimum_password_length) %>
          <%= f.input :password_confirmation, required: true %>
          
          <%= f.input :terms_of_service, as: :boolean, 
              label: '利用規約に同意する',
              error: '利用規約への同意が必要です' %>
        </div>
        
        <div class="form-actions">
          <%= f.button :submit, '登録する', class: 'btn btn-primary btn-block' %>
        </div>
      <% end %>
      
      <hr>
      
      <p class="text-center">
        既にアカウントをお持ちですか?
        <%= link_to 'ログイン', login_path %>
      </p>
    </div>
  </div>
</div>

検索フォーム

<%= simple_form_for :search, url: search_path, method: :get do |f| %>
  <div class="row g-3">
    <div class="col-md-4">
      <%= f.input :keyword, label: false, 
          placeholder: 'キーワードを入力',
          input_html: { value: params.dig(:search, :keyword) } %>
    </div>
    
    <div class="col-md-3">
      <%= f.input :category, 
          collection: Category.all,
          include_blank: '全てのカテゴリ',
          label: false,
          selected: params.dig(:search, :category) %>
    </div>
    
    <div class="col-md-3">
      <%= f.input :sort_by,
          collection: [['新着順', 'created_at'], ['人気順', 'popularity'], ['価格順', 'price']],
          label: false,
          selected: params.dig(:search, :sort_by) || 'created_at' %>
    </div>
    
    <div class="col-md-2">
      <%= f.button :submit, '検索', class: 'btn btn-primary w-100' %>
    </div>
  </div>
<% end %>

Ajax対応フォーム

<%= simple_form_for @comment, remote: true, html: { data: { type: 'json' } } do |f| %>
  <div id="comment-errors"></div>
  
  <%= f.input :content, as: :text, label: false, 
      placeholder: 'コメントを入力...',
      input_html: { rows: 3 } %>
  
  <div class="d-flex justify-content-between align-items-center">
    <div class="form-check">
      <%= f.input :anonymous, as: :boolean, 
          label: '匿名で投稿',
          wrapper: false,
          label_html: { class: 'form-check-label' },
          input_html: { class: 'form-check-input' } %>
    </div>
    
    <%= f.button :submit, '投稿', 
        class: 'btn btn-primary',
        data: { disable_with: '投稿中...' } %>
  </div>
<% end %>

<script>
document.addEventListener('turbo:load', () => {
  const form = document.querySelector('#new_comment');
  
  form.addEventListener('ajax:success', (event) => {
    const [data, status, xhr] = event.detail;
    // 成功時の処理
    form.reset();
  });
  
  form.addEventListener('ajax:error', (event) => {
    const [data, status, xhr] = event.detail;
    // エラー処理
    document.getElementById('comment-errors').innerHTML = 
      '<div class="alert alert-danger">エラーが発生しました</div>';
  });
});
</script>

高度な機能

条件付き表示

<%= simple_form_for @subscription do |f| %>
  <%= f.input :plan, as: :radio_buttons, 
      collection: ['basic', 'premium', 'enterprise'] %>
  
  <div id="billing-info" style="display: none;">
    <%= f.input :card_number %>
    <%= f.input :expiry_date %>
    <%= f.input :cvv %>
  </div>
  
  <script>
    document.querySelectorAll('input[name="subscription[plan]"]').forEach(radio => {
      radio.addEventListener('change', (e) => {
        const billingInfo = document.getElementById('billing-info');
        billingInfo.style.display = e.target.value !== 'basic' ? 'block' : 'none';
      });
    });
  </script>
<% end %>

ネストされたフォーム

<%= simple_form_for @project do |f| %>
  <%= f.input :name %>
  <%= f.input :description %>
  
  <h3>タスク</h3>
  <div id="tasks">
    <%= f.simple_fields_for :tasks do |task| %>
      <%= render 'task_fields', f: task %>
    <% end %>
  </div>
  
  <div class="links">
    <%= link_to_add_association 'タスクを追加', f, :tasks, 
        class: 'btn btn-sm btn-secondary' %>
  </div>
  
  <%= f.button :submit %>
<% end %>

<!-- _task_fields.html.erb -->
<div class="nested-fields">
  <%= f.input :title %>
  <%= f.input :completed, as: :boolean %>
  <%= link_to_remove_association "削除", f, class: 'btn btn-sm btn-danger' %>
</div>

パフォーマンス最適化

フォームオブジェクトパターン

# app/forms/user_registration_form.rb
class UserRegistrationForm
  include ActiveModel::Model
  
  attr_accessor :name, :email, :password, :password_confirmation, :terms_of_service
  
  validates :name, presence: true
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true, confirmation: true, length: { minimum: 8 }
  validates :terms_of_service, acceptance: true
  
  def save
    return false unless valid?
    
    user = User.new(user_params)
    if user.save
      UserMailer.welcome(user).deliver_later
      true
    else
      errors.merge!(user.errors)
      false
    end
  end
  
  private
  
  def user_params
    { name: name, email: email, password: password }
  end
end
<%= simple_form_for @registration_form, url: registrations_path do |f| %>
  <%= f.input :name %>
  <%= f.input :email %>
  <%= f.input :password %>
  <%= f.input :password_confirmation %>
  <%= f.input :terms_of_service, as: :boolean %>
  <%= f.button :submit, '登録' %>
<% end %>

キャッシュの活用

<% cache [@user, 'edit_form'] do %>
  <%= simple_form_for @user do |f| %>
    <!-- フォーム内容 -->
  <% end %>
<% end %>

トラブルシューティング

よくある問題と解決策

  1. バリデーションエラーが表示されない

    # コントローラーでエラーを保持
    def create
      @user = User.new(user_params)
      if @user.save
        redirect_to @user
      else
        render :new # renderを使用してエラーを保持
      end
    end
    
  2. カスタムラッパーが適用されない

    <!-- wrapper オプションを明示的に指定 -->
    <%= f.input :name, wrapper: :custom_wrapper %>
    
  3. 日本語化が反映されない

    # config/application.rb
    config.i18n.default_locale = :ja
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.yml')]
    

まとめ

Simple Formは、Railsアプリケーションでのフォーム作成を大幅に簡素化する強力なツールです。バリデーションの自動統合、多様な入力タイプ、CSSフレームワークとの連携など、開発効率を向上させる多くの機能を提供します。適切に設定・カスタマイズすることで、保守性の高い美しいフォームを効率的に実装できます。