stimulus_plumbers 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/lib/stimulus_plumbers/components/action_list/section.rb +6 -5
  4. data/lib/stimulus_plumbers/components/action_list.rb +3 -3
  5. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_month.rb +10 -8
  6. data/lib/stimulus_plumbers/components/card/section.rb +3 -3
  7. data/lib/stimulus_plumbers/components/card.rb +3 -3
  8. data/lib/stimulus_plumbers/components/combobox/autocomplete.rb +8 -14
  9. data/lib/stimulus_plumbers/components/combobox/date.rb +6 -2
  10. data/lib/stimulus_plumbers/components/combobox/dropdown.rb +6 -2
  11. data/lib/stimulus_plumbers/components/combobox/popover.rb +9 -5
  12. data/lib/stimulus_plumbers/components/combobox/time.rb +1 -1
  13. data/lib/stimulus_plumbers/components/combobox/trigger.rb +20 -11
  14. data/lib/stimulus_plumbers/components/combobox.rb +33 -23
  15. data/lib/stimulus_plumbers/components/divider.rb +16 -0
  16. data/lib/stimulus_plumbers/components/popover/builder.rb +2 -2
  17. data/lib/stimulus_plumbers/form/builder.rb +39 -43
  18. data/lib/stimulus_plumbers/form/field.rb +96 -45
  19. data/lib/stimulus_plumbers/form/fields/error.rb +2 -2
  20. data/lib/stimulus_plumbers/form/fields/fieldset.rb +54 -0
  21. data/lib/stimulus_plumbers/form/fields/group.rb +2 -2
  22. data/lib/stimulus_plumbers/form/fields/hint.rb +2 -2
  23. data/lib/stimulus_plumbers/form/fields/input_group.rb +25 -0
  24. data/lib/stimulus_plumbers/form/fields/inputs/choice.rb +69 -0
  25. data/lib/stimulus_plumbers/form/fields/inputs/datetime.rb +81 -0
  26. data/lib/stimulus_plumbers/form/fields/inputs/file.rb +22 -0
  27. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +59 -0
  28. data/lib/stimulus_plumbers/form/fields/inputs/search.rb +102 -0
  29. data/lib/stimulus_plumbers/form/fields/inputs/select/grouped.rb +56 -0
  30. data/lib/stimulus_plumbers/form/fields/inputs/select/timezone.rb +59 -0
  31. data/lib/stimulus_plumbers/form/fields/inputs/select/weekday.rb +45 -0
  32. data/lib/stimulus_plumbers/form/fields/inputs/select.rb +91 -0
  33. data/lib/stimulus_plumbers/form/fields/inputs/submit.rb +25 -0
  34. data/lib/stimulus_plumbers/form/fields/inputs/text.rb +37 -0
  35. data/lib/stimulus_plumbers/form/fields/inputs/text_area.rb +22 -0
  36. data/lib/stimulus_plumbers/form/fields/label.rb +13 -9
  37. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +1 -1
  38. data/lib/stimulus_plumbers/helpers/combobox_helper.rb +29 -36
  39. data/lib/stimulus_plumbers/helpers/divider_helper.rb +11 -0
  40. data/lib/stimulus_plumbers/helpers.rb +2 -0
  41. data/lib/stimulus_plumbers/themes/base.rb +1 -0
  42. data/lib/stimulus_plumbers/themes/schema.rb +7 -1
  43. data/lib/stimulus_plumbers/version.rb +1 -1
  44. data/lib/stimulus_plumbers.rb +4 -2
  45. metadata +17 -11
  46. data/lib/stimulus_plumbers/form/fields/choice.rb +0 -25
  47. data/lib/stimulus_plumbers/form/fields/combobox.rb +0 -44
  48. data/lib/stimulus_plumbers/form/fields/file.rb +0 -16
  49. data/lib/stimulus_plumbers/form/fields/password.rb +0 -55
  50. data/lib/stimulus_plumbers/form/fields/renderer.rb +0 -55
  51. data/lib/stimulus_plumbers/form/fields/search.rb +0 -54
  52. data/lib/stimulus_plumbers/form/fields/select.rb +0 -33
  53. data/lib/stimulus_plumbers/form/fields/submit.rb +0 -23
  54. data/lib/stimulus_plumbers/form/fields/text.rb +0 -33
  55. data/lib/stimulus_plumbers/form/fields/text_area.rb +0 -16
@@ -3,79 +3,130 @@
3
3
  module StimulusPlumbers
4
4
  module Form
5
5
  class Field
6
- OPTIONS = %i[label details error required label_visibility layout reveal clearable].freeze
6
+ attr_reader :label, :required, :layout
7
7
 
8
- attr_reader :object,
9
- :attribute,
10
- :input_id,
11
- :label_text,
12
- :details,
13
- :required,
14
- :label_visibility,
15
- :layout
8
+ def self.label_id(input_id)
9
+ [input_id, "label"].compact.join("_")
10
+ end
16
11
 
17
12
  def initialize(
18
- object:,
19
- attribute:,
20
- input_id:,
13
+ template,
21
14
  label: nil,
22
- details: nil,
15
+ hint: nil,
23
16
  error: nil,
24
17
  required: false,
25
- label_visibility: :visible,
26
- layout: :stacked
18
+ hide_label: false,
19
+ layout: :stacked,
20
+ **kwargs
27
21
  )
28
- @object = object
29
- @attribute = attribute
30
- @input_id = input_id
31
- @label_text = label || attribute.to_s.humanize
32
- @details = details
33
- @error_override = error
34
- @required = required
35
- @label_visibility = label_visibility.to_sym
36
- @layout = layout.to_sym
22
+ @template = template
23
+ @label = label
24
+ @hint = hint
25
+ @error_override = error
26
+ @required = required
27
+ @hide_label = hide_label
28
+ @layout = layout.to_sym
29
+ @kwargs = kwargs
30
+ end
31
+
32
+ def label_hidden?
33
+ @hide_label
34
+ end
35
+
36
+ def error?(object, attribute)
37
+ build_errors(object, attribute).any?
37
38
  end
38
39
 
39
- def errors
40
+ def described_by(object, attribute, input_id)
41
+ ids = []
42
+ ids << hint_id(input_id) if @hint.present?
43
+ ids.concat(build_error_ids(object, attribute, input_id))
44
+ ids.join(" ").presence
45
+ end
46
+
47
+ def render(object, attribute, input_id:, &block)
48
+ @label ||= attribute.to_s.humanize
49
+ error = error?(object, attribute)
50
+ aria = build_aria(object, attribute, input_id)
51
+ generated_opts = build_html_options(input_id, aria)
52
+ field_html = @template.capture(generated_opts, @kwargs, error, &block)
53
+ Fields::Group.new(@template).render(layout: @layout, error: error) do
54
+ @template.safe_join(
55
+ [
56
+ field_label(input_id),
57
+ field_html,
58
+ render_hint(input_id),
59
+ render_errors(object, attribute, input_id)
60
+ ]
61
+ )
62
+ end
63
+ end
64
+
65
+ def render_hint(input_id)
66
+ Fields::Hint.new(@template).render(text: @hint, id: hint_id(input_id)) if @hint.present?
67
+ end
68
+
69
+ def render_errors(object, attribute, input_id)
70
+ errs = build_errors(object, attribute)
71
+ return if errs.none?
72
+
73
+ @template.safe_join(
74
+ errs.map.with_index do |message, i|
75
+ Fields::Error.new(@template).render(message: message, id: build_error_ids(object, attribute, input_id)[i])
76
+ end
77
+ )
78
+ end
79
+
80
+ private
81
+
82
+ def build_errors(object, attribute)
40
83
  if @error_override
41
84
  Array(@error_override)
42
85
  elsif object.respond_to?(:errors)
43
- object.errors[@attribute]
86
+ object.errors[attribute]
44
87
  else
45
88
  []
46
89
  end
47
90
  end
48
91
 
49
- def error?
50
- errors.any?
92
+ def build_aria(object, attribute, input_id)
93
+ aria = {}
94
+ aria[:describedby] = described_by(object, attribute, input_id)
95
+ aria[:invalid] = "true" if error?(object, attribute)
96
+ aria[:required] = "true" if @required
97
+ aria.compact
51
98
  end
52
99
 
53
- def html_opts
54
- attrs = { id: input_id }
55
- attrs[:"aria-describedby"] = described_by if described_by
56
- attrs[:"aria-invalid"] = "true" if error?
57
- attrs[:required] = true if required
58
- attrs[:"aria-required"] = "true" if required
100
+ def build_html_options(input_id, aria)
101
+ attrs = { id: input_id, aria: aria }
102
+ attrs[:required] = true if @required
59
103
  attrs
60
104
  end
61
105
 
62
- def hint_id
63
- "#{input_id}_hint"
106
+ def hint_id(input_id)
107
+ [input_id, "hint"].compact.join("_")
64
108
  end
65
109
 
66
- def error_id
67
- "#{input_id}_error"
110
+ def error_id(input_id)
111
+ [input_id, "error"].compact.join("_")
68
112
  end
69
113
 
70
- def described_by
71
- ids = []
72
- ids << hint_id if details.present?
73
- ids << error_id if errors.any?
74
- ids.join(" ").presence
114
+ def build_error_ids(object, attribute, input_id)
115
+ errs = build_errors(object, attribute)
116
+ return [] if errs.none?
117
+ return [error_id(input_id)] if errs.one?
118
+
119
+ errs.each_index.map { |i| [error_id(input_id), i + 1].compact.join("_") }
75
120
  end
76
121
 
77
- def label_hidden?
78
- label_visibility == :exclusive
122
+ def field_label(input_id)
123
+ Fields::Label.new(@template).render(
124
+ text: @label,
125
+ for_id: input_id,
126
+ id: self.class.label_id(input_id),
127
+ required: @required,
128
+ hidden: @hide_label
129
+ )
79
130
  end
80
131
  end
81
132
  end
@@ -5,8 +5,8 @@ module StimulusPlumbers
5
5
  module Fields
6
6
  class Error < Plumber::Base
7
7
  def render(message:, id:)
8
- klass = theme.resolve(:form_error).fetch(:classes, "")
9
- template.content_tag(:p, message, id: id, class: klass.presence, role: "alert")
8
+ html_options = merge_html_options(theme.resolve(:form_error))
9
+ template.content_tag(:p, message, id: id, role: "alert", **html_options)
10
10
  end
11
11
  end
12
12
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ class Fieldset < Plumber::Base
7
+ def render(object, attribute, input_id, field, &block)
8
+ error = field.error?(object, attribute)
9
+ fieldset_opts = build_fieldset_aria(field, object, attribute, input_id, error)
10
+ Group.new(template).render(layout: field.layout, error: error) do
11
+ template.safe_join(
12
+ [
13
+ build_fieldset(fieldset_opts, field, attribute, error, &block),
14
+ field.render_hint(input_id),
15
+ field.render_errors(object, attribute, input_id)
16
+ ]
17
+ )
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def build_fieldset(fieldset_opts, field, attribute, error, &block)
24
+ template.content_tag(:fieldset, **fieldset_opts) do
25
+ template.safe_join(
26
+ [
27
+ legend(field, attribute),
28
+ template.capture(error, &block)
29
+ ]
30
+ )
31
+ end
32
+ end
33
+
34
+ def legend(field, attribute)
35
+ Label.new(template).render(
36
+ text: field.label || attribute.to_s.humanize,
37
+ required: field.required,
38
+ hidden: field.label_hidden?,
39
+ tag: :legend
40
+ )
41
+ end
42
+
43
+ def build_fieldset_aria(field, object, attribute, input_id, error)
44
+ opts = {}
45
+ db = field.described_by(object, attribute, input_id)
46
+ opts[:"aria-describedby"] = db if db
47
+ opts[:"aria-invalid"] = "true" if error
48
+ opts[:"aria-required"] = "true" if field.required
49
+ opts
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -5,8 +5,8 @@ module StimulusPlumbers
5
5
  module Fields
6
6
  class Group < Plumber::Base
7
7
  def render(layout: :stacked, error: false, &block)
8
- klass = theme.resolve(:form_group, layout: layout, error: error).fetch(:classes, "")
9
- template.content_tag(:div, class: klass.presence, &block)
8
+ html_options = merge_html_options(theme.resolve(:form_group, layout: layout, error: error))
9
+ template.content_tag(:div, **html_options, &block)
10
10
  end
11
11
  end
12
12
  end
@@ -5,8 +5,8 @@ module StimulusPlumbers
5
5
  module Fields
6
6
  class Hint < Plumber::Base
7
7
  def render(text:, id:)
8
- klass = theme.resolve(:form_details).fetch(:classes, "")
9
- template.content_tag(:p, text, id: id, class: klass.presence)
8
+ html_options = merge_html_options(theme.resolve(:form_details))
9
+ template.content_tag(:p, text, id: id, **html_options)
10
10
  end
11
11
  end
12
12
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ class InputGroup < Plumber::Base
7
+ def render(leading: nil, trailing: nil, error: false, **wrapper_opts, &block)
8
+ html_options = merge_html_options(
9
+ theme.resolve(:form_input_group, error: error),
10
+ wrapper_opts
11
+ )
12
+ template.content_tag(:div, **html_options) do
13
+ template.safe_join(
14
+ [
15
+ leading.respond_to?(:call) ? leading.call : leading,
16
+ template.capture(&block),
17
+ trailing.respond_to?(:call) ? trailing.call : trailing
18
+ ]
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ module Inputs
7
+ module Choice
8
+ def check_box(attribute, options = {}, checked_value = "1", unchecked_value = "0")
9
+ options[:layout] ||= :inline
10
+ Field.new(@template, **options).render(
11
+ object,
12
+ attribute,
13
+ input_id: field_id(attribute)
14
+ ) do |html_opts, opts, error|
15
+ html_options = merge_html_options(opts, html_opts, field_theme(:form_checkbox, error: error))
16
+ super(attribute, html_options, checked_value, unchecked_value)
17
+ end
18
+ end
19
+
20
+ def collection_check_boxes(
21
+ attribute,
22
+ collection,
23
+ value_method,
24
+ text_method,
25
+ options = {},
26
+ html_options = {},
27
+ &block
28
+ )
29
+ options[:layout] ||= :inline
30
+ field = Field.new(@template, **options)
31
+ render_fieldset(attribute, field) do |error|
32
+ item_opts = merge_html_options(html_options, field_theme(:form_checkbox, error: error))
33
+ super(attribute, collection, value_method, text_method, {}, item_opts, &block)
34
+ end
35
+ end
36
+
37
+ def radio_button(attribute, tag_value, options = {})
38
+ options[:layout] ||= :inline
39
+ Field.new(@template, **options).render(
40
+ object,
41
+ attribute,
42
+ input_id: field_id(attribute, tag_value)
43
+ ) do |html_opts, opts, error|
44
+ html_options = merge_html_options(opts, html_opts, field_theme(:form_radio, error: error))
45
+ super(attribute, tag_value, html_options)
46
+ end
47
+ end
48
+
49
+ def collection_radio_buttons(
50
+ attribute,
51
+ collection,
52
+ value_method,
53
+ text_method,
54
+ options = {},
55
+ html_options = {},
56
+ &block
57
+ )
58
+ options[:layout] ||= :inline
59
+ field = Field.new(@template, **options)
60
+ render_fieldset(attribute, field) do |error|
61
+ item_opts = merge_html_options(html_options, field_theme(:form_radio, error: error))
62
+ super(attribute, collection, value_method, text_method, {}, item_opts, &block)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ module Inputs
7
+ module Datetime
8
+ def date_field(attribute, options = {})
9
+ html_native = options.delete(:html_native) { false }
10
+ Field.new(@template, **options).render(
11
+ object,
12
+ attribute,
13
+ input_id: field_id(attribute)
14
+ ) do |html_opts, opts, error|
15
+ if html_native
16
+ html_options = merge_html_options(opts, html_opts, field_theme(:form_input, error: error))
17
+ super(attribute, html_options)
18
+ else
19
+ render_date_combobox(attribute, html_opts, error)
20
+ end
21
+ end
22
+ end
23
+
24
+ def time_field(attribute, options = {})
25
+ html_native = options.delete(:html_native) { false }
26
+ format = options.delete(:format) { :h12 }
27
+ step = options.delete(:step) { 1 }
28
+ Field.new(@template, **options).render(
29
+ object,
30
+ attribute,
31
+ input_id: field_id(attribute)
32
+ ) do |html_opts, opts, error|
33
+ if html_native
34
+ html_options = merge_html_options(opts, html_opts, field_theme(:form_input, error: error))
35
+ super(attribute, html_options)
36
+ else
37
+ render_time_combobox(attribute, html_opts, error, format: format, step: step)
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def render_date_combobox(attribute, html_opts, error)
45
+ current_value = object.respond_to?(attribute) ? object.public_send(attribute) : nil
46
+ opts = Components::Combobox::Date.default_opts.deep_merge(
47
+ input: { value: current_value, data: { combobox_date_date_value: current_value } },
48
+ trigger: { aria: html_opts[:aria] },
49
+ popover: { labelledby: Field.label_id(html_opts[:id]) }
50
+ )
51
+ render_combobox(
52
+ attribute,
53
+ input_id: html_opts[:id],
54
+ opts: opts,
55
+ err: error,
56
+ data: { input_format_type_value: "date" }
57
+ ) do |popover_id|
58
+ Components::Combobox::Date.new(@template).render(value: current_value, popover_id: popover_id)
59
+ end
60
+ end
61
+
62
+ def render_time_combobox(attribute, html_opts, error, format:, step:)
63
+ current_value = object.respond_to?(attribute) ? object.public_send(attribute) : nil
64
+ opts = Components::Combobox::Time.default_opts.deep_merge(
65
+ input: { value: current_value },
66
+ trigger: { aria: html_opts[:aria] },
67
+ popover: { labelledby: Field.label_id(html_opts[:id]) }
68
+ )
69
+ render_combobox(
70
+ attribute,
71
+ input_id: html_opts[:id],
72
+ opts: opts,
73
+ err: error,
74
+ data: { input_format_type_value: "time", input_format_options_value: { format: format }.to_json }
75
+ ) { Components::Combobox::Time.new(@template).render(format: format, step: step, value: current_value) }
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ module Inputs
7
+ module File
8
+ def file_field(attribute, options = {})
9
+ Field.new(@template, **options).render(
10
+ object,
11
+ attribute,
12
+ input_id: field_id(attribute)
13
+ ) do |html_opts, opts, error|
14
+ html_options = merge_html_options(opts, html_opts, field_theme(:form_file, error: error))
15
+ super(attribute, html_options)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ module Inputs
7
+ module Password
8
+ def password_field(attribute, options = {})
9
+ reveal = options.delete(:reveal) { false }
10
+ Field.new(@template, **options).render(
11
+ object,
12
+ attribute,
13
+ input_id: field_id(attribute)
14
+ ) do |html_opts, opts, error|
15
+ if reveal
16
+ render_reveal_password(merge_html_options(opts, html_opts), error) do |html_options|
17
+ super(attribute, html_options)
18
+ end
19
+ else
20
+ html_options = merge_html_options(opts, html_opts, field_theme(:form_input, error: error))
21
+ super(attribute, html_options)
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def render_reveal_password(html_opts, error, &block)
29
+ html_options = merge_html_options(
30
+ html_opts,
31
+ field_theme(:form_input, error: error),
32
+ { data: { input_format_target: "input" } }
33
+ )
34
+ render_input_group(
35
+ error: error,
36
+ trailing: method(:reveal_button),
37
+ **merge_html_options(
38
+ field_theme(:form_input_reveal, error: error),
39
+ { data: { controller: "input-format", input_format_type_value: "password" } }
40
+ )
41
+ ) { @template.capture(html_options, &block) }
42
+ end
43
+
44
+ def reveal_button
45
+ html_options = merge_html_options(
46
+ field_theme(:form_button_reveal),
47
+ {
48
+ type: "button",
49
+ aria: { label: "Show password", pressed: "false" },
50
+ data: { input_format_target: "toggle", action: "click->input-format#toggle" }
51
+ }
52
+ )
53
+ @template.content_tag(:button, "", **html_options)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ module Inputs
7
+ module Search
8
+ def search_field(attribute, options = {})
9
+ html_native = options.delete(:html_native) { false }
10
+ clearable = options.delete(:clearable) { false }
11
+ url = options.delete(:url) { nil }
12
+ choices = options.delete(:options) { [] }
13
+
14
+ Field.new(@template, **options).render(
15
+ object,
16
+ attribute,
17
+ input_id: field_id(attribute)
18
+ ) do |html_opts, opts, error|
19
+ if html_native
20
+ render_search_input(html_opts, opts, error, clearable: clearable) do |html_options|
21
+ super(attribute, html_options)
22
+ end
23
+ else
24
+ render_search_combobox(
25
+ attribute,
26
+ html_opts,
27
+ error,
28
+ url: url,
29
+ clearable: clearable
30
+ ) do |combobox_opts, input_id, current_value|
31
+ render_search_autocomplete(attribute, input_id, combobox_opts, error, choices, current_value)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def render_search_input(html_opts, opts, error, clearable:, &block)
40
+ data = clearable ? { data: { input_search_target: "input" } } : {}
41
+ html_options = merge_html_options(opts, html_opts, field_theme(:form_input, error: error), data)
42
+ input_html = @template.capture(html_options, &block)
43
+
44
+ return input_html unless clearable
45
+
46
+ render_input_group(
47
+ trailing: method(:clear_button),
48
+ error: !!error,
49
+ **merge_html_options(field_theme(:form_input_search), { data: { controller: "input-search" } })
50
+ ) { input_html }
51
+ end
52
+
53
+ def render_search_combobox(attribute, html_opts, error, url:, clearable:, &block)
54
+ current_value = object.respond_to?(attribute) ? object.public_send(attribute) : nil
55
+ input_id = html_opts[:id]
56
+ opts = Components::Combobox::Autocomplete.default_opts.deep_merge(
57
+ input: { value: current_value },
58
+ trigger: { data: clearable ? { input_search_target: "input" } : {}, aria: html_opts[:aria] },
59
+ popover: { data: url ? { combobox_dropdown_url_value: url } : {} }
60
+ )
61
+
62
+ combobox_html = @template.capture(opts, input_id, current_value, &block)
63
+ return combobox_html unless clearable
64
+
65
+ render_input_group(
66
+ trailing: method(:clear_button),
67
+ error: !!error,
68
+ **merge_html_options(field_theme(:form_input_search), { data: { controller: "input-search" } })
69
+ ) { combobox_html }
70
+ end
71
+
72
+ def render_search_autocomplete(attribute, input_id, combobox_opts, error, choices, current_value)
73
+ render_combobox(
74
+ attribute,
75
+ input_id: input_id,
76
+ opts: combobox_opts,
77
+ err: error,
78
+ data: {
79
+ input_combobox_combobox_dropdown_outlet: "##{Components::Combobox.popover_id_for(input_id)}",
80
+ action: "input->input-combobox#onInput"
81
+ }
82
+ ) { Components::Combobox::Autocomplete.new(@template).render(options: choices, value: current_value, labelledby: Field.label_id(input_id)) }
83
+ end
84
+
85
+ def clear_button
86
+ Components::Button.new(@template).render(
87
+ "",
88
+ **merge_html_options(
89
+ field_theme(:form_button_clear),
90
+ {
91
+ aria: { label: "Clear search" },
92
+ hidden: true,
93
+ data: { input_search_target: "clear", action: "click->input-search#clear" }
94
+ }
95
+ )
96
+ )
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ module Inputs
7
+ module Select
8
+ module Grouped
9
+ def grouped_collection_select(
10
+ attribute,
11
+ collection,
12
+ group_method,
13
+ group_label_method,
14
+ option_key_method,
15
+ option_value_method,
16
+ options = {},
17
+ html_options = {}
18
+ )
19
+ html_native = options.delete(:html_native) { false }
20
+ Field.new(@template, **options).render(
21
+ object,
22
+ attribute,
23
+ input_id: field_id(attribute)
24
+ ) do |html_opts, opts, error|
25
+ merged_html_opts = merge_html_options(html_options, html_opts, field_theme(:form_select, error: error))
26
+ if html_native
27
+ super(
28
+ attribute,
29
+ collection,
30
+ group_method,
31
+ group_label_method,
32
+ option_key_method,
33
+ option_value_method,
34
+ opts,
35
+ merged_html_opts
36
+ )
37
+ else
38
+ render_select_dropdown(attribute, opts, merged_html_opts, err: error) do
39
+ collection.map do |group|
40
+ {
41
+ label: group.public_send(group_label_method),
42
+ options: group.public_send(group_method).map do |item|
43
+ [item.public_send(option_value_method), item.public_send(option_key_method)]
44
+ end
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end