Skip to content

Commit adddba8

Browse files
committed
Support array values in checkbox component
Checkbox now detects three modes: - Boolean: field value is scalar, renders hidden+checkbox (unchanged) - Collection: field is inside a FieldCollection, renders with field value - Array: field value is an Array, computes checked from inclusion This lets users build checkbox groups with full HTML control: Role.all.each do |role| label do Field(:role_ids).checkbox(value: role.id) whitespace plain role.name end end
1 parent d072ab1 commit adddba8

3 files changed

Lines changed: 124 additions & 13 deletions

File tree

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -404,14 +404,15 @@ class SignupForm < Components::Form
404404
Field(:agreement).checkbox(checked: true)
405405
end
406406

407-
# Checkbox and radio groups use collections. The collection handles
408-
# naming (including []) and data binding. You control the HTML structure.
407+
# Checkbox groups: loop over all possible options. Superform handles
408+
# the name (with []), value, and checked state. You control the HTML.
409409
fieldset do
410-
legend { "Select your roles" }
411-
Field(:role_ids).collection.each do |role|
410+
legend { "Roles" }
411+
Role.all.each do |role|
412412
label do
413-
role.checkbox
414-
plain " #{role.value}"
413+
Field(:role_ids).checkbox(value: role.id)
414+
whitespace
415+
plain role.name
415416
end
416417
end
417418
end

lib/superform/rails/components/checkbox.rb

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,39 @@ module Rails
33
module Components
44
class Checkbox < Field
55
def view_template(&)
6-
# Rails has a hidden and checkbox input to deal with sending back a value
7-
# to the server regardless of if the input is checked or not.
8-
input(name: dom.name, type: :hidden, value: "0")
9-
# The hard coded keys need to be in here so the user can't overrite them.
10-
input(type: :checkbox, value: "1", **attributes)
6+
if boolean?
7+
# Rails convention: hidden input ensures a value is sent even when unchecked
8+
input(name: dom.name, type: :hidden, value: "0")
9+
input(type: :checkbox, value: "1", **attributes)
10+
elsif collection?
11+
input(type: :checkbox, value: dom.value, **attributes)
12+
else
13+
input(type: :checkbox, **attributes)
14+
end
1115
end
1216

1317
def field_attributes
14-
{ id: dom.id, name: dom.name, checked: field.value }
18+
if boolean?
19+
{ id: dom.id, name: dom.name, checked: field.value }
20+
elsif collection?
21+
{ id: dom.id, name: dom.name, checked: true }
22+
else
23+
{ id: dom.id, name: dom.array_name, checked: Array(field.value).include?(@attributes[:value]) }
24+
end
25+
end
26+
27+
private
28+
29+
# Inside a FieldCollection — the field is a child of another Field
30+
def collection?
31+
field.parent.is_a?(Superform::Field)
32+
end
33+
34+
# Scalar field with no explicit value — classic on/off toggle
35+
def boolean?
36+
!collection? && !field.value.is_a?(Array)
1537
end
1638
end
1739
end
1840
end
19-
end
41+
end
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
RSpec.describe Superform::Rails::Components::Checkbox, type: :view do
2+
describe 'boolean mode' do
3+
let(:object) { double('object', featured: featured_value) }
4+
let(:featured_value) { false }
5+
let(:field) do
6+
Superform::Rails::Field.new(:featured, parent: nil, object: object)
7+
end
8+
let(:component) { described_class.new(field, attributes: {}) }
9+
10+
subject { render(component) }
11+
12+
it 'renders hidden input and checkbox with boolean values' do
13+
expect(subject).to eq(
14+
'<input name="featured" type="hidden" value="0">' \
15+
'<input type="checkbox" value="1" id="featured" name="featured">'
16+
)
17+
end
18+
19+
context 'when checked' do
20+
let(:featured_value) { true }
21+
22+
it 'renders with checked attribute' do
23+
expect(subject).to match(/<input[^>]*type="checkbox"[^>]*checked/)
24+
end
25+
end
26+
27+
context 'when unchecked' do
28+
let(:featured_value) { false }
29+
30+
it 'renders without checked attribute' do
31+
expect(subject).not_to include('checked')
32+
end
33+
end
34+
end
35+
36+
describe 'collection mode' do
37+
let(:object) { double('object', role_ids: [1, 3]) }
38+
let(:field) do
39+
Superform::Rails::Field.new(:role_ids, parent: nil, object: object)
40+
end
41+
42+
it 'renders checked checkboxes for each value in the collection' do
43+
html = ""
44+
field.collection.each do |role|
45+
html += render(role.checkbox)
46+
end
47+
48+
expect(html).to include('value="1"')
49+
expect(html).to include('value="3"')
50+
expect(html).to include('name="role_ids[]"')
51+
expect(html).not_to include('type="hidden"')
52+
expect(html.scan(/checked/).count).to eq(2)
53+
end
54+
end
55+
56+
describe 'all-options mode' do
57+
let(:object) { double('object', role_ids: [1, 3]) }
58+
let(:field) do
59+
Superform::Rails::Field.new(:role_ids, parent: nil, object: object)
60+
end
61+
62+
it 'checks selected values and leaves others unchecked' do
63+
all_roles = [[1, "Admin"], [2, "Editor"], [3, "Viewer"]]
64+
html = ""
65+
all_roles.each do |id, _name|
66+
html += render(described_class.new(field, attributes: { value: id }))
67+
end
68+
69+
expect(html).to include('name="role_ids[]"')
70+
expect(html).not_to include('type="hidden"')
71+
# Only 1 and 3 are checked
72+
expect(html.scan(/checked/).count).to eq(2)
73+
expect(html).to match(/<input[^>]*checked[^>]*value="1"/)
74+
expect(html).to match(/<input[^>]*checked[^>]*value="3"/)
75+
expect(html).not_to match(/<input[^>]*checked[^>]*value="2"/)
76+
end
77+
78+
it 'works through the field helper' do
79+
html = render(field.checkbox(value: 1))
80+
expect(html).to include('name="role_ids[]"')
81+
expect(html).to include('checked')
82+
83+
html = render(field.checkbox(value: 2))
84+
expect(html).to include('name="role_ids[]"')
85+
expect(html).not_to include('checked')
86+
end
87+
end
88+
end

0 commit comments

Comments
 (0)