Class: ShadcnViewComponent::FormBuilder

Inherits:
ActionView::Helpers::FormBuilder
  • Object
show all
Defined in:
lib/shadcn_view_component/form_builder.rb

Overview

Bridges the components to Rails forms.

shadcn's Field family is the right shape for this — label, control, description, error — but on its own it knows nothing about a model. This builder wires the two together: ids and names come from Rails, the error text and aria-invalid come from ActiveModel::Errors, and Field gets the data-invalid its styles key off.

<%= shadcn_form_with model: @user do |f| %>
<%= f.shadcn_field :email, label: "Email", description: "We never share it." do %>
  <%= f.shadcn_input :email, type: "email" %>
<% end %>
<% end %>

Or, when the control needs no arguments of its own:

<%= f.shadcn_input_field :email, label: "Email", type: "email" %>

Instance Method Summary collapse

Instance Method Details

#shadcn_calendar(method, mode: :single, to: nil, **options) ⇒ Object

A calendar bound to the model, in any of its three modes.

f.shadcn_calendar :starts_on
f.shadcn_calendar :dates, mode: :multiple
f.shadcn_calendar :starts_on, mode: :range, to: :ends_on

to: is what makes a range Rails-shaped: two dates are usually two columns, so the two ends are named after two attributes and each comes back as itself. Without it a range submits name[from] and name[to] under one parameter, which suits a form object rather than a record.

The month shown follows the value rather than today, so reopening a form lands on the date already chosen.



113
114
115
116
117
118
119
120
121
122
123
# File 'lib/shadcn_view_component/form_builder.rb', line 113

def shadcn_calendar(method, mode: :single, to: nil, **options)
  selected = calendar_selection(method, mode:, to:)

  @template.render(Shadcn::Calendar::Component.new(
    mode:,
    selected:,
    name: to ? [ field_name(method), field_name(to) ] : field_name(method),
    month: options.delete(:month) || Array(selected).compact.first,
    **aria_labelled(method, options)
  ))
end

#shadcn_checkbox(method, **options) ⇒ Object



125
126
127
128
129
130
131
# File 'lib/shadcn_view_component/form_builder.rb', line 125

def shadcn_checkbox(method, **options)
  @template.render(Shadcn::Checkbox::Component.new(
    name: field_name(method),
    checked: !!value_for(method),
    **aria_labelled(method, **control_options(method, **options))
  ))
end

#shadcn_field(method, label: nil, description: nil, orientation: :vertical, **options, &block) ⇒ Object

A labelled control with its description and any validation errors, wired together by id. Field styles itself from data-invalid, so that is set from the model rather than passed in.

Parameters:

  • method (Symbol)

    the attribute

  • label (String) (defaults to: nil)

    omit to leave the label out entirely

  • description (String) (defaults to: nil)

    hint text below the control

  • orientation (Symbol) (defaults to: :vertical)

    :vertical, :horizontal or :responsive



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/shadcn_view_component/form_builder.rb', line 30

def shadcn_field(method, label: nil, description: nil, orientation: :vertical, **options, &block)
  messages = errors_for(method)
  described[method.to_s] = description.present?

  @template.render(Shadcn::Field::Component.new(
    orientation:,
    "data-invalid": (true if messages.any?),
    **options
  )) do |field|
    field.with_label(id: label_id(method), for: field_id(method)) { label } if label
    field.with_field_content { @template.capture(&block) }
    field.with_description(id: description_id(method)) { description } if description
    field.with_error(id: error_id(method), errors: messages) if messages.any?
  end
end

#shadcn_input(method, **options) ⇒ Object



59
60
61
# File 'lib/shadcn_view_component/form_builder.rb', line 59

def shadcn_input(method, **options)
  @template.render(Shadcn::Input::Component.new(**control_options(method, **options)))
end

#shadcn_native_select(method, choices = [], **options) ⇒ Object

A real <select>: browser validation, autofill and keyboard behaviour for free. Prefer it over shadcn_select unless you need the styled listbox.



71
72
73
74
75
# File 'lib/shadcn_view_component/form_builder.rb', line 71

def shadcn_native_select(method, choices = [], **options)
  @template.render(Shadcn::NativeSelect::Component.new(**control_options(method, **options))) do
    @template.safe_join(Array(choices).map { |choice| native_option(method, choice) })
  end
end

#shadcn_radio_group(method, choices = [], **options) ⇒ Object

Each option is rendered as block content rather than through the item slot: the items have to sit inside their own label rows, and slot content is emitted before block content, which would pull them all out of place.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/shadcn_view_component/form_builder.rb', line 144

def shadcn_radio_group(method, choices = [], **options)
  selected = value_for(method).to_s

  @template.render(Shadcn::RadioGroup::Component.new(
    name: field_name(method), value: selected, **options
  )) do
    @template.safe_join(Array(choices).map { |choice|
      label, value = option_pair(choice)
      id = "#{field_id(method)}_#{value.parameterize.underscore}"

      @template.tag.div(class: "flex items-center gap-2") do
        @template.safe_join([
          @template.render(Shadcn::RadioGroup::Item::Component.new(
            value:, checked: value == selected, id:
          )),
          @template.render(Shadcn::Label::Component.new(for: id)) { label }
        ])
      end
    })
  end
end

#shadcn_select(method, choices = [], placeholder: nil, **options) ⇒ Object

The Radix-style listbox. It submits through a hidden input, so it has no browser validation — required will not stop the form.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/shadcn_view_component/form_builder.rb', line 79

def shadcn_select(method, choices = [], placeholder: nil, **options)
  selected = value_for(method).to_s

  @template.render(Shadcn::Select::Component.new(
    name: field_name(method), value: selected, placeholder:, **options
  )) do |select|
    select.with_trigger(**aria_labelled(method,
                                        id: field_id(method),
                                        placeholder: selected.blank?,
                                        "aria-invalid": invalid(method))) do |trigger|
      trigger.with_value(placeholder:) { label_for(choices, selected) }
    end
    select.with_select_content do
      @template.safe_join(Array(choices).map { |choice|
        label, value = option_pair(choice)
        @template.render(Shadcn::Select::Item::Component.new(value:, selected: value == selected)) { label }
      })
    end
  end
end

#shadcn_submit(text = nil, **options, &block) ⇒ Object



166
167
168
169
170
# File 'lib/shadcn_view_component/form_builder.rb', line 166

def shadcn_submit(text = nil, **options, &block)
  @template.render(Shadcn::Button::Component.new(type: "submit", **options)) do
    block ? @template.capture(&block) : (text || submit_default_value)
  end
end

#shadcn_switch(method, **options) ⇒ Object



133
134
135
136
137
138
139
# File 'lib/shadcn_view_component/form_builder.rb', line 133

def shadcn_switch(method, **options)
  @template.render(Shadcn::Switch::Component.new(
    name: field_name(method),
    checked: !!value_for(method),
    **aria_labelled(method, **control_options(method, **options))
  ))
end

#shadcn_textarea(method, **options) ⇒ Object



63
64
65
66
67
# File 'lib/shadcn_view_component/form_builder.rb', line 63

def shadcn_textarea(method, **options)
  @template.render(Shadcn::Textarea::Component.new(**control_options(method, **options))) do
    value_for(method).to_s
  end
end