stimulus_plumbers 0.2.8 → 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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +3 -0
  4. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.es.js +339 -302
  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 +30 -34
  19. data/lib/stimulus_plumbers/components/combobox/date.rb +16 -18
  20. data/lib/stimulus_plumbers/components/combobox/dropdown.rb +13 -16
  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 +32 -15
  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/navigator.rb +1 -1
  30. data/lib/stimulus_plumbers/components/icon.rb +49 -0
  31. data/lib/stimulus_plumbers/components/popover/builder.rb +25 -0
  32. data/lib/stimulus_plumbers/components/popover.rb +26 -0
  33. data/lib/stimulus_plumbers/form/builder.rb +7 -5
  34. data/lib/stimulus_plumbers/form/{field_component.rb → field.rb} +1 -1
  35. data/lib/stimulus_plumbers/form/fields/combobox.rb +1 -1
  36. data/lib/stimulus_plumbers/form/fields/error.rb +14 -0
  37. data/lib/stimulus_plumbers/form/fields/group.rb +14 -0
  38. data/lib/stimulus_plumbers/form/fields/hint.rb +14 -0
  39. data/lib/stimulus_plumbers/form/fields/label.rb +21 -0
  40. data/lib/stimulus_plumbers/form/fields/renderer.rb +16 -20
  41. data/lib/stimulus_plumbers/form/fields/search.rb +23 -9
  42. data/lib/stimulus_plumbers/form/fields/submit.rb +23 -0
  43. data/lib/stimulus_plumbers/helpers/action_list_helper.rb +2 -2
  44. data/lib/stimulus_plumbers/helpers/avatar_helper.rb +2 -2
  45. data/lib/stimulus_plumbers/helpers/button_helper.rb +2 -2
  46. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +1 -1
  47. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +1 -1
  48. data/lib/stimulus_plumbers/helpers/card_helper.rb +2 -2
  49. data/lib/stimulus_plumbers/helpers/combobox_helper.rb +5 -5
  50. data/lib/stimulus_plumbers/helpers/popover_helper.rb +2 -2
  51. data/lib/stimulus_plumbers/plumber/base.rb +20 -0
  52. data/lib/stimulus_plumbers/plumber/dispatcher.rb +111 -0
  53. data/lib/stimulus_plumbers/plumber/html_options.rb +51 -0
  54. data/lib/stimulus_plumbers/plumber/renderer.rb +89 -0
  55. data/lib/stimulus_plumbers/themes/base.rb +9 -15
  56. data/lib/stimulus_plumbers/themes/schema/ranges.rb +5 -5
  57. data/lib/stimulus_plumbers/themes/schema.rb +97 -0
  58. data/lib/stimulus_plumbers/themes/tailwind/calendar.rb +48 -2
  59. data/lib/stimulus_plumbers/themes/tailwind/combobox.rb +75 -0
  60. data/lib/stimulus_plumbers/themes/tailwind_theme.rb +2 -0
  61. data/lib/stimulus_plumbers/version.rb +1 -1
  62. data/lib/stimulus_plumbers.rb +29 -19
  63. metadata +33 -25
  64. data/lib/stimulus_plumbers/components/action_list/renderer.rb +0 -47
  65. data/lib/stimulus_plumbers/components/avatar/renderer.rb +0 -74
  66. data/lib/stimulus_plumbers/components/button/renderer.rb +0 -33
  67. data/lib/stimulus_plumbers/components/calendar/month/turbo/renderer.rb +0 -57
  68. data/lib/stimulus_plumbers/components/calendar/renderer.rb +0 -35
  69. data/lib/stimulus_plumbers/components/card/renderer.rb +0 -41
  70. data/lib/stimulus_plumbers/components/combobox/option.rb +0 -27
  71. data/lib/stimulus_plumbers/components/combobox/option_group.rb +0 -52
  72. data/lib/stimulus_plumbers/components/combobox/renderer.rb +0 -78
  73. data/lib/stimulus_plumbers/components/icon/renderer.rb +0 -51
  74. data/lib/stimulus_plumbers/components/plumber/base.rb +0 -22
  75. data/lib/stimulus_plumbers/components/plumber/dispatcher.rb +0 -113
  76. data/lib/stimulus_plumbers/components/plumber/html_options.rb +0 -53
  77. data/lib/stimulus_plumbers/components/plumber/renderer.rb +0 -91
  78. data/lib/stimulus_plumbers/components/popover/renderer.rb +0 -46
  79. data/lib/stimulus_plumbers/components/time_picker/renderer.rb +0 -38
  80. data/lib/stimulus_plumbers/themes/base/action_list.rb +0 -14
  81. data/lib/stimulus_plumbers/themes/base/avatar.rb +0 -14
  82. data/lib/stimulus_plumbers/themes/base/button.rb +0 -18
  83. data/lib/stimulus_plumbers/themes/base/calendar.rb +0 -15
  84. data/lib/stimulus_plumbers/themes/base/card.rb +0 -12
  85. data/lib/stimulus_plumbers/themes/base/form.rb +0 -34
  86. data/lib/stimulus_plumbers/themes/base/layout.rb +0 -12
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "action_view/version"
4
4
 
5
- require_relative "field_component"
5
+ require_relative "field"
6
6
  require_relative "fields/choice"
7
7
  require_relative "fields/combobox"
8
8
  require_relative "fields/file"
@@ -12,25 +12,27 @@ require_relative "fields/search"
12
12
  require_relative "fields/select"
13
13
  require_relative "fields/text"
14
14
  require_relative "fields/text_area"
15
- require_relative "../components/plumber/html_options"
15
+ require_relative "fields/submit"
16
+ require_relative "../plumber/html_options"
16
17
 
17
18
  module StimulusPlumbers
18
19
  module Form
19
20
  class Builder < ActionView::Helpers::FormBuilder
20
- include Components::Plumber::HtmlOptions
21
+ include Plumber::HtmlOptions
21
22
  include Fields::Choice
22
23
  include Fields::Combobox
23
24
  include Fields::File
24
25
  include Fields::Password
25
26
  include Fields::Search
26
27
  include Fields::Select
28
+ include Fields::Submit
27
29
  include Fields::Text
28
30
  include Fields::TextArea
29
31
 
30
32
  private
31
33
 
32
34
  def build_field(attribute, form_field_opts, input_id: field_id(attribute))
33
- FieldComponent.new(
35
+ Field.new(
34
36
  object: object,
35
37
  attribute: attribute,
36
38
  input_id: input_id,
@@ -57,7 +59,7 @@ module StimulusPlumbers
57
59
  end
58
60
 
59
61
  def extract_options(options)
60
- [options.except(*FieldComponent::OPTIONS), options.slice(*FieldComponent::OPTIONS)]
62
+ [options.except(*Field::OPTIONS), options.slice(*Field::OPTIONS)]
61
63
  end
62
64
 
63
65
  def field_theme(key, **variants)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module StimulusPlumbers
4
4
  module Form
5
- class FieldComponent
5
+ class Field
6
6
  OPTIONS = %i[label details error required label_visibility layout reveal clearable].freeze
7
7
 
8
8
  attr_reader :object,
@@ -27,7 +27,7 @@ module StimulusPlumbers
27
27
  input: { name: field_name(attribute), value: current_value },
28
28
  popover: { content: popover }
29
29
  )
30
- wrapper = Components::Combobox::Renderer.new(@template).render(
30
+ wrapper = Components::Combobox.new(@template).render(
31
31
  base_id: base_id,
32
32
  options: opts,
33
33
  **field_theme(:form_combobox, error: field.error?),
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ class Error < Plumber::Base
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")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ class Group < Plumber::Base
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)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ class Hint < Plumber::Base
7
+ def render(text:, id:)
8
+ klass = theme.resolve(:form_details).fetch(:classes, "")
9
+ template.content_tag(:p, text, id: id, class: klass.presence)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ class Label < Plumber::Base
7
+ def render(text:, for_id:, required: false, hidden: false)
8
+ klass = theme.resolve(:form_label, required: required, hidden: hidden).fetch(:classes, "")
9
+
10
+ inner = text.dup.html_safe
11
+ if required
12
+ mark_klass = theme.resolve(:form_required_mark).fetch(:classes, "")
13
+ inner += template.content_tag(:span, "*", "aria-hidden": "true", class: mark_klass.presence)
14
+ end
15
+
16
+ template.content_tag(:label, inner, for: for_id, class: klass.presence)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -13,41 +13,37 @@ module StimulusPlumbers
13
13
  end
14
14
 
15
15
  def call(input_html)
16
- field_klass = theme.resolve(:form_group, layout: field.layout, error: field.error?).fetch(:classes, "")
17
-
18
- template.content_tag(:div, class: field_klass) do
19
- label_html + input_html.html_safe + hint_html + errors_html
16
+ Group.new(template).render(layout: field.layout, error: field.error?) do
17
+ (field_label + input_html.html_safe + field_hint + field_errors).html_safe
20
18
  end
21
19
  end
22
20
 
23
21
  private
24
22
 
25
- def label_html
26
- klass = theme.resolve(:form_label, required: field.required, hidden: field.label_hidden?).fetch(:classes, "")
27
-
28
- inner = field.label_text.dup.html_safe
29
- if field.required
30
- mark_klass = theme.resolve(:form_required_mark).fetch(:classes, "")
31
- inner += template.content_tag(:span, "*", "aria-hidden": "true", class: mark_klass)
32
- end
33
-
34
- template.content_tag(:label, inner, for: field.input_id, class: klass)
23
+ def field_label
24
+ Label.new(template).render(
25
+ text: field.label_text,
26
+ for_id: field.input_id,
27
+ required: field.required,
28
+ hidden: field.label_hidden?
29
+ )
35
30
  end
36
31
 
37
- def hint_html
32
+ def field_hint
38
33
  return "".html_safe unless field.details.present?
39
34
 
40
- klass = theme.resolve(:form_details).fetch(:classes, "")
41
- template.content_tag(:p, field.details, id: field.hint_id, class: klass)
35
+ Hint.new(template).render(
36
+ text: field.details,
37
+ id: field.hint_id
38
+ )
42
39
  end
43
40
 
44
- def errors_html
41
+ def field_errors
45
42
  return "".html_safe if field.errors.none?
46
43
 
47
- klass = theme.resolve(:form_error).fetch(:classes, "")
48
44
  field.errors.map.with_index(1) do |message, i|
49
45
  id = field.errors.one? ? field.error_id : "#{field.error_id}_#{i}"
50
- template.content_tag(:p, message, id: id, class: klass, role: "alert")
46
+ Error.new(template).render(message: message, id: id)
51
47
  end.join.html_safe
52
48
  end
53
49
  end
@@ -9,14 +9,26 @@ module StimulusPlumbers
9
9
  clearable = form_field_opts.delete(:clearable) { false }
10
10
  field = build_field(attribute, form_field_opts)
11
11
 
12
- html_opts = merge_html_options(
13
- rails_opts,
14
- field_theme(:form_input, error: field.error?),
15
- field.html_opts
16
- )
17
12
  input_html = if clearable
18
- build_input_group(super(attribute, html_opts), field, trailing: clear_button)
13
+ input_opts = merge_html_options(
14
+ rails_opts,
15
+ field_theme(:form_input, error: field.error?),
16
+ field.html_opts,
17
+ { "data-input-search-target": "input", inputmode: "search" }
18
+ )
19
+ build_input_group(
20
+ super(attribute, input_opts),
21
+ field,
22
+ trailing: clear_button,
23
+ "data-controller": "input-search",
24
+ role: "search"
25
+ )
19
26
  else
27
+ html_opts = merge_html_options(
28
+ rails_opts,
29
+ field_theme(:form_input, error: field.error?),
30
+ field.html_opts
31
+ )
20
32
  super(attribute, html_opts)
21
33
  end
22
34
 
@@ -29,9 +41,11 @@ module StimulusPlumbers
29
41
  @template.content_tag(
30
42
  :button,
31
43
  "",
32
- type: "button",
33
- class: field_theme(:form_button_reveal)[:class],
34
- "aria-label": "Clear search"
44
+ type: "button",
45
+ class: field_theme(:form_button_reveal)[:class],
46
+ "aria-label": "Clear search",
47
+ "data-input-search-target": "clear",
48
+ "data-action": "click->input-search#clear"
35
49
  )
36
50
  end
37
51
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Form
5
+ module Fields
6
+ module Submit
7
+ def submit(value = nil, options = {})
8
+ if value.is_a?(Hash)
9
+ options = value
10
+ value = nil
11
+ end
12
+ value ||= submit_default_value
13
+ variant = options.delete(:variant) { :default }
14
+ @template.tag.input(
15
+ type: "submit",
16
+ value: value,
17
+ **merge_html_options(field_theme(:form_submit, variant: variant), options)
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -4,7 +4,7 @@ module StimulusPlumbers
4
4
  module Helpers
5
5
  module ActionListHelper
6
6
  def sp_action_list(**html_options, &block)
7
- action_list_renderer.list(**html_options, &block)
7
+ action_list_renderer.render(**html_options, &block)
8
8
  end
9
9
 
10
10
  def sp_action_list_section(title: nil, **html_options, &block)
@@ -18,7 +18,7 @@ module StimulusPlumbers
18
18
  private
19
19
 
20
20
  def action_list_renderer
21
- Components::ActionList::Renderer.new(self)
21
+ Components::ActionList.new(self)
22
22
  end
23
23
  end
24
24
  end
@@ -4,13 +4,13 @@ module StimulusPlumbers
4
4
  module Helpers
5
5
  module AvatarHelper
6
6
  def sp_avatar(name: nil, initials: nil, url: nil, color: nil, size: :md, **html_options, &block)
7
- avatar_renderer.avatar(name: name, initials: initials, url: url, color: color, size: size, **html_options, &block)
7
+ avatar_renderer.render(name: name, initials: initials, url: url, color: color, size: size, **html_options, &block)
8
8
  end
9
9
 
10
10
  private
11
11
 
12
12
  def avatar_renderer
13
- Components::Avatar::Renderer.new(self)
13
+ Components::Avatar.new(self)
14
14
  end
15
15
  end
16
16
  end
@@ -4,7 +4,7 @@ module StimulusPlumbers
4
4
  module Helpers
5
5
  module ButtonHelper
6
6
  def sp_button(content = nil, url: nil, external: false, variant: :primary, size: :md, **html_options, &block)
7
- button_renderer.button(
7
+ button_renderer.render(
8
8
  content,
9
9
  url: url, external: external, variant: variant, size: size, **html_options,
10
10
  &block
@@ -18,7 +18,7 @@ module StimulusPlumbers
18
18
  private
19
19
 
20
20
  def button_renderer
21
- Components::Button::Renderer.new(self)
21
+ Components::Button.new(self)
22
22
  end
23
23
  end
24
24
  end
@@ -19,7 +19,7 @@ module StimulusPlumbers
19
19
  private
20
20
 
21
21
  def calendar_renderer
22
- Components::Calendar::Renderer.new(self)
22
+ Components::Calendar.new(self)
23
23
  end
24
24
  end
25
25
  end
@@ -24,7 +24,7 @@ module StimulusPlumbers
24
24
  private
25
25
 
26
26
  def calendar_month_turbo_renderer
27
- Components::Calendar::Month::Turbo::Renderer.new(self)
27
+ Components::Calendar::Month::Turbo.new(self)
28
28
  end
29
29
  end
30
30
  end
@@ -4,7 +4,7 @@ module StimulusPlumbers
4
4
  module Helpers
5
5
  module CardHelper
6
6
  def sp_card(title: nil, **html_options, &block)
7
- card_renderer.card(title: title, **html_options, &block)
7
+ card_renderer.render(title: title, **html_options, &block)
8
8
  end
9
9
 
10
10
  def sp_card_section(title: nil, **html_options, &block)
@@ -14,7 +14,7 @@ module StimulusPlumbers
14
14
  private
15
15
 
16
16
  def card_renderer
17
- Components::Card::Renderer.new(self)
17
+ Components::Card.new(self)
18
18
  end
19
19
  end
20
20
  end
@@ -9,7 +9,7 @@ module StimulusPlumbers
9
9
  popover: { content: Components::Combobox::Date.new(self).render(value: value) }
10
10
  )
11
11
  opts = opts.deep_merge(trigger: { aria_label: label }) if label
12
- Components::Combobox::Renderer.new(self).render(
12
+ Components::Combobox.new(self).render(
13
13
  base_id: sp_dom_id,
14
14
  options: opts,
15
15
  data: { input_format_type_value: "date" },
@@ -23,7 +23,7 @@ module StimulusPlumbers
23
23
  popover: { content: Components::Combobox::Dropdown.new(self).render(options: options, value: value, label: label) }
24
24
  )
25
25
  opts = opts.deep_merge(trigger: { aria_label: label }) if label
26
- Components::Combobox::Renderer.new(self).render(
26
+ Components::Combobox.new(self).render(
27
27
  base_id: sp_dom_id,
28
28
  options: opts,
29
29
  **html_options
@@ -45,12 +45,12 @@ module StimulusPlumbers
45
45
  }
46
46
  )
47
47
  opts = opts.deep_merge(trigger: { aria_label: label }) if label
48
- Components::Combobox::Renderer.new(self).render(
48
+ Components::Combobox.new(self).render(
49
49
  base_id: id,
50
50
  options: opts,
51
51
  data: {
52
52
  input_combobox_combobox_dropdown_outlet: "##{popover_id}",
53
- action: "input->input-combobox#filter"
53
+ action: "input->input-combobox#onInput"
54
54
  },
55
55
  **html_options
56
56
  )
@@ -62,7 +62,7 @@ module StimulusPlumbers
62
62
  popover: { content: Components::Combobox::Time.new(self).render(format: format, step: step, value: value) }
63
63
  )
64
64
  opts = opts.deep_merge(trigger: { aria_label: label }) if label
65
- Components::Combobox::Renderer.new(self).render(
65
+ Components::Combobox.new(self).render(
66
66
  base_id: sp_dom_id,
67
67
  options: opts,
68
68
  data: { input_format_type_value: "time", input_format_options_value: { format: format }.to_json },
@@ -4,13 +4,13 @@ module StimulusPlumbers
4
4
  module Helpers
5
5
  module PopoverHelper
6
6
  def sp_popover(interactive: true, **html_options, &block)
7
- popover_renderer.popover(interactive: interactive, **html_options, &block)
7
+ popover_renderer.render(interactive: interactive, **html_options, &block)
8
8
  end
9
9
 
10
10
  private
11
11
 
12
12
  def popover_renderer
13
- Components::Popover::Renderer.new(self)
13
+ Components::Popover.new(self)
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Plumber
5
+ class Base
6
+ include HtmlOptions
7
+ include Renderer
8
+
9
+ attr_reader :template
10
+
11
+ def initialize(template)
12
+ @template = template
13
+ end
14
+
15
+ def theme
16
+ StimulusPlumbers.config.theme
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module StimulusPlumbers
6
+ module Plumber
7
+ module Dispatcher
8
+ class MethodCall
9
+ attr_reader :method_name, :args, :kwargs
10
+
11
+ def initialize(method_name, *args, **kwargs)
12
+ @method_name = method_name
13
+ @args = args
14
+ @kwargs = kwargs
15
+ validate!
16
+ end
17
+
18
+ def call(target)
19
+ raise NotImplementedError, "#{method_name.inspect} not implemented" unless target.respond_to?(method_name, true)
20
+
21
+ method_call = target.method(method_name)
22
+ accepts_args = method_call.arity.negative? ? args : args.take(method_call.arity)
23
+ accepts_kwargs = method_call.parameters.any? { |type, _| %i[key keyreq keyrest].include?(type) }
24
+ accepts_kwargs ? method_call.call(*accepts_args, **kwargs) : method_call.call(*accepts_args)
25
+ end
26
+
27
+ private
28
+
29
+ def validate!
30
+ return if method_name.is_a?(String) || method_name.is_a?(Symbol)
31
+
32
+ raise ArgumentError, "invalid method name: #{method_name.inspect}"
33
+ end
34
+ end
35
+
36
+ class InstanceExec
37
+ attr_reader :block, :args, :kwargs
38
+
39
+ def initialize(block, *args, **kwargs)
40
+ @block = block
41
+ @args = args
42
+ @kwargs = kwargs
43
+ validate!
44
+ end
45
+
46
+ def call(target)
47
+ accepts_args = block.arity.negative? ? args : args.take(block.arity)
48
+ accepts_kwargs = block.parameters.any? { |type, _| %i[key keyreq keyrest].include?(type) }
49
+ if accepts_kwargs
50
+ target.instance_exec(
51
+ *accepts_args,
52
+ **kwargs,
53
+ &block
54
+ )
55
+ else
56
+ target.instance_exec(*accepts_args, &block)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def validate!
63
+ raise ArgumentError, "invalid block: #{block.inspect}" unless block.is_a?(Proc)
64
+ end
65
+ end
66
+
67
+ class KlassProxy
68
+ attr_reader :klass, :method_name, :args, :kwargs, :init_args, :init_kwargs
69
+
70
+ def initialize(klass, method_name, *args, init_args: [], init_kwargs: {}, **kwargs)
71
+ @klass = klass
72
+ @method_name = method_name
73
+ @args = args
74
+ @kwargs = kwargs
75
+ @init_args = init_args
76
+ @init_kwargs = init_kwargs
77
+ validate!
78
+ end
79
+
80
+ def call(_target)
81
+ klass.new(*init_args, **init_kwargs).public_send(method_name, *args, **kwargs)
82
+ end
83
+
84
+ private
85
+
86
+ def validate!
87
+ raise ArgumentError, "invalid class: #{klass.inspect}" unless klass.is_a?(Module)
88
+ return if method_name.is_a?(String) || method_name.is_a?(Symbol)
89
+
90
+ raise ArgumentError, "invalid method name: #{method_name.inspect}"
91
+ end
92
+ end
93
+
94
+ def self.build(callable, *args, method_name: nil, init_args: [], init_kwargs: {}, **kwargs)
95
+ case callable
96
+ when Symbol
97
+ MethodCall.new(callable, *args, **kwargs)
98
+ when Proc
99
+ InstanceExec.new(callable, *args, **kwargs)
100
+ when Module
101
+ KlassProxy.new(callable, method_name, *args, init_args: init_args, init_kwargs: init_kwargs, **kwargs)
102
+ when String
103
+ klass = callable.safe_constantize
104
+ raise ArgumentError, "could not resolve class from: #{callable.inspect}" unless klass
105
+
106
+ KlassProxy.new(klass, method_name, *args, init_args: init_args, init_kwargs: init_kwargs, **kwargs)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module StimulusPlumbers
6
+ module Plumber
7
+ module HtmlOptions
8
+ extend ActiveSupport::Concern
9
+
10
+ def merge_html_options(*hashes)
11
+ classes = hashes.flat_map { |h| [h[:class], h[:classes]] }
12
+ data_hashes = hashes.map { |h| h[:data] || {} }
13
+ rest = hashes.map { |h| h.except(:class, :classes, :data) }.reduce({}, :deep_merge)
14
+
15
+ class_value = merge_string_option(*classes).presence
16
+ merged_data = merge_data_options(*data_hashes)
17
+
18
+ result = class_value ? rest.merge(class: class_value) : rest
19
+ merged_data.present? ? result.merge(data: merged_data) : result
20
+ end
21
+
22
+ STIMULUS_SPACEJOIN_KEYS = %i[controller action].freeze
23
+
24
+ def merge_data_options(*hashes, spacejoin: STIMULUS_SPACEJOIN_KEYS)
25
+ hashes.reduce({}) do |acc, d|
26
+ acc.merge(d) do |key, old_val, new_val|
27
+ if spacejoin.include?(key.to_sym)
28
+ merge_string_option(old_val, new_val).presence || new_val
29
+ else
30
+ new_val
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def merge_string_option(*parts, delimiter: " ")
37
+ tokens = parts.flat_map { |part| normalize_part(part, delimiter) }
38
+ tokens.compact.uniq.join(delimiter)
39
+ end
40
+
41
+ def normalize_part(value, delimiter)
42
+ case value
43
+ when String then value.present? ? value.split(delimiter) : []
44
+ when Hash then value.filter_map { |key, val| key if val }
45
+ when Array then [merge_string_option(*value).presence]
46
+ else []
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end