Skip to content

Commit 74aa973

Browse files
committed
Add Radio component with collection support
Radio buttons now bind to field values automatically: # Single radio Field(:gender).radio("male") # checked when field.value == "male" # Collection of options Field(:status).radio("active", "inactive", "pending").each do |status| label do status.radio whitespace plain status.value.humanize end end RadioCollection reuses OptionMapper for consistent option handling across select, checkbox, and radio components.
1 parent adddba8 commit 74aa973

6 files changed

Lines changed: 171 additions & 5 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,19 @@ class SignupForm < Components::Form
417417
end
418418
end
419419

420+
# Radio groups: pass options to radio and iterate. Superform handles
421+
# the name, value, and checked state automatically.
422+
fieldset do
423+
legend { "Status" }
424+
Field(:status).radio(User.statuses).each do |status|
425+
label do
426+
status.radio
427+
whitespace
428+
plain status.value.humanize
429+
end
430+
end
431+
end
432+
420433
render button { "Submit" }
421434
end
422435
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module Superform
2+
module Rails
3+
module Components
4+
class Radio < Field
5+
def initialize(field, value:, index: nil, attributes: {})
6+
super(field, attributes: attributes)
7+
@value = value
8+
@index = index
9+
end
10+
11+
def view_template(&)
12+
input(type: :radio, **attributes)
13+
end
14+
15+
def field_attributes
16+
id = @index ? "#{dom.id}_#{@index}" : dom.id
17+
{ id: id, name: dom.name, value: @value, checked: field.value == @value }
18+
end
19+
end
20+
end
21+
end
22+
end

lib/superform/rails/field.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,15 @@ def file(*, **)
143143
input(*, **, type: :file)
144144
end
145145

146-
def radio(value, *, **)
147-
input(*, **, type: :radio, value: value)
146+
def radio(*args, **attributes)
147+
if args.length == 1 && !args.first.is_a?(Array)
148+
# Single scalar value: Field(:gender).radio("male")
149+
Components::Radio.new(field, value: args.first, attributes: attributes)
150+
else
151+
# Collection of options: Field(:status).radio("active", "inactive", "pending")
152+
# or Field(:status).radio(["active", "Active"], ["inactive", "Inactive"])
153+
RadioCollection.new(field: field, options: args)
154+
end
148155
end
149156

150157
# Rails compatibility aliases
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
module Superform
2+
module Rails
3+
class RadioCollection
4+
include Enumerable
5+
6+
def initialize(field:, options:)
7+
@field = field
8+
@options = options
9+
@index = 0
10+
end
11+
12+
def each
13+
OptionMapper.new(@options).each do |value, label|
14+
@index += 1
15+
yield RadioOption.new(field: @field, value: value, label: label, index: @index)
16+
end
17+
end
18+
end
19+
20+
class RadioOption
21+
attr_reader :value, :label
22+
23+
def initialize(field:, value:, label:, index:)
24+
@field = field
25+
@value = value
26+
@label = label
27+
@index = index
28+
end
29+
30+
def radio(**attributes)
31+
Components::Radio.new(@field, value: @value, index: @index, attributes: attributes)
32+
end
33+
end
34+
end
35+
end
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
RSpec.describe Superform::Rails::Components::Radio, type: :view do
2+
describe 'single radio' do
3+
let(:object) { double('object', gender: "male") }
4+
let(:field) do
5+
Superform::Rails::Field.new(:gender, parent: nil, object: object)
6+
end
7+
8+
it 'renders a checked radio when value matches' do
9+
html = render(field.radio("male"))
10+
expect(html).to include('type="radio"')
11+
expect(html).to include('name="gender"')
12+
expect(html).to include('value="male"')
13+
expect(html).to include('checked')
14+
end
15+
16+
it 'renders an unchecked radio when value does not match' do
17+
html = render(field.radio("female"))
18+
expect(html).to include('value="female"')
19+
expect(html).not_to include('checked')
20+
end
21+
22+
it 'accepts custom attributes' do
23+
html = render(field.radio("male", class: "radio-input"))
24+
expect(html).to include('class="radio-input"')
25+
end
26+
end
27+
28+
describe 'radio collection' do
29+
let(:object) { double('object', status: "active") }
30+
let(:field) do
31+
Superform::Rails::Field.new(:status, parent: nil, object: object)
32+
end
33+
34+
it 'returns an enumerable' do
35+
collection = field.radio("active", "inactive", "pending")
36+
expect(collection).to respond_to(:each)
37+
end
38+
39+
it 'yields options with value, label, and radio' do
40+
options = field.radio(["active", "Active"], ["inactive", "Inactive"]).to_a
41+
expect(options.length).to eq(2)
42+
expect(options.first.value).to eq("active")
43+
expect(options.first.label).to eq("Active")
44+
expect(options.first).to respond_to(:radio)
45+
end
46+
47+
it 'renders radios with correct checked state' do
48+
html = ""
49+
field.radio(["active", "Active"], ["inactive", "Inactive"], ["pending", "Pending"]).each do |status|
50+
html += render(status.radio)
51+
end
52+
53+
expect(html.scan(/type="radio"/).count).to eq(3)
54+
expect(html.scan(/name="status"/).count).to eq(3)
55+
expect(html.scan(/checked/).count).to eq(1)
56+
expect(html).to match(/<input[^>]*value="active"[^>]*checked/)
57+
expect(html).not_to match(/<input[^>]*value="inactive"[^>]*checked/)
58+
expect(html).not_to match(/<input[^>]*value="pending"[^>]*checked/)
59+
end
60+
61+
it 'generates unique IDs for each radio' do
62+
html = ""
63+
field.radio("active", "inactive").each do |status|
64+
html += render(status.radio)
65+
end
66+
67+
expect(html).to include('id="status_1"')
68+
expect(html).to include('id="status_2"')
69+
end
70+
71+
it 'works with the label pattern from the docs' do
72+
html = ""
73+
field.radio(["active", "Active"], ["inactive", "Inactive"]).each do |status|
74+
# Simulating: label { status.radio; whitespace; plain status.value.humanize }
75+
html += render(status.radio)
76+
end
77+
78+
expect(html).to include('value="active"')
79+
expect(html).to include('value="inactive"')
80+
expect(html.scan(/checked/).count).to eq(1)
81+
end
82+
83+
it 'works with single-value options (value used as label)' do
84+
options = field.radio("active", "inactive").to_a
85+
expect(options.first.value).to eq("active")
86+
expect(options.first.label).to eq("active")
87+
end
88+
end
89+
end

spec/superform/rails/field_convenience_methods_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
it { expect(field.color.type).to eq("color") }
2626
it { expect(field.search.type).to eq("search") }
2727
it { expect(field.file.type).to eq("file") }
28-
it { expect(field.radio("male").type).to eq("radio") }
28+
it { expect(field.radio("male")).to be_a(Superform::Rails::Components::Radio) }
2929
end
3030

3131
describe "Rails compatibility aliases" do
@@ -56,8 +56,8 @@
5656
end
5757

5858
it "handles radio button value parameter correctly" do
59-
component = field.radio("female", class: "radio-input", data: { value: "f" })
60-
expect(component.type).to eq("radio")
59+
component = field.radio("female", class: "radio-input")
60+
expect(component).to be_a(Superform::Rails::Components::Radio)
6161
end
6262
end
6363
end

0 commit comments

Comments
 (0)