Skip to content

Commit e786bfe

Browse files
committed
Consolidate unique_id and unique_alpine_store_key into instance_key
The two params were doing overlapping namespacing work (DOM ids and Alpine store keys) for the same purpose: letting multiple instances of a component coexist on a single page. Collapse to one param across RadioButtonsComponent, CheckboxesComponent, and ConditionalComponent.
1 parent 7118022 commit e786bfe

12 files changed

Lines changed: 70 additions & 59 deletions

app/components/checkboxes_component.html.erb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@
99
"x-model" => "$store.#{store_key}",
1010
:class => "cfa-checkbox #{box_classes} rounded #{border_class}"
1111
}
12-
if @unique_id
13-
input_id = "#{@form.object_name}_#{@method}_#{item_value}_#{@unique_id}"
12+
if @instance_key.present?
13+
input_id = "#{@form.object_name}_#{@method}_#{item_value}_#{@instance_key}"
1414
checkbox_attrs[:id] = input_id
1515
end
1616
checkbox_attrs["x-init"] = "$nextTick(() => $el.indeterminate = true)" if item_state == :indeterminate
1717
checkbox_attrs[:disabled] = true if item_state == :disabled
1818
content_tag :div, class: "checkbox_item" do
1919
concat checkbox_wrap(checkbox_item.checkbox(checkbox_attrs), small: @small)
2020
label_opts = {}
21-
label_opts[:for] = input_id if @unique_id
21+
label_opts[:for] = input_id if @instance_key.present?
2222
label_opts[:class] = "text-text-disabled" if item_state == :disabled
2323
concat(label_opts.empty? ? checkbox_item.label : checkbox_item.label(label_opts))
2424
end

app/components/checkboxes_component.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
class CheckboxesComponent < AttributeBoundFormElementComponent
44
ALLOWED_ITEM_STATES = [:disabled, :indeterminate].freeze
55

6-
def initialize(form:, method:, collection:, item_value_method:, item_label_method:, small: false, warning_message: nil, item_states: {}, unique_id: nil)
6+
# instance_key namespaces both the Alpine store key and the input id/label-for,
7+
# so multiple instances of this component can coexist on a single page.
8+
def initialize(form:, method:, collection:, item_value_method:, item_label_method:, small: false, warning_message: nil, item_states: {}, instance_key: nil)
79
super(form:, method:)
810
@collection = collection
911
@item_value_method = item_value_method
@@ -13,7 +15,7 @@ def initialize(form:, method:, collection:, item_value_method:, item_label_metho
1315
@item_states = item_states.transform_keys(&:to_s).transform_values(&:to_sym)
1416
invalid = @item_states.values - ALLOWED_ITEM_STATES
1517
raise ArgumentError, "Unknown item_states: #{invalid.inspect}. Allowed: #{ALLOWED_ITEM_STATES.inspect}" if invalid.any?
16-
@unique_id = unique_id
18+
@instance_key = instance_key
1719
end
1820

1921
private
@@ -45,7 +47,7 @@ def state_for(value)
4547
end
4648

4749
def store_key
48-
suffix = @unique_id ? "_#{@unique_id}" : ""
50+
suffix = @instance_key.present? ? "_#{@instance_key}" : ""
4951
"#{@method}#{suffix}"
5052
end
5153
end

app/components/conditional_component.html.erb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div x-data="{condition: false}"
2-
x-effect="condition = $store.<%= @method.to_s + @unique_alpine_store_key.to_s %> === '<%= @value %>' ||
3-
(Array.isArray($store.<%= @method.to_s + @unique_alpine_store_key.to_s %>) && $store.<%= @method.to_s + @unique_alpine_store_key.to_s %>.includes('<%= @value %>'))">
2+
x-effect="condition = $store.<%= store_key %> === '<%= @value %>' ||
3+
(Array.isArray($store.<%= store_key %>) && $store.<%= store_key %>.includes('<%= @value %>'))">
44
<p class="sr-only" aria-live="polite" role="status"
55
x-text="'<%= content_description %> is now ' + (condition ? 'visible' : 'hidden')">
66
</p>
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
# frozen_string_literal: true
22

33
class ConditionalComponent < ViewComponent::Base
4-
def initialize(method:, value:, content_description: nil, unique_alpine_store_key: "")
4+
# instance_key must match the instance_key passed to the corresponding
5+
# RadioButtonsComponent or CheckboxesComponent so this conditional reads the
6+
# right Alpine store when multiple instances coexist on a page.
7+
def initialize(method:, value:, content_description: nil, instance_key: nil)
58
@method = method
69
@value = value
710
@content_description = content_description
8-
@unique_alpine_store_key = unique_alpine_store_key
11+
@instance_key = instance_key
912
end
1013

1114
private
1215

1316
def content_description
1417
@content_description || "Conditional section"
1518
end
19+
20+
def store_key
21+
suffix = @instance_key.present? ? "_#{@instance_key}" : ""
22+
"#{@method}#{suffix}"
23+
end
1624
end

app/components/radio_buttons_component.html.erb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111
"x-model" => "$store.#{store_key}",
1212
:class => radio_classes
1313
}
14-
if @unique_id
15-
input_id = "#{@form.object_name}_#{@method}_#{item_value}_#{@unique_id}"
14+
if @instance_key.present?
15+
input_id = "#{@form.object_name}_#{@method}_#{item_value}_#{@instance_key}"
1616
radio_attrs[:id] = input_id
1717
end
1818
label_opts = {}
19-
label_opts[:for] = input_id if @unique_id
19+
label_opts[:for] = input_id if @instance_key.present?
2020
content_tag :div, class: "radio_button_item" do
2121
concat radio_button_item.radio_button(radio_attrs)
2222
concat(label_opts.empty? ? radio_button_item.label : radio_button_item.label(label_opts))

app/components/radio_buttons_component.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# frozen_string_literal: true
22

33
class RadioButtonsComponent < AttributeBoundFormElementComponent
4-
def initialize(form:, method:, collection:, item_value_method:, item_label_method:, unique_alpine_store_key: "", layout: :vertical, small: false, warning_message: nil, unique_id: nil, legend: nil)
4+
# instance_key namespaces both the Alpine store key and the input id/label-for,
5+
# so multiple instances of this component can coexist on a single page.
6+
def initialize(form:, method:, collection:, item_value_method:, item_label_method:, instance_key: nil, layout: :vertical, small: false, warning_message: nil, legend: nil)
57
super(form:, method:)
68
@collection = collection
79
@item_value_method = item_value_method
810
@item_label_method = item_label_method
9-
@unique_alpine_store_key = unique_alpine_store_key
11+
@instance_key = instance_key
1012
@small = small
1113
@warning_message = warning_message
12-
@unique_id = unique_id
1314
@legend = legend
1415
@layout =
1516
case layout
@@ -45,7 +46,7 @@ def radio_classes
4546
end
4647

4748
def store_key
48-
suffix = @unique_id ? "_#{@unique_id}" : @unique_alpine_store_key.to_s
49+
suffix = @instance_key.present? ? "_#{@instance_key}" : ""
4950
"#{@method}#{suffix}"
5051
end
5152
end

test/components/checkboxes_component_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,20 +133,20 @@ def test_item_states_rejects_unknown_state
133133
end
134134
end
135135

136-
def test_unique_id_namespaces_input_ids_and_label_for
136+
def test_instance_key_namespaces_input_ids_and_label_for
137137
render_inline(CheckboxesComponent.new(
138138
form: build_form,
139139
method: :checkboxes_field,
140140
collection: simple_collection,
141141
item_value_method: :value,
142142
item_label_method: :label,
143-
unique_id: "abc"
143+
instance_key: "abc"
144144
))
145145
assert_selector "input[type='checkbox'][id$='_abc']", count: 2
146146
assert_selector "label[for$='_abc']", count: 2
147147
end
148148

149-
def test_no_unique_id_keeps_default_input_ids
149+
def test_no_instance_key_keeps_default_input_ids
150150
render_inline(CheckboxesComponent.new(
151151
form: build_form,
152152
method: :checkboxes_field,
Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,79 @@
11
class CheckboxesComponentPreview < FormComponentPreview
22
# @!group Default
33
def default
4-
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, unique_id: "default"))
4+
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, instance_key: "default"))
55
end
66

77
def prefilled
88
custom_model = TestModel.new(favorite_fruits: ["apple", "orange"])
9-
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, unique_id: "prefilled"))
9+
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, instance_key: "prefilled"))
1010
end
1111

1212
def with_error
1313
custom_model = TestModel.new
1414
custom_model.valid?
15-
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, unique_id: "with_error"))
15+
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, instance_key: "with_error"))
1616
end
1717

1818
def with_warning
19-
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, warning_message: "Message goes here.", unique_id: "with_warning"))
19+
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, warning_message: "Message goes here.", instance_key: "with_warning"))
2020
end
2121

2222
def indeterminate
23-
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, item_states: {"orange" => :indeterminate}, unique_id: "indeterminate"))
23+
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, item_states: {"orange" => :indeterminate}, instance_key: "indeterminate"))
2424
end
2525

2626
def disabled_item
27-
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, item_states: {"orange" => :disabled}, unique_id: "disabled_item"))
27+
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, item_states: {"orange" => :disabled}, instance_key: "disabled_item"))
2828
end
2929

3030
def mixed_states
31-
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, item_states: {"orange" => :indeterminate, "apple" => :disabled}, unique_id: "mixed_states"))
31+
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, item_states: {"orange" => :indeterminate, "apple" => :disabled}, instance_key: "mixed_states"))
3232
end
3333

3434
def mixed_states_with_error_and_warning
3535
custom_model = TestModel.new
3636
custom_model.valid?
37-
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, warning_message: "Message goes here.", item_states: {"orange" => :indeterminate, "apple" => :disabled}, unique_id: "mixed_states_with_error_and_warning"))
37+
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, warning_message: "Message goes here.", item_states: {"orange" => :indeterminate, "apple" => :disabled}, instance_key: "mixed_states_with_error_and_warning"))
3838
end
3939
# @!endgroup
4040

4141
# @!group Small
4242
def small_default
43-
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, unique_id: "small_default"))
43+
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, instance_key: "small_default"))
4444
end
4545

4646
def small_prefilled
4747
custom_model = TestModel.new(favorite_fruits: ["apple", "orange"])
48-
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, unique_id: "small_prefilled"))
48+
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, instance_key: "small_prefilled"))
4949
end
5050

5151
def small_with_error
5252
custom_model = TestModel.new
5353
custom_model.valid?
54-
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, unique_id: "small_with_error"))
54+
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, instance_key: "small_with_error"))
5555
end
5656

5757
def small_with_warning
58-
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, warning_message: "Message goes here.", unique_id: "small_with_warning"))
58+
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, warning_message: "Message goes here.", instance_key: "small_with_warning"))
5959
end
6060

6161
def small_indeterminate
62-
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, item_states: {"orange" => :indeterminate}, unique_id: "small_indeterminate"))
62+
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, item_states: {"orange" => :indeterminate}, instance_key: "small_indeterminate"))
6363
end
6464

6565
def small_disabled_item
66-
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, item_states: {"orange" => :disabled}, unique_id: "small_disabled_item"))
66+
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, item_states: {"orange" => :disabled}, instance_key: "small_disabled_item"))
6767
end
6868

6969
def small_mixed_states
70-
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, item_states: {"orange" => :indeterminate, "apple" => :disabled}, unique_id: "small_mixed_states"))
70+
render(CheckboxesComponent.new(form:, method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, item_states: {"orange" => :indeterminate, "apple" => :disabled}, instance_key: "small_mixed_states"))
7171
end
7272

7373
def small_mixed_states_with_error_and_warning
7474
custom_model = TestModel.new
7575
custom_model.valid?
76-
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, warning_message: "Message goes here.", item_states: {"orange" => :indeterminate, "apple" => :disabled}, unique_id: "small_mixed_states_with_error_and_warning"))
76+
render(CheckboxesComponent.new(form: form(model: custom_model), method: :favorite_fruits, collection: self.class.fruit_options, item_value_method: :value, item_label_method: :label, small: true, warning_message: "Message goes here.", item_states: {"orange" => :indeterminate, "apple" => :disabled}, instance_key: "small_mixed_states_with_error_and_warning"))
7777
end
7878
# @!endgroup
7979
end

test/components/previews/conditional_component_preview.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ def prefilled_combobox
2727
end
2828
# @!endgroup
2929

30-
def unique_alpine_store_key
30+
def instance_key
3131
end
3232
end

test/components/previews/conditional_component_preview/unique_alpine_store_key.html.erb renamed to test/components/previews/conditional_component_preview/instance_key.html.erb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
<p>
22
When two forms on the same page share a field with the same name, they would normally share
33
a single Alpine store, causing each form's ConditionalComponent to react to the other form's
4-
selection. Passing a <code>unique_alpine_store_key</code> to both RadioButtonsComponent and
4+
selection. Passing an <code>instance_key</code> to both RadioButtonsComponent and
55
ConditionalComponent scopes each pair to its own store so they operate independently. This
66
is relevant for forms that update primary and secondary filers with the same-named attribute.
77
</p>
88

99
<h2>Form 1</h2>
1010
<%= form_with url: "/", method: :post, model: FormComponentPreview::TestModel.new do |f| %>
11-
<%= render RadioButtonsComponent.new(form: f, method: :pineapple_pizza_preference, collection: FormComponentPreview.yes_no_options, item_value_method: :value, item_label_method: :label, unique_alpine_store_key: "_form1") %>
12-
<%= render ConditionalComponent.new(method: :pineapple_pizza_preference, value: "yes", content_description: "Followup question for form 1", unique_alpine_store_key: "_form1") do %>
11+
<%= render RadioButtonsComponent.new(form: f, method: :pineapple_pizza_preference, collection: FormComponentPreview.yes_no_options, item_value_method: :value, item_label_method: :label, instance_key: "form1") %>
12+
<%= render ConditionalComponent.new(method: :pineapple_pizza_preference, value: "yes", content_description: "Followup question for form 1", instance_key: "form1") do %>
1313
But why!?
1414
<% end %>
1515
<% end %>
1616

1717
<h2>Form 2</h2>
1818
<%= form_with url: "/", method: :post, model: FormComponentPreview::TestModel.new do |f| %>
19-
<%= render RadioButtonsComponent.new(form: f, method: :pineapple_pizza_preference, collection: FormComponentPreview.yes_no_options, item_value_method: :value, item_label_method: :label, unique_alpine_store_key: "_form2") %>
20-
<%= render ConditionalComponent.new(method: :pineapple_pizza_preference, value: "yes", content_description: "Followup question for form 2", unique_alpine_store_key: "_form2") do %>
19+
<%= render RadioButtonsComponent.new(form: f, method: :pineapple_pizza_preference, collection: FormComponentPreview.yes_no_options, item_value_method: :value, item_label_method: :label, instance_key: "form2") %>
20+
<%= render ConditionalComponent.new(method: :pineapple_pizza_preference, value: "yes", content_description: "Followup question for form 2", instance_key: "form2") do %>
2121
But why!?
2222
<% end %>
2323
<% end %>

0 commit comments

Comments
 (0)