Skip to content

Commit fc58de2

Browse files
committed
Add enum auto-detection for radios/checkboxes and default label text
When radios() or checkboxes() is called with no arguments, auto-detect Rails enum options via Model.defined_enums and generate [key, humanized] pairs. Explicit options always take precedence. Also make choice.label without a block default to rendering choice.text.
1 parent 06b4170 commit fc58de2

5 files changed

Lines changed: 104 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
- **Radio and checkbox groups** via `field(:plan).radios(...)` and `field(:roles).checkboxes(...)`.
66
Accepts the same option formats as `select`. Each iteration yields a `Choice` with
7-
`.radio`, `.checkbox`, `.label`, `.value`, `.text`.
7+
`.radio`, `.checkbox`, `.label`, `.value`, `.text`. Auto-detects Rails enums when called
8+
with no arguments — options are generated from `Model.defined_enums` with humanized labels.
9+
`choice.label` without a block defaults to rendering `choice.text`.
810
- **Hash options** for `select`, `radios`, and `checkboxes` — e.g. `radios(1 => "Basic", 2 => "Pro")`.
911
- **Radio component** with `field(:gender).radio("male")` API. Automatically handles name, value, and checked state. Each radio gets a unique DOM id based on its value (e.g. `user_gender_male`).
1012
- **Checkbox collection support** — three modes:

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -384,23 +384,23 @@ class JobPostingForm < Components::Form
384384
Field(:remote_friendly).checkbox
385385
end
386386

387-
# Radio groups. Accepts all the same option formats as select:
388-
# arrays, single values, hashes, or ActiveRecord relations.
387+
# Radio group auto-detected from a Rails enum.
388+
# Given: enum :employment_type, full_time: 0, part_time: 1, contract: 2
389389
fieldset do
390390
legend { "Employment type" }
391-
field(:employment_type).radios(
392-
"full_time" => "Full-time",
393-
"part_time" => "Part-time",
394-
"contract" => "Contract"
395-
).each do |choice|
391+
field(:employment_type).radios.each do |choice|
396392
render choice.label {
397393
render choice.radio
398394
whitespace
399-
plain choice.text
395+
plain choice.text # "Full time", "Part time", "Contract"
400396
}
401397
end
402398
end
403399

400+
# You can also pass explicit options to override enum detection.
401+
# Accepts arrays, single values, hashes, or ActiveRecord relations.
402+
# field(:employment_type).radios("full_time" => "Full-time", ...)
403+
404404
# Checkbox groups. Same option formats, same Choice API.
405405
# Handles name[], checked state, and unique ids automatically.
406406
fieldset do

lib/superform/rails/choices.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ def checkbox(**attributes)
3232
@field.checkbox(value: @value, index: @index, **attributes)
3333
end
3434

35-
def label(**attributes, &)
36-
Components::Label.new(@field, for: DOM.join(@field.dom.id, @index), **attributes, &)
35+
def label(**attributes, &block)
36+
label_text = @text
37+
block ||= proc { label_text }
38+
Components::Label.new(@field, for: DOM.join(@field.dom.id, @index), **attributes, &block)
3739
end
3840
end
3941
end

lib/superform/rails/field.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,12 @@ def radio(value, index: value, **attributes)
148148
end
149149

150150
def radios(*options)
151+
options = enum_options if options.empty?
151152
Choices.new(field: self, options:)
152153
end
153154

154155
def checkboxes(*options)
156+
options = enum_options if options.empty?
155157
Choices.new(field: self, options:)
156158
end
157159

@@ -162,6 +164,17 @@ def checkboxes(*options)
162164
def title
163165
key.to_s.titleize
164166
end
167+
168+
private
169+
170+
def enum_options
171+
return [] unless object
172+
enums = object.class.try(:defined_enums)
173+
return [] unless enums
174+
enum = enums[key.to_s]
175+
return [] unless enum
176+
enum.keys.map { |k| [k, k.humanize] }
177+
end
165178
end
166179
end
167180
end

spec/superform/rails/choices_spec.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,82 @@
114114
end
115115
end
116116

117+
describe "enum auto-detection" do
118+
let(:enum_class) do
119+
Class.new do
120+
def self.defined_enums
121+
{ "status" => { "draft" => 0, "published" => 1, "archived" => 2 } }
122+
end
123+
124+
def self.try(method)
125+
send(method) if respond_to?(method)
126+
end
127+
end
128+
end
129+
let(:object) { enum_class.new.tap { |o| o.define_singleton_method(:status) { "published" } } }
130+
let(:field) { Superform::Rails::Field.new(:status, parent: nil, object: object) }
131+
132+
it "auto-detects enum options when no args given" do
133+
choices = field.radios
134+
values = choices.map { |c| [c.value, c.text] }
135+
expect(values).to eq([["draft", "Draft"], ["published", "Published"], ["archived", "Archived"]])
136+
end
137+
138+
it "checks the radio matching the current value" do
139+
html = ""
140+
field.radios.each do |choice|
141+
html += render(choice.radio)
142+
end
143+
144+
expect(html).to match(/<input[^>]*value="published"[^>]*checked/)
145+
expect(html).not_to match(/<input[^>]*value="draft"[^>]*checked/)
146+
end
147+
148+
it "uses explicit options when provided (skips enum)" do
149+
choices = field.radios("active", "inactive")
150+
values = choices.map { |c| [c.value, c.text] }
151+
expect(values).to eq([["active", "active"], ["inactive", "inactive"]])
152+
end
153+
154+
it "returns empty choices when field is not an enum" do
155+
non_enum_field = Superform::Rails::Field.new(:name, parent: nil, object: object)
156+
object.define_singleton_method(:name) { "test" }
157+
choices = non_enum_field.radios
158+
expect(choices.count).to eq(0)
159+
end
160+
161+
it "returns empty choices when object is nil" do
162+
nil_field = Superform::Rails::Field.new(:status, parent: nil, object: nil)
163+
choices = nil_field.radios
164+
expect(choices.count).to eq(0)
165+
end
166+
167+
it "works with checkboxes too" do
168+
choices = field.checkboxes
169+
values = choices.map { |c| [c.value, c.text] }
170+
expect(values).to eq([["draft", "Draft"], ["published", "Published"], ["archived", "Archived"]])
171+
end
172+
end
173+
174+
describe "default label text" do
175+
let(:object) { double("object", plan_id: 1) }
176+
let(:field) do
177+
Superform::Rails::Field.new(:plan_id, parent: nil, object: object)
178+
end
179+
180+
it "renders choice.text when label is called without a block" do
181+
html = ""
182+
field.radios([1, "Basic"], [2, "Pro"]).each do |choice|
183+
html += render(choice.label)
184+
end
185+
186+
expect(html).to include("Basic")
187+
expect(html).to include("Pro")
188+
expect(html).to include('for="plan_id_0"')
189+
expect(html).to include('for="plan_id_1"')
190+
end
191+
end
192+
117193
describe "is Enumerable" do
118194
let(:object) { double("object", status: "active") }
119195
let(:field) do

0 commit comments

Comments
 (0)