stimulus_plumbers 0.2.7 → 0.2.9

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/README.md +60 -41
  4. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.es.js +760 -237
  5. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.umd.js +1 -1
  6. data/lib/stimulus_plumbers/components/action_list/item.rb +27 -0
  7. data/lib/stimulus_plumbers/components/action_list/section.rb +21 -0
  8. data/lib/stimulus_plumbers/components/action_list.rb +23 -0
  9. data/lib/stimulus_plumbers/components/avatar.rb +72 -0
  10. data/lib/stimulus_plumbers/components/button/group.rb +17 -0
  11. data/lib/stimulus_plumbers/components/button.rb +27 -0
  12. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_month.rb +2 -2
  13. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_week.rb +2 -2
  14. data/lib/stimulus_plumbers/components/calendar/month/turbo.rb +55 -0
  15. data/lib/stimulus_plumbers/components/calendar.rb +33 -0
  16. data/lib/stimulus_plumbers/components/card/section.rb +25 -0
  17. data/lib/stimulus_plumbers/components/card.rb +27 -0
  18. data/lib/stimulus_plumbers/components/combobox/autocomplete.rb +53 -0
  19. data/lib/stimulus_plumbers/components/combobox/date.rb +50 -0
  20. data/lib/stimulus_plumbers/components/combobox/dropdown.rb +38 -0
  21. data/lib/stimulus_plumbers/components/combobox/options/option.rb +34 -0
  22. data/lib/stimulus_plumbers/components/combobox/options/option_group.rb +29 -0
  23. data/lib/stimulus_plumbers/components/combobox/options.rb +59 -0
  24. data/lib/stimulus_plumbers/components/combobox/popover.rb +20 -0
  25. data/lib/stimulus_plumbers/components/combobox/time/drum.rb +37 -0
  26. data/lib/stimulus_plumbers/components/combobox/time.rb +120 -0
  27. data/lib/stimulus_plumbers/components/combobox/trigger.rb +38 -0
  28. data/lib/stimulus_plumbers/components/combobox.rb +59 -0
  29. data/lib/stimulus_plumbers/components/date_picker/navigation.rb +1 -1
  30. data/lib/stimulus_plumbers/components/date_picker/navigator.rb +1 -1
  31. data/lib/stimulus_plumbers/components/icon.rb +49 -0
  32. data/lib/stimulus_plumbers/components/popover/builder.rb +25 -0
  33. data/lib/stimulus_plumbers/components/popover.rb +26 -0
  34. data/lib/stimulus_plumbers/form/builder.rb +64 -17
  35. data/lib/stimulus_plumbers/form/{field_component.rb → field.rb} +13 -11
  36. data/lib/stimulus_plumbers/form/fields/combobox.rb +41 -0
  37. data/lib/stimulus_plumbers/form/fields/error.rb +14 -0
  38. data/lib/stimulus_plumbers/form/fields/group.rb +14 -0
  39. data/lib/stimulus_plumbers/form/fields/hint.rb +14 -0
  40. data/lib/stimulus_plumbers/form/fields/label.rb +21 -0
  41. data/lib/stimulus_plumbers/form/fields/password.rb +55 -0
  42. data/lib/stimulus_plumbers/form/fields/renderer.rb +16 -21
  43. data/lib/stimulus_plumbers/form/fields/search.rb +54 -0
  44. data/lib/stimulus_plumbers/form/fields/select.rb +8 -2
  45. data/lib/stimulus_plumbers/form/fields/submit.rb +23 -0
  46. data/lib/stimulus_plumbers/form/fields/text.rb +12 -4
  47. data/lib/stimulus_plumbers/helpers/action_list_helper.rb +2 -2
  48. data/lib/stimulus_plumbers/helpers/avatar_helper.rb +2 -2
  49. data/lib/stimulus_plumbers/helpers/button_helper.rb +2 -2
  50. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +1 -1
  51. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +1 -1
  52. data/lib/stimulus_plumbers/helpers/card_helper.rb +2 -2
  53. data/lib/stimulus_plumbers/helpers/combobox_helper.rb +74 -0
  54. data/lib/stimulus_plumbers/helpers/popover_helper.rb +2 -2
  55. data/lib/stimulus_plumbers/helpers.rb +2 -2
  56. data/lib/stimulus_plumbers/plumber/base.rb +20 -0
  57. data/lib/stimulus_plumbers/plumber/dispatcher.rb +111 -0
  58. data/lib/stimulus_plumbers/plumber/html_options.rb +51 -0
  59. data/lib/stimulus_plumbers/plumber/renderer.rb +89 -0
  60. data/lib/stimulus_plumbers/themes/base.rb +9 -15
  61. data/lib/stimulus_plumbers/themes/schema/ranges.rb +5 -5
  62. data/lib/stimulus_plumbers/themes/schema.rb +97 -0
  63. data/lib/stimulus_plumbers/themes/tailwind/calendar.rb +48 -2
  64. data/lib/stimulus_plumbers/themes/tailwind/combobox.rb +75 -0
  65. data/lib/stimulus_plumbers/themes/tailwind/form.rb +10 -6
  66. data/lib/stimulus_plumbers/themes/tailwind_theme.rb +2 -0
  67. data/lib/stimulus_plumbers/version.rb +1 -1
  68. data/lib/stimulus_plumbers.rb +41 -14
  69. metadata +42 -23
  70. data/lib/stimulus_plumbers/components/action_list/renderer.rb +0 -47
  71. data/lib/stimulus_plumbers/components/avatar/renderer.rb +0 -74
  72. data/lib/stimulus_plumbers/components/button/renderer.rb +0 -33
  73. data/lib/stimulus_plumbers/components/calendar/month/turbo/renderer.rb +0 -57
  74. data/lib/stimulus_plumbers/components/calendar/renderer.rb +0 -35
  75. data/lib/stimulus_plumbers/components/card/renderer.rb +0 -41
  76. data/lib/stimulus_plumbers/components/date_picker/renderer.rb +0 -82
  77. data/lib/stimulus_plumbers/components/icon/renderer.rb +0 -51
  78. data/lib/stimulus_plumbers/components/plumber/base.rb +0 -22
  79. data/lib/stimulus_plumbers/components/plumber/dispatcher.rb +0 -113
  80. data/lib/stimulus_plumbers/components/plumber/html_options.rb +0 -34
  81. data/lib/stimulus_plumbers/components/plumber/renderer.rb +0 -91
  82. data/lib/stimulus_plumbers/components/popover/renderer.rb +0 -46
  83. data/lib/stimulus_plumbers/helpers/date_picker_helper.rb +0 -17
  84. data/lib/stimulus_plumbers/themes/action_list.rb +0 -14
  85. data/lib/stimulus_plumbers/themes/avatar.rb +0 -14
  86. data/lib/stimulus_plumbers/themes/button.rb +0 -18
  87. data/lib/stimulus_plumbers/themes/calendar.rb +0 -15
  88. data/lib/stimulus_plumbers/themes/card.rb +0 -12
  89. data/lib/stimulus_plumbers/themes/form.rb +0 -30
  90. data/lib/stimulus_plumbers/themes/layout.rb +0 -12
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Dropdown < Plumber::Base
7
+ STIMULUS_CONTROLLER = "combobox-dropdown"
8
+ STIMULUS_ACTION = [
9
+ "click->#{STIMULUS_CONTROLLER}#select",
10
+ "keydown->#{STIMULUS_CONTROLLER}#onNavigate",
11
+ "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
12
+ ].join(" ").freeze
13
+
14
+ def self.default_opts
15
+ {
16
+ popover: {
17
+ tag: :div,
18
+ haspopup: "listbox",
19
+ data: { controller: STIMULUS_CONTROLLER, action: STIMULUS_ACTION }
20
+ }
21
+ }
22
+ end
23
+
24
+ def render(options: [], value: nil, label: nil, **_kwargs)
25
+ listbox_attrs = merge_html_options(
26
+ { classes: theme.resolve(:combobox_listbox).fetch(:classes, "") },
27
+ { role: "listbox", data: { "#{STIMULUS_CONTROLLER}_target": "listbox" } }
28
+ )
29
+ listbox_attrs[:aria] = { label: label } if label
30
+
31
+ template.content_tag(:ul, **listbox_attrs) do
32
+ Options.new(template).render(options, value: value)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Options
7
+ class Option < Plumber::Base
8
+ def render(label:, value:, description: nil, disabled: false, selected: false)
9
+ aria = { selected: selected ? "true" : "false" }
10
+ aria[:disabled] = "true" if disabled
11
+
12
+ attrs = merge_html_options(
13
+ { classes: theme.resolve(:combobox_option, selected: selected, disabled: disabled).fetch(:classes, "") },
14
+ { role: "option", aria: aria, data: { value: value } }
15
+ )
16
+
17
+ template.content_tag(:li, **attrs) do
18
+ if description
19
+ template.safe_join(
20
+ [
21
+ template.content_tag(:span, label),
22
+ template.content_tag(:span, description)
23
+ ]
24
+ )
25
+ else
26
+ label
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Options
7
+ class OptionGroup < Plumber::Base
8
+ def render(label:, options:, value: nil)
9
+ attrs = merge_html_options(
10
+ { classes: theme.resolve(:combobox_option_group).fetch(:classes, "") },
11
+ { role: "group", aria: { label: label } }
12
+ )
13
+
14
+ template.content_tag(:li, **attrs) do
15
+ template.safe_join(
16
+ [
17
+ template.content_tag(:span, label, aria: { hidden: "true" }),
18
+ template.content_tag(:ul) do
19
+ Options.new(template).render(options, value: value)
20
+ end
21
+ ]
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Options < Plumber::Base
7
+ def render(items, value: nil, &block)
8
+ @selected_value = value.to_s
9
+ template.safe_join(items.filter_map { |item| render_item(item, &block) })
10
+ end
11
+
12
+ private
13
+
14
+ def render_item(item, &block)
15
+ attrs = normalize_item(item)
16
+ return nil if attrs.nil?
17
+
18
+ if attrs.key?(:optgroup)
19
+ block ? block.call(attrs) : OptionGroup.new(template).render(**attrs[:optgroup], value: @selected_value)
20
+ else
21
+ block ? block.call(attrs) : Option.new(template).render(**attrs)
22
+ end
23
+ end
24
+
25
+ def normalize_item(item)
26
+ case item
27
+ when Hash then normalize_hash(item)
28
+ when Array then normalize_array(item)
29
+ else
30
+ StimulusPlumbers::Logger.warn("Options#normalize_item: unrecognized item type #{item.class}, skipping")
31
+ nil
32
+ end
33
+ end
34
+
35
+ def normalize_hash(item)
36
+ if item.key?(:options)
37
+ { optgroup: { label: item[:label], options: item[:options] } }
38
+ else
39
+ normalize_option(item[:label], item[:value].to_s, item)
40
+ end
41
+ end
42
+
43
+ def normalize_array(item)
44
+ normalize_option(item[0], item[1].to_s, item[2] || {})
45
+ end
46
+
47
+ def normalize_option(label, value, attrs)
48
+ {
49
+ label: label,
50
+ value: value,
51
+ selected: @selected_value == value,
52
+ disabled: attrs[:disabled] || false,
53
+ description: attrs[:description]
54
+ }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Popover < Plumber::Base
7
+ def render(stimulus_controller:, id:, tag: :div, role: nil, label: nil, content: nil, data: {}, **_rest, &block)
8
+ base_data = { "#{stimulus_controller}_target": "popover" }
9
+
10
+ attrs = { id: id, hidden: "", data: merge_data_options(base_data, data.symbolize_keys) }
11
+ attrs[:role] = role if role
12
+ attrs[:aria] = { label: label } if label
13
+
14
+ html_content = block_given? ? template.capture(&block) : content
15
+ template.content_tag(tag, **attrs) { html_content }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Time
7
+ class Drum < Plumber::Base
8
+ def render(stimulus_controller:, target:, label:, items:, selected: nil)
9
+ template.content_tag(
10
+ :ul,
11
+ **merge_html_options(
12
+ { classes: theme.resolve(:combobox_listbox).fetch(:classes, "") },
13
+ {
14
+ role: "listbox",
15
+ tabindex: "0",
16
+ aria: { label: label },
17
+ data: { "#{stimulus_controller}_target": target }
18
+ },
19
+ { data: { action: "click->#{stimulus_controller}#select keydown->#{stimulus_controller}#onNavigate" } }
20
+ )
21
+ ) do
22
+ template.safe_join(
23
+ items.map do |text, value|
24
+ Options::Option.new(template).render(
25
+ label: text,
26
+ value: value,
27
+ selected: value.to_s == selected.to_s
28
+ )
29
+ end
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Time < Plumber::Base
7
+ STIMULUS_CONTROLLER = "combobox-time"
8
+
9
+ def self.default_opts
10
+ {
11
+ popover: { label: "Picker", role: "dialog", tag: :div }
12
+ }
13
+ end
14
+
15
+ def render(format: :h12, step: 1, value: nil, **_kwargs)
16
+ @format = format
17
+ @step = [1, step.to_i].max
18
+ @time = parse_time(value)
19
+
20
+ template.content_tag(
21
+ :div,
22
+ **merge_html_options(
23
+ { classes: theme.resolve(:combobox_time).fetch(:classes, "") },
24
+ { data: { controller: STIMULUS_CONTROLLER,
25
+ action: "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
26
+ }
27
+ }
28
+ )
29
+ ) do
30
+ template.safe_join(drums)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def drums
37
+ cols = [hour_drum, minute_drum]
38
+ cols << period_drum if @format == :h12
39
+ cols
40
+ end
41
+
42
+ def hour_drum
43
+ drum.render(
44
+ stimulus_controller: STIMULUS_CONTROLLER,
45
+ target: "hour",
46
+ label: "Hour",
47
+ items: hour_items,
48
+ selected: current_hour
49
+ )
50
+ end
51
+
52
+ def minute_drum
53
+ items = (0...60).step(@step).map do |m|
54
+ s = m.to_s.rjust(2, "0")
55
+ [s, s]
56
+ end
57
+ selected = @time ? snap_minute(@time.min).to_s.rjust(2, "0") : nil
58
+ drum.render(
59
+ stimulus_controller: STIMULUS_CONTROLLER,
60
+ target: "minute",
61
+ label: "Minute",
62
+ items: items,
63
+ selected: selected
64
+ )
65
+ end
66
+
67
+ def period_drum
68
+ selected = @time && (@time.hour < 12 ? "AM" : "PM")
69
+ drum.render(
70
+ stimulus_controller: STIMULUS_CONTROLLER,
71
+ target: "period",
72
+ label: "Period",
73
+ items: [%w[AM AM], %w[PM PM]],
74
+ selected: selected
75
+ )
76
+ end
77
+
78
+ def hour_items
79
+ if @format == :h12
80
+ (1..12).map { |h| [h.to_s, h.to_s] }
81
+ else
82
+ (0..23).map do |h|
83
+ s = h.to_s.rjust(2, "0")
84
+ [s, s]
85
+ end
86
+ end
87
+ end
88
+
89
+ def current_hour
90
+ return nil unless @time
91
+
92
+ if @format == :h12
93
+ h = @time.hour % 12
94
+ (h.zero? ? 12 : h).to_s
95
+ else
96
+ @time.hour.to_s.rjust(2, "0")
97
+ end
98
+ end
99
+
100
+ def snap_minute(minute)
101
+ return minute if @step == 1
102
+
103
+ ((minute.to_f / @step).round * @step) % 60
104
+ end
105
+
106
+ def drum
107
+ @drum ||= Time::Drum.new(template)
108
+ end
109
+
110
+ def parse_time(value)
111
+ return nil if value.nil? || value.to_s.strip.empty?
112
+
113
+ ::Time.parse(value.to_s)
114
+ rescue ArgumentError
115
+ nil
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Trigger < Plumber::Base
7
+ def render(
8
+ stimulus_controller:,
9
+ popover_id:,
10
+ haspopup:,
11
+ readonly: true,
12
+ aria_autocomplete: nil,
13
+ aria_label: nil,
14
+ data: {},
15
+ **_rest
16
+ )
17
+ base_data = {
18
+ "#{stimulus_controller}_target": "trigger",
19
+ input_format_target: "input",
20
+ action: "focus->#{stimulus_controller}#open keydown.esc->#{stimulus_controller}#close"
21
+ }
22
+
23
+ aria = { haspopup: haspopup, expanded: "false", controls: popover_id }
24
+ aria[:autocomplete] = aria_autocomplete if aria_autocomplete
25
+ aria[:label] = aria_label if aria_label
26
+
27
+ template.tag.input(
28
+ type: "text",
29
+ readonly: (readonly ? true : nil),
30
+ role: "combobox",
31
+ aria: aria,
32
+ data: merge_data_options(base_data, data.symbolize_keys)
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox < Plumber::Base
6
+ STIMULUS_CONTROLLER = "input-combobox"
7
+ FORMAT_CONTROLLER = "input-format"
8
+ FORMAT_ACTION = "input-combobox:changed->input-format#format"
9
+
10
+ def render(base_id:, options: {}, **kwargs)
11
+ popover_id = "#{base_id}_popover"
12
+ initial_value = options.dig(:input, :value)
13
+
14
+ base_data = {
15
+ controller: "#{STIMULUS_CONTROLLER} #{FORMAT_CONTROLLER}",
16
+ action: FORMAT_ACTION
17
+ }
18
+ base_data[:input_combobox_value_value] = initial_value if initial_value.present?
19
+
20
+ html_options = merge_html_options({ data: base_data }, kwargs)
21
+
22
+ template.content_tag(:div, **html_options) do
23
+ template.safe_join(
24
+ [
25
+ trigger(popover_id, options),
26
+ hidden_input(options.fetch(:input, {})),
27
+ popover(popover_id, options)
28
+ ]
29
+ )
30
+ end
31
+ end
32
+
33
+ def trigger(popover_id, options)
34
+ haspopup = options.dig(:popover, :haspopup) || options.dig(:popover, :role) || "dialog"
35
+ Combobox::Trigger.new(template).render(
36
+ stimulus_controller: STIMULUS_CONTROLLER,
37
+ popover_id: popover_id,
38
+ haspopup: haspopup,
39
+ **options.fetch(:trigger, {})
40
+ )
41
+ end
42
+
43
+ def popover(popover_id, options)
44
+ Combobox::Popover.new(template).render(
45
+ stimulus_controller: STIMULUS_CONTROLLER,
46
+ id: popover_id,
47
+ **options.fetch(:popover, {})
48
+ )
49
+ end
50
+
51
+ private
52
+
53
+ def hidden_input(opts)
54
+ data = { "#{STIMULUS_CONTROLLER}_target": "value" }.merge(opts.fetch(:data, {}))
55
+ template.tag.input(type: "hidden", name: opts[:name], value: opts[:value], data: data)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -4,7 +4,7 @@ module StimulusPlumbers
4
4
  module Components
5
5
  module DatePicker
6
6
  class Navigation < Plumber::Base
7
- def render(stimulus_controller:, step:, **kwargs)
7
+ def render(step:, stimulus_controller:, **kwargs)
8
8
  html_options = merge_html_options(
9
9
  { classes: theme.resolve(:calendar_navigation).fetch(:classes, ""), aria: { label: "DatePicker Navigation" } },
10
10
  kwargs
@@ -20,7 +20,7 @@ module StimulusPlumbers
20
20
  private
21
21
 
22
22
  def icon(icon_options)
23
- Icon::Renderer.new(template).icon(
23
+ Icon.new(template).render(
24
24
  classes: theme.resolve(:calendar_navigation_navigator_icon).fetch(:classes, ""),
25
25
  **icon_options
26
26
  )
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Icon < Plumber::Base
6
+ ICONS = {
7
+ "arrow-left" => "M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18",
8
+ "arrow-right" => "M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3",
9
+ "arrow-up" => "M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18",
10
+ "arrow-down" => "M19.5 13.5 12 21m0 0-7.5-7.5M12 21V3"
11
+ }.freeze
12
+
13
+ def render(name:, **kwargs)
14
+ html_options = merge_html_options(
15
+ { classes: theme.resolve(:icon).fetch(:classes, "") },
16
+ kwargs
17
+ )
18
+
19
+ if ICONS[name]
20
+ svg_icon(ICONS[name], html_options)
21
+ else
22
+ template.content_tag(:span, nil, **html_options)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def svg_icon(path, html_options)
29
+ template.content_tag(
30
+ :svg,
31
+ xmlns: "http://www.w3.org/2000/svg",
32
+ fill: "none",
33
+ viewBox: "0 0 24 24",
34
+ width: "24",
35
+ height: "24",
36
+ "stroke-width": "1.5",
37
+ stroke: "currentColor",
38
+ **html_options
39
+ ) do
40
+ template.tag.path(
41
+ "stroke-linecap": "round",
42
+ "stroke-linejoin": "round",
43
+ d: path
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Popover
6
+ class Builder
7
+ attr_reader :activator_html, :content_html
8
+
9
+ def initialize(template)
10
+ @template = template
11
+ @activator_html = "".html_safe
12
+ @content_html = "".html_safe
13
+ end
14
+
15
+ def activator(&block)
16
+ @activator_html = @template.capture(&block)
17
+ end
18
+
19
+ def content(&block)
20
+ @content_html = @template.capture(&block)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Popover < Plumber::Base
6
+ def render(interactive: true, **kwargs, &block)
7
+ html_options = merge_html_options(
8
+ { classes: theme.resolve(:popover).fetch(:classes, "") },
9
+ kwargs
10
+ )
11
+
12
+ builder = Popover::Builder.new(template)
13
+ template.capture(builder, &block)
14
+
15
+ template.content_tag(:div, **html_options) do
16
+ wrapped_content = if interactive
17
+ template.content_tag(:template, builder.content_html)
18
+ else
19
+ builder.content_html
20
+ end
21
+ template.safe_join([builder.activator_html, wrapped_content])
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -2,29 +2,37 @@
2
2
 
3
3
  require "action_view/version"
4
4
 
5
- require_relative "field_component"
5
+ require_relative "field"
6
+ require_relative "fields/choice"
7
+ require_relative "fields/combobox"
8
+ require_relative "fields/file"
9
+ require_relative "fields/password"
6
10
  require_relative "fields/renderer"
11
+ require_relative "fields/search"
12
+ require_relative "fields/select"
7
13
  require_relative "fields/text"
8
14
  require_relative "fields/text_area"
9
- require_relative "fields/file"
10
- require_relative "fields/select"
11
- require_relative "fields/choice"
12
- require_relative "../components/plumber/html_options"
15
+ require_relative "fields/submit"
16
+ require_relative "../plumber/html_options"
13
17
 
14
18
  module StimulusPlumbers
15
19
  module Form
16
20
  class Builder < ActionView::Helpers::FormBuilder
17
- include Components::Plumber::HtmlOptions
18
- include Fields::Text
19
- include Fields::TextArea
21
+ include Plumber::HtmlOptions
22
+ include Fields::Choice
23
+ include Fields::Combobox
20
24
  include Fields::File
25
+ include Fields::Password
26
+ include Fields::Search
21
27
  include Fields::Select
22
- include Fields::Choice
28
+ include Fields::Submit
29
+ include Fields::Text
30
+ include Fields::TextArea
23
31
 
24
32
  private
25
33
 
26
34
  def build_field(attribute, form_field_opts, input_id: field_id(attribute))
27
- FieldComponent.new(
35
+ Field.new(
28
36
  object: object,
29
37
  attribute: attribute,
30
38
  input_id: input_id,
@@ -41,8 +49,17 @@ module StimulusPlumbers
41
49
  Fields::Renderer.new(@template, theme, field).call(input_html)
42
50
  end
43
51
 
52
+ def build_input_group(input_tag, field, trailing:, **wrapper_opts)
53
+ @template.content_tag(
54
+ :div,
55
+ input_tag.html_safe + trailing,
56
+ class: field_theme(:form_input_group, error: field.error?)[:class],
57
+ **wrapper_opts
58
+ )
59
+ end
60
+
44
61
  def extract_options(options)
45
- [options.except(*FieldComponent::OPTIONS), options.slice(*FieldComponent::OPTIONS)]
62
+ [options.except(*Field::OPTIONS), options.slice(*Field::OPTIONS)]
46
63
  end
47
64
 
48
65
  def field_theme(key, **variants)
@@ -53,15 +70,45 @@ module StimulusPlumbers
53
70
  StimulusPlumbers.config.theme
54
71
  end
55
72
 
56
- # field_id was added in Rails 7.0. Provide a compatible implementation for Rails 6.1.
73
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
57
74
  if ActionView.version < "7.0"
58
- def field_id(attribute, *suffixes, index: @index, namespace: @options[:namespace])
59
- tokens = [namespace, @object_name, index, attribute, *suffixes]
60
- tokens.select!(&:present?)
61
- tokens.map! { |t| t.to_s.delete("]").tr("^-a-zA-Z0-9:.", "_") }
62
- tokens.join("_")
75
+ # field_id was added in Rails 7.0, backports it to Rails 6.1.
76
+ # https://github.com/rails/rails/blob/2d670320f7b02ae879545d5202f0633841b8f196/actionview/lib/action_view/helpers/form_helper.rb#L1777
77
+ # https://github.com/rails/rails/blob/2d670320f7b02ae879545d5202f0633841b8f196/actionview/lib/action_view/helpers/form_tag_helper.rb#L101
78
+ def field_id(method_name, *suffixes, namespace: @options[:namespace], index: @options[:index])
79
+ object_name = @object_name.respond_to?(:model_name) ? @object_name.model_name.singular : @object_name
80
+
81
+ sanitized_object_name = object_name.to_s.gsub(%r{\]\[|[^-a-zA-Z0-9:.]}, "_").delete_suffix("_")
82
+ sanitized_method_name = method_name.to_s.delete_suffix("?")
83
+
84
+ [
85
+ namespace,
86
+ sanitized_object_name.presence,
87
+ (index unless sanitized_object_name.empty?),
88
+ sanitized_method_name,
89
+ *suffixes
90
+ ].tap(&:compact!).join("_")
91
+ end
92
+
93
+ # field_name was added in Rails 7.0, backports it to Rails 6.1.
94
+ # https://github.com/rails/rails/blob/2d670320f7b02ae879545d5202f0633841b8f196/actionview/lib/action_view/helpers/form_helper.rb#L1797
95
+ # https://github.com/rails/rails/blob/2d670320f7b02ae879545d5202f0633841b8f196/actionview/lib/action_view/helpers/form_tag_helper.rb#L131
96
+ def field_name(method_name, *method_names, multiple: false, index: @options[:index])
97
+ object_name = @options.fetch(:as) { @object_name }
98
+
99
+ names = method_names.map! { |name| "[#{name}]" }.join
100
+
101
+ # a little duplication to construct fewer strings
102
+ if object_name.blank?
103
+ "#{method_name}#{names}#{"[]" if multiple}"
104
+ elsif index
105
+ "#{object_name}[#{index}][#{method_name}]#{names}#{"[]" if multiple}"
106
+ else
107
+ "#{object_name}[#{method_name}]#{names}#{"[]" if multiple}"
108
+ end
63
109
  end
64
110
  end
111
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
65
112
  end
66
113
  end
67
114
  end