ComponentsThemes
Palette
Default
Stone
Rose
Blue
Green
Violet
Yellow
Orange
6

← Components

Form

Wrapper over Rails form_with with FormField, FormLabel, FormDescription, FormMessage.

Installation

bin/rails g wabi:add form

Form wraps Rails' form_with and yields the form builder so you can keep using form.email_field, form.text_area, etc. with full ActiveModel / ActiveRecord integration.

Example

Try submitting with an invalid email vs a valid one (e.g. you@example.com). The demo controller intercepts submit so the docs page never reloads.

Source

app/components/ui/form.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    # Wrapper over Rails' form_with. Yields the form builder so apps continue
    # using Rails form helpers (form.email_field, form.text_area, etc.) for
    # idiomatic ActiveModel/ActiveRecord integration.
    #
    # Requires a Rails view context — uses the Phlex::Rails::Helpers::FormWith
    # adapter (the recommended pattern in phlex-rails 2.4+).
    class Form < Wabi::Base
      include Phlex::Rails::Helpers::FormWith if defined?(Phlex::Rails::Helpers::FormWith)

      def initialize(model: nil, url: nil, scope: nil, **opts)
        @model = model
        @url   = url
        @scope = scope
        @opts  = opts
      end

      def view_template(&block)
        user_class = @opts.delete(:class)
        form_with(
          model: @model, url: @url, scope: @scope, **@opts,
          class: merge_class("space-y-6", user_class)
        ) do |form|
          yield(form) if block_given?
        end
      end
    end
  end
end

app/components/ui/form_field.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class FormField < Wabi::Base
      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(**@attrs, class: merge_class("space-y-2", user_class)) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/form_label.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class FormLabel < Wabi::Base
      LABEL_CLASS = "text-sm font-medium leading-none " \
                    "peer-disabled:cursor-not-allowed peer-disabled:opacity-70"

      # `for_:` matches the standalone Components::UI::Label convention.
      def initialize(for_: nil, **attrs)
        @for_  = for_
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        label(
          **@attrs,
          for: @for_,
          class: merge_class(LABEL_CLASS, user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/form_description.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    # Renders helper/hint text for a form field.
    #
    # Accessible-name convention (WCAG 1.3.1):
    #   Pass `id: "#{field_name}_description"` and wire the adjacent input with
    #   `aria-describedby: "#{field_name}_description"` so assistive technology
    #   announces the description when the input is focused.
    #
    #   Example:
    #     render FormDescription.new(id: "email_description") { "We'll never share your email." }
    #     # input: aria_describedby: "email_description"
    class FormDescription < Wabi::Base
      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        p(**@attrs, class: merge_class("text-sm text-muted-foreground", user_class)) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/form_message.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    # Renders a field-level validation error message.
    #
    # Accessible-name convention (WCAG 1.3.1 / 4.1.3):
    #   Pass `id: "#{field_name}_error"` so the adjacent input can reference it via
    #   `aria-describedby: "#{field_name}_error"`. This allows assistive technology
    #   to announce the error when the input is focused.
    #
    #   Example:
    #     render FormMessage.new(model: @user, field: :email, id: "email_error")
    #     # input: aria_describedby: "email_error", aria_invalid: true
    #
    # aria-invalid convention (WCAG 1.3.1):
    #   When FormMessage renders for a field (indicating invalidity), the corresponding
    #   input MUST carry `aria-invalid: true`. FormMessage itself emits role="alert" to
    #   announce the error via live region, but the input's aria-invalid state must be
    #   set by the caller since FormMessage has no direct reference to the input element.
    class FormMessage < Wabi::Base
      def initialize(model: nil, field: nil, text: nil, **attrs)
        @model = model
        @field = field
        @text  = text
        @attrs = attrs
      end

      def view_template
        msg = @text || (@model && @field && @model.errors[@field].first)
        return unless msg

        user_class = @attrs.delete(:class)
        # role="alert" so screen readers announce the validation error when it
        # appears; callers can override via attrs (e.g. role: "status").
        p(role: "alert", **@attrs, class: merge_class("text-sm font-medium text-destructive", user_class)) { msg }
      end
    end
  end
end

Accessibility

  • FormLabel uses for= to associate with the input id.
  • FormDescription is intended to be referenced via aria-describedby on the input (app wiring).
  • FormMessage is intended to be referenced via aria-describedby on the input when an error is present.
  • Form yields the standard Rails form builder so all native input helpers (email_field, text_area, etc.) keep working.