Module: Protege::Components::FormsHelper

Included in:
ComponentsHelper
Defined in:
app/helpers/protege/components/forms_helper.rb

Overview

Form-building UI elements shared across engine views.

Split out of ComponentsHelper so each module stays focused; FormsHelper owns the input styling constants and the field/select/checkbox builders that wrap Rails form helpers in Protege's design system.

Colors use CSS custom properties (--surface, --border, --text, --text-faint) defined in tailwind/application.css so dark mode works via a single .dark class toggle.

Constant Summary collapse

FORM_INPUT =

Tailwind classes applied to text inputs, selects, and textareas.

'block w-full rounded-sm px-2.5 py-1.5 text-sm shadow-sm ' \
'focus:border-amber-500 focus:outline-none focus:ring-1 focus:ring-amber-500'
FORM_LABEL =

Tailwind classes applied to field labels.

'block text-sm font-medium mb-1'
FORM_CHECKBOX =

Tailwind classes applied to checkboxes.

'h-4 w-4 rounded text-amber-600 focus:ring-amber-500'

Instance Method Summary collapse

Instance Method Details

#form_checkbox(form, field, label: nil) ⇒ ActiveSupport::SafeBuffer

Render a labelled checkbox wired to a form builder.

Parameters:

  • form (ActionView::Helpers::FormBuilder)

    the form builder

  • field (Symbol)

    the attribute the checkbox edits

  • label (String, nil) (defaults to: nil)

    the label text; defaults to the attribute's humanized name

Returns:

  • (ActiveSupport::SafeBuffer)

    the checkbox markup



123
124
125
126
127
128
# File 'app/helpers/protege/components/forms_helper.rb', line 123

def form_checkbox(form, field, label: nil)
  tag.div(class: 'flex items-center gap-3') do
    form.check_box(field, class: FORM_CHECKBOX, style: 'border: 1px solid var(--border)') +
      form.label(field, label, class: 'text-sm font-medium')
  end
end

#form_checklist(form, field, collection, label: nil, hint: nil, value: :to_s, text: :to_s) ⇒ ActiveSupport::SafeBuffer

Render a labelled checklist — one checkbox per item, backed by an array attribute, wired to a form builder. Rails' collection_check_boxes emits the hidden blank field for us, so unchecking every box submits an empty array (clearing the attribute) instead of omitting it.

Parameters:

  • form (ActionView::Helpers::FormBuilder)

    the form builder

  • field (Symbol)

    the array attribute the checklist edits

  • collection (Enumerable)

    the items, one checkbox each

  • label (String, nil) (defaults to: nil)

    the group label; omitted when nil

  • hint (String, nil) (defaults to: nil)

    optional helper text shown beneath the label

  • value (Symbol, Proc) (defaults to: :to_s)

    maps an item to its submitted value (default: :to_s)

  • text (Symbol, Proc) (defaults to: :to_s)

    maps an item to its checkbox label (default: :to_s)

Returns:

  • (ActiveSupport::SafeBuffer)

    the checklist markup



142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'app/helpers/protege/components/forms_helper.rb', line 142

def form_checklist(form, field, collection, label: nil, hint: nil, value: :to_s, text: :to_s)
  tag.div do
    (label ? tag.label(label, class: FORM_LABEL) : ''.html_safe) +
      form_hint(hint) +
      tag.div(class: 'space-y-2 pt-1') do
        form.collection_check_boxes(field, collection, value, text) do |b|
          b.label(class: 'flex items-center gap-3 text-sm') do
            b.check_box(class: FORM_CHECKBOX, style: 'border: 1px solid var(--border)') + b.text
          end
        end
      end
  end
end

#form_email_address(record, domains) ⇒ ActiveSupport::SafeBuffer

Render the split prefix/domain email-address control for a persona.

When domains exist, render a prefix text box, an @ separator, and a domain select preselected to the record's current domain; otherwise prompt the operator to add a domain first.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    the email-address control markup



87
88
89
90
91
92
93
94
# File 'app/helpers/protege/components/forms_helper.rb', line 87

def form_email_address(record, domains)
  tag.div do
    tag.label('Email address', class: FORM_LABEL) +
      tag.div(class: 'flex items-center gap-2') do
        domains.any? ? email_address_fields(record, domains) : email_address_empty
      end
  end
end

#form_errors(record) ⇒ ActiveSupport::SafeBuffer

Render a record's validation errors as a styled list, or nothing when valid.

Parameters:

  • record (ActiveModel::Validations)

    the record whose errors to render

Returns:

  • (ActiveSupport::SafeBuffer)

    the error block, or an empty buffer



34
35
36
37
38
39
40
41
42
# File 'app/helpers/protege/components/forms_helper.rb', line 34

def form_errors(record)
  return ''.html_safe unless record.errors.any?

  tag.div(class: 'rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700') do
    tag.ul(class: 'list-disc pl-4 space-y-1') do
      safe_join(record.errors.full_messages.map { |message| tag.li(message) })
    end
  end
end

#form_field(form, field, label: nil, hint: nil, **input_attrs) ⇒ ActiveSupport::SafeBuffer

Render a labelled text field with an optional hint, wired to a form builder.

Parameters:

  • form (ActionView::Helpers::FormBuilder)

    the form builder

  • field (Symbol)

    the attribute the field edits

  • label (String, nil) (defaults to: nil)

    the label text; defaults to the attribute's humanized name

  • hint (String, nil) (defaults to: nil)

    optional helper text shown beneath the field

  • input_attrs (Hash)

    extra HTML attributes forwarded to the input

Returns:

  • (ActiveSupport::SafeBuffer)

    the field markup



52
53
54
55
56
57
58
# File 'app/helpers/protege/components/forms_helper.rb', line 52

def form_field(form, field, label: nil, hint: nil, **input_attrs)
  tag.div do
    form.label(field, label, class: FORM_LABEL) +
      form.text_field(field, class: FORM_INPUT, style: form_input_style, **input_attrs) +
      form_hint(hint)
  end
end

#form_input_styleString

Build the inline style applied to form controls so they track the theme.

Returns:

  • (String)

    the CSS custom-property-backed style string



26
27
28
# File 'app/helpers/protege/components/forms_helper.rb', line 26

def form_input_style
  'background: var(--surface); border: 1px solid var(--border); color: var(--text)'
end

#form_select(form, field, choices, label: nil, prompt: nil, selected: nil, **attrs) ⇒ ActiveSupport::SafeBuffer

Render a labelled select wired to a form builder.

Parameters:

  • form (ActionView::Helpers::FormBuilder)

    the form builder

  • field (Symbol)

    the attribute the select edits

  • choices (Object)

    the option set passed to FormBuilder#select

  • label (String, nil) (defaults to: nil)

    the label text; defaults to the attribute's humanized name

  • prompt (String, nil) (defaults to: nil)

    optional blank-choice prompt

  • selected (String, nil) (defaults to: nil)

    the value to preselect — needed on a modelless form (e.g. a GET search form) where the builder has no object to read the current value from

  • attrs (Hash)

    extra HTML attributes forwarded to the select

Returns:

  • (ActiveSupport::SafeBuffer)

    the select markup



71
72
73
74
75
76
# File 'app/helpers/protege/components/forms_helper.rb', line 71

def form_select(form, field, choices, label: nil, prompt: nil, selected: nil, **attrs)
  tag.div do
    form.label(field, label, class: FORM_LABEL) +
      form.select(field, choices, { prompt:, selected: }, class: FORM_INPUT, style: form_input_style, **attrs)
  end
end

#form_text_area(form, field, label: nil, hint: nil, fill: false, **input_attrs) ⇒ ActiveSupport::SafeBuffer

Render a labelled textarea with an optional hint, wired to a form builder.

Parameters:

  • form (ActionView::Helpers::FormBuilder)

    the form builder

  • field (Symbol)

    the attribute the textarea edits

  • label (String, nil) (defaults to: nil)

    optional label text; omitted when nil

  • hint (String, nil) (defaults to: nil)

    optional helper text shown beneath the textarea

  • fill (Boolean) (defaults to: false)

    when true, the wrapper and textarea flex to fill the height of a flex-column parent (e.g. a resizable reply composer) instead of using the default fixed-minimum, self-resizable size

  • input_attrs (Hash)

    extra HTML attributes forwarded to the textarea

Returns:

  • (ActiveSupport::SafeBuffer)

    the textarea markup



106
107
108
109
110
111
112
113
114
115
# File 'app/helpers/protege/components/forms_helper.rb', line 106

def form_text_area(form, field, label: nil, hint: nil, fill: false, **input_attrs)
  input_attrs[:rows] ||= 3
  size_class           = fill ? 'flex-1 min-h-0 resize-none' : 'resize-y min-h-[6rem]'
  tag.div(class: fill ? 'flex min-h-0 flex-1 flex-col' : nil) do
    (label ? form.label(field, label, class: FORM_LABEL) : ''.html_safe) +
      form.text_area(field, class: "#{FORM_INPUT} font-mono #{size_class}",
                            style: form_input_style, **input_attrs) +
      form_hint(hint)
  end
end