stimulus_plumbers 0.3.2 → 0.4.0

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +3 -1
  4. data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +273 -0
  5. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.es.js +269 -160
  6. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.umd.js +1 -1
  7. data/app/assets/stylesheets/stimulus_plumbers/tokens.css +56 -13
  8. data/config/locales/en.yml +10 -0
  9. data/lib/stimulus_plumbers/components/avatar.rb +24 -17
  10. data/lib/stimulus_plumbers/components/button/group.rb +15 -4
  11. data/lib/stimulus_plumbers/components/button/slots.rb +11 -0
  12. data/lib/stimulus_plumbers/components/button.rb +45 -11
  13. data/lib/stimulus_plumbers/components/calendar/turbo/days_of_month.rb +151 -0
  14. data/lib/stimulus_plumbers/components/calendar/turbo/days_of_week.rb +62 -0
  15. data/lib/stimulus_plumbers/components/calendar/turbo/months_of_year.rb +99 -0
  16. data/lib/stimulus_plumbers/components/calendar/turbo/years_of_decade.rb +86 -0
  17. data/lib/stimulus_plumbers/components/calendar/turbo.rb +65 -0
  18. data/lib/stimulus_plumbers/components/calendar.rb +70 -26
  19. data/lib/stimulus_plumbers/components/card/slots.rb +26 -0
  20. data/lib/stimulus_plumbers/components/card.rb +56 -10
  21. data/lib/stimulus_plumbers/components/combobox/builder.rb +45 -0
  22. data/lib/stimulus_plumbers/components/combobox/date/navigation.rb +72 -0
  23. data/lib/stimulus_plumbers/components/combobox/date/navigator.rb +25 -0
  24. data/lib/stimulus_plumbers/components/combobox/date.rb +37 -23
  25. data/lib/stimulus_plumbers/components/combobox/dropdown.rb +30 -21
  26. data/lib/stimulus_plumbers/components/combobox/options/option.rb +8 -2
  27. data/lib/stimulus_plumbers/components/combobox/options/option_group.rb +8 -2
  28. data/lib/stimulus_plumbers/components/combobox/options.rb +9 -5
  29. data/lib/stimulus_plumbers/components/combobox/time/drum.rb +8 -2
  30. data/lib/stimulus_plumbers/components/combobox/time.rb +50 -47
  31. data/lib/stimulus_plumbers/components/combobox/trigger.rb +62 -14
  32. data/lib/stimulus_plumbers/components/combobox/typeahead.rb +96 -0
  33. data/lib/stimulus_plumbers/components/combobox.rb +62 -38
  34. data/lib/stimulus_plumbers/components/divider.rb +25 -4
  35. data/lib/stimulus_plumbers/components/icon.rb +11 -17
  36. data/lib/stimulus_plumbers/components/input_group.rb +29 -0
  37. data/lib/stimulus_plumbers/components/link/slots.rb +11 -0
  38. data/lib/stimulus_plumbers/components/link.rb +63 -0
  39. data/lib/stimulus_plumbers/components/list/item/slots.rb +13 -0
  40. data/lib/stimulus_plumbers/components/list/item.rb +83 -0
  41. data/lib/stimulus_plumbers/components/list/section.rb +73 -0
  42. data/lib/stimulus_plumbers/components/list.rb +31 -0
  43. data/lib/stimulus_plumbers/components/popover/panel.rb +32 -0
  44. data/lib/stimulus_plumbers/components/popover/trigger.rb +27 -0
  45. data/lib/stimulus_plumbers/components/popover.rb +44 -14
  46. data/lib/stimulus_plumbers/engine.rb +1 -0
  47. data/lib/stimulus_plumbers/form/base.rb +103 -0
  48. data/lib/stimulus_plumbers/form/builder.rb +71 -24
  49. data/lib/stimulus_plumbers/form/field.rb +56 -88
  50. data/lib/stimulus_plumbers/form/fields/error.rb +1 -1
  51. data/lib/stimulus_plumbers/form/fields/fieldset.rb +11 -8
  52. data/lib/stimulus_plumbers/form/fields/hint.rb +1 -1
  53. data/lib/stimulus_plumbers/form/fields/inputs/checkbox.rb +115 -0
  54. data/lib/stimulus_plumbers/form/fields/inputs/combobox.rb +24 -0
  55. data/lib/stimulus_plumbers/form/fields/inputs/datetime.rb +42 -48
  56. data/lib/stimulus_plumbers/form/fields/inputs/file.rb +9 -8
  57. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +32 -25
  58. data/lib/stimulus_plumbers/form/fields/inputs/radio.rb +60 -0
  59. data/lib/stimulus_plumbers/form/fields/inputs/search.rb +34 -57
  60. data/lib/stimulus_plumbers/form/fields/inputs/select/grouped.rb +22 -29
  61. data/lib/stimulus_plumbers/form/fields/inputs/select/timezone.rb +3 -44
  62. data/lib/stimulus_plumbers/form/fields/inputs/select/weekday.rb +3 -28
  63. data/lib/stimulus_plumbers/form/fields/inputs/select.rb +62 -49
  64. data/lib/stimulus_plumbers/form/fields/inputs/submit.rb +10 -7
  65. data/lib/stimulus_plumbers/form/fields/inputs/text.rb +29 -22
  66. data/lib/stimulus_plumbers/form/fields/inputs/text_area.rb +9 -8
  67. data/lib/stimulus_plumbers/form/fields/label/floating.rb +41 -0
  68. data/lib/stimulus_plumbers/form/fields/label.rb +9 -3
  69. data/lib/stimulus_plumbers/form/fields/renderer.rb +39 -0
  70. data/lib/stimulus_plumbers/helpers/avatar_helper.rb +2 -2
  71. data/lib/stimulus_plumbers/helpers/button_helper.rb +4 -8
  72. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +14 -11
  73. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +49 -11
  74. data/lib/stimulus_plumbers/helpers/card_helper.rb +2 -12
  75. data/lib/stimulus_plumbers/helpers/combobox_helper.rb +27 -47
  76. data/lib/stimulus_plumbers/helpers/divider_helper.rb +2 -2
  77. data/lib/stimulus_plumbers/helpers/icon_helper.rb +11 -0
  78. data/lib/stimulus_plumbers/helpers/link_helper.rb +11 -0
  79. data/lib/stimulus_plumbers/helpers/list_helper.rb +11 -0
  80. data/lib/stimulus_plumbers/helpers/plumber_helper.rb +3 -6
  81. data/lib/stimulus_plumbers/helpers/popover_helper.rb +2 -2
  82. data/lib/stimulus_plumbers/helpers.rb +6 -2
  83. data/lib/stimulus_plumbers/logger.rb +4 -3
  84. data/lib/stimulus_plumbers/plumber/base.rb +6 -1
  85. data/lib/stimulus_plumbers/plumber/dispatcher/klass_proxy.rb +4 -3
  86. data/lib/stimulus_plumbers/plumber/dispatcher/method_call.rb +4 -3
  87. data/lib/stimulus_plumbers/plumber/dispatcher.rb +4 -4
  88. data/lib/stimulus_plumbers/plumber/options/aria.rb +17 -0
  89. data/lib/stimulus_plumbers/plumber/options/html.rb +29 -0
  90. data/lib/stimulus_plumbers/plumber/options/stimulus.rb +29 -0
  91. data/lib/stimulus_plumbers/plumber/options/theme.rb +19 -0
  92. data/lib/stimulus_plumbers/plumber/options/token_list.rb +29 -0
  93. data/lib/stimulus_plumbers/plumber/renderer.rb +136 -41
  94. data/lib/stimulus_plumbers/plumber/slots.rb +74 -0
  95. data/lib/stimulus_plumbers/themes/base.rb +20 -23
  96. data/lib/stimulus_plumbers/themes/icons/external.rb +60 -0
  97. data/lib/stimulus_plumbers/themes/icons/registry.rb +36 -0
  98. data/lib/stimulus_plumbers/themes/schema/avatar/ranges.rb +13 -0
  99. data/lib/stimulus_plumbers/themes/schema/button/ranges.rb +16 -0
  100. data/lib/stimulus_plumbers/themes/schema/card/ranges.rb +13 -0
  101. data/lib/stimulus_plumbers/themes/schema/form/checkbox/ranges.rb +16 -0
  102. data/lib/stimulus_plumbers/themes/schema/form/radio/ranges.rb +16 -0
  103. data/lib/stimulus_plumbers/themes/schema/form/ranges.rb +1 -2
  104. data/lib/stimulus_plumbers/themes/schema/icon.rb +57 -15
  105. data/lib/stimulus_plumbers/themes/schema/link/ranges.rb +14 -0
  106. data/lib/stimulus_plumbers/themes/schema/ranges.rb +1 -5
  107. data/lib/stimulus_plumbers/themes/schema.rb +142 -67
  108. data/lib/stimulus_plumbers/version.rb +1 -1
  109. data/lib/stimulus_plumbers.rb +22 -17
  110. metadata +46 -17
  111. data/lib/stimulus_plumbers/components/action_list/item.rb +0 -27
  112. data/lib/stimulus_plumbers/components/action_list/section.rb +0 -22
  113. data/lib/stimulus_plumbers/components/action_list.rb +0 -23
  114. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_month.rb +0 -145
  115. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_week.rb +0 -39
  116. data/lib/stimulus_plumbers/components/calendar/month/turbo.rb +0 -55
  117. data/lib/stimulus_plumbers/components/card/section.rb +0 -25
  118. data/lib/stimulus_plumbers/components/combobox/autocomplete.rb +0 -47
  119. data/lib/stimulus_plumbers/components/combobox/popover.rb +0 -24
  120. data/lib/stimulus_plumbers/components/date_picker/navigation.rb +0 -41
  121. data/lib/stimulus_plumbers/components/date_picker/navigator.rb +0 -31
  122. data/lib/stimulus_plumbers/components/popover/builder.rb +0 -25
  123. data/lib/stimulus_plumbers/form/fields/input_group.rb +0 -25
  124. data/lib/stimulus_plumbers/form/fields/inputs/choice.rb +0 -69
  125. data/lib/stimulus_plumbers/helpers/action_list_helper.rb +0 -25
  126. data/lib/stimulus_plumbers/plumber/html_options.rb +0 -52
@@ -3,63 +3,43 @@
3
3
  module StimulusPlumbers
4
4
  module Helpers
5
5
  module ComboboxHelper
6
- def sp_combobox_date(label: nil, value: nil, **html_options)
7
- id = sp_dom_id
8
- opts = Components::Combobox::Date.default_opts.deep_merge(
9
- input: { value: value },
10
- trigger: { id: id, aria_label: label }.compact
11
- )
6
+ # Single entry point; panel type chosen by a block method (c.dropdown/date/...).
7
+ def sp_combobox(value: nil, label: nil, id: nil, close_on_select: nil, **kwargs, &block)
12
8
  Components::Combobox.new(self).render(
13
- **opts,
14
- data: { input_format_type_value: "date" },
15
- **html_options
16
- ) do |popover_id|
17
- Components::Combobox::Date.new(self).render(value: value, popover_id: popover_id)
9
+ id: id || sp_dom_id,
10
+ label: label,
11
+ input: { value: value },
12
+ close_on_select: close_on_select,
13
+ **kwargs,
14
+ &block
15
+ )
16
+ end
17
+
18
+ def sp_combobox_date(value: nil, label: nil, **kwargs)
19
+ sp_combobox(value: value, label: label, **kwargs) do |c|
20
+ panel_opts = { value: value }
21
+ panel_opts[:label] = label if label
22
+ c.date(**panel_opts)
18
23
  end
19
24
  end
20
25
 
21
- def sp_combobox_dropdown(label: nil, options: [], value: nil, **html_options)
22
- id = sp_dom_id
23
- opts = Components::Combobox::Dropdown.default_opts.deep_merge(
24
- input: { value: value },
25
- trigger: { id: id, aria_label: label }.compact
26
- )
27
- Components::Combobox.new(self).render(**opts, **html_options) do
28
- Components::Combobox::Dropdown.new(self).render(options: options, value: value, label: label)
26
+ def sp_combobox_dropdown(options: [], value: nil, label: nil, **kwargs)
27
+ sp_combobox(value: value, label: label, **kwargs) do |c|
28
+ c.dropdown(options: options, value: value, label: label)
29
29
  end
30
30
  end
31
31
 
32
- def sp_combobox_autocomplete(label: nil, options: [], value: nil, url: nil, **html_options)
33
- id = sp_dom_id
34
- opts = Components::Combobox::Autocomplete.default_opts.deep_merge(
35
- input: { value: value },
36
- trigger: { id: id, aria_label: label }.compact,
37
- popover: { data: url ? { combobox_dropdown_url_value: url } : {} }
38
- )
39
- Components::Combobox.new(self).render(
40
- **opts,
41
- data: {
42
- input_combobox_combobox_dropdown_outlet: "##{Components::Combobox.popover_id_for(id)}",
43
- action: "input->input-combobox#onInput"
44
- },
45
- **html_options
46
- ) do
47
- Components::Combobox::Autocomplete.new(self).render(options: options, value: value, label: label)
32
+ def sp_combobox_typeahead(options: [], value: nil, label: nil, url: nil, **kwargs)
33
+ sp_combobox(value: value, label: label, **kwargs) do |c|
34
+ c.typeahead(options: options, value: value, label: label, url: url)
48
35
  end
49
36
  end
50
37
 
51
- def sp_combobox_time(format: :h12, label: nil, step: 1, value: nil, **html_options)
52
- id = sp_dom_id
53
- opts = Components::Combobox::Time.default_opts.deep_merge(
54
- input: { value: value },
55
- trigger: { id: id, aria_label: label }.compact
56
- )
57
- Components::Combobox.new(self).render(
58
- **opts,
59
- data: { input_format_type_value: "time", input_format_options_value: { format: format }.to_json },
60
- **html_options
61
- ) do
62
- Components::Combobox::Time.new(self).render(format: format, step: step, value: value)
38
+ def sp_combobox_time(format: :h12, step: 1, value: nil, label: nil, **kwargs)
39
+ sp_combobox(value: value, label: label, **kwargs) do |c|
40
+ panel_opts = { format: format, step: step, value: value }
41
+ panel_opts[:label] = label if label
42
+ c.time(**panel_opts)
63
43
  end
64
44
  end
65
45
  end
@@ -3,8 +3,8 @@
3
3
  module StimulusPlumbers
4
4
  module Helpers
5
5
  module DividerHelper
6
- def sp_divider(**html_options)
7
- Components::Divider.new(self).render(**html_options)
6
+ def sp_divider(label = nil, **kwargs)
7
+ Components::Divider.new(self).render(label, **kwargs)
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Helpers
5
+ module IconHelper
6
+ def sp_icon(name:, **kwargs)
7
+ Components::Icon.new(self).render(name: name, **kwargs)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Helpers
5
+ module LinkHelper
6
+ def sp_link(content = nil, **kwargs, &block)
7
+ Components::Link.new(self).render(content, **kwargs, &block)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Helpers
5
+ module ListHelper
6
+ def sp_list(...)
7
+ Components::List.new(self).render(...)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -3,12 +3,9 @@
3
3
  module StimulusPlumbers
4
4
  module Helpers
5
5
  module PlumberHelper
6
- def sp_dom_id(record = nil, suffix = nil)
7
- if record
8
- dom_id(record, suffix)
9
- else
10
- "#{suffix}_#{SecureRandom.hex(8)}"
11
- end
6
+ def sp_dom_id(record = nil, prefix: nil, suffix: nil)
7
+ base = record ? dom_id(record, prefix) : SecureRandom.hex(8)
8
+ ["sp", base, suffix].compact.join("_")
12
9
  end
13
10
  end
14
11
  end
@@ -3,8 +3,8 @@
3
3
  module StimulusPlumbers
4
4
  module Helpers
5
5
  module PopoverHelper
6
- def sp_popover(interactive: true, **html_options, &block)
7
- popover_renderer.render(interactive: interactive, **html_options, &block)
6
+ def sp_popover(...)
7
+ popover_renderer.render(...)
8
8
  end
9
9
 
10
10
  private
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "helpers/plumber_helper"
4
- require_relative "helpers/action_list_helper"
4
+ require_relative "helpers/icon_helper"
5
+ require_relative "helpers/list_helper"
5
6
  require_relative "helpers/avatar_helper"
6
7
  require_relative "helpers/button_helper"
7
8
  require_relative "helpers/calendar_helper"
@@ -9,12 +10,14 @@ require_relative "helpers/calendar_turbo_helper"
9
10
  require_relative "helpers/card_helper"
10
11
  require_relative "helpers/combobox_helper"
11
12
  require_relative "helpers/divider_helper"
13
+ require_relative "helpers/link_helper"
12
14
  require_relative "helpers/popover_helper"
13
15
 
14
16
  module StimulusPlumbers
15
17
  module Helpers
16
18
  include PlumberHelper
17
- include ActionListHelper
19
+ include IconHelper
20
+ include ListHelper
18
21
  include AvatarHelper
19
22
  include ButtonHelper
20
23
  include CalendarHelper
@@ -22,6 +25,7 @@ module StimulusPlumbers
22
25
  include CardHelper
23
26
  include ComboboxHelper
24
27
  include DividerHelper
28
+ include LinkHelper
25
29
  include PopoverHelper
26
30
  end
27
31
  end
@@ -4,17 +4,18 @@ module StimulusPlumbers
4
4
  module Logger
5
5
  LEVELS = %i[debug info warn error].freeze
6
6
 
7
- module_function
8
-
9
7
  LEVELS.each do |level|
10
8
  define_method(level) do |message|
11
9
  tagged = StimulusPlumbers.config.log_formatter.call(message)
12
10
  if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
13
11
  Rails.logger.public_send(level, tagged)
14
12
  else
15
- warn(tagged)
13
+ # Kernel.warn, not bare `warn` which would recurse into this module's own warn.
14
+ Kernel.warn(tagged)
16
15
  end
17
16
  end
18
17
  end
18
+
19
+ module_function(*LEVELS)
19
20
  end
20
21
  end
@@ -1,9 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "options/html"
4
+ require_relative "options/aria"
5
+ require_relative "renderer"
6
+
3
7
  module StimulusPlumbers
4
8
  module Plumber
5
9
  class Base
6
- include HtmlOptions
10
+ include Options::Html
11
+ include Options::Aria
7
12
  include Renderer
8
13
 
9
14
  attr_reader :template
@@ -4,20 +4,21 @@ module StimulusPlumbers
4
4
  module Plumber
5
5
  module Dispatcher
6
6
  class KlassProxy
7
- attr_reader :klass, :method_name, :args, :kwargs, :init_args, :init_kwargs
7
+ attr_reader :klass, :method_name, :args, :kwargs, :init_args, :init_kwargs, :block
8
8
 
9
- def initialize(klass, method_name, *args, init_args: [], init_kwargs: {}, **kwargs)
9
+ def initialize(klass, method_name, *args, init_args: [], init_kwargs: {}, **kwargs, &block)
10
10
  @klass = klass
11
11
  @method_name = method_name
12
12
  @args = args
13
13
  @kwargs = kwargs
14
14
  @init_args = init_args
15
15
  @init_kwargs = init_kwargs
16
+ @block = block
16
17
  validate!
17
18
  end
18
19
 
19
20
  def call(_target)
20
- klass.new(*init_args, **init_kwargs).public_send(method_name, *args, **kwargs)
21
+ klass.new(*init_args, **init_kwargs).public_send(method_name, *args, **kwargs, &@block)
21
22
  end
22
23
 
23
24
  private
@@ -6,12 +6,13 @@ module StimulusPlumbers
6
6
  class MethodCall
7
7
  include CallableInspector
8
8
 
9
- attr_reader :method_name, :args, :kwargs
9
+ attr_reader :method_name, :args, :kwargs, :block
10
10
 
11
- def initialize(method_name, *args, **kwargs)
11
+ def initialize(method_name, *args, **kwargs, &block)
12
12
  @method_name = method_name
13
13
  @args = args
14
14
  @kwargs = kwargs
15
+ @block = block
15
16
  validate!
16
17
  end
17
18
 
@@ -20,7 +21,7 @@ module StimulusPlumbers
20
21
 
21
22
  method_call = target.method(method_name)
22
23
  dispatched = args_for(method_call)
23
- accepts_kwargs?(method_call) ? method_call.call(*dispatched, **kwargs) : method_call.call(*dispatched)
24
+ accepts_kwargs?(method_call) ? method_call.call(*dispatched, **kwargs, &@block) : method_call.call(*dispatched, &@block)
24
25
  end
25
26
 
26
27
  private
@@ -8,19 +8,19 @@ require_relative "dispatcher/klass_proxy"
8
8
  module StimulusPlumbers
9
9
  module Plumber
10
10
  module Dispatcher
11
- def self.build(callable, *args, method_name: nil, init_args: [], init_kwargs: {}, **kwargs)
11
+ def self.build(callable, *args, method_name: nil, init_args: [], init_kwargs: {}, **kwargs, &block)
12
12
  case callable
13
13
  when Symbol
14
- MethodCall.new(callable, *args, **kwargs)
14
+ MethodCall.new(callable, *args, **kwargs, &block)
15
15
  when Proc
16
16
  InstanceExec.new(callable, *args, **kwargs)
17
17
  when Module
18
- KlassProxy.new(callable, method_name, *args, init_args: init_args, init_kwargs: init_kwargs, **kwargs)
18
+ KlassProxy.new(callable, method_name, *args, init_args: init_args, init_kwargs: init_kwargs, **kwargs, &block)
19
19
  when String
20
20
  klass = callable.safe_constantize
21
21
  raise ArgumentError, "could not resolve class from: #{callable.inspect}" unless klass
22
22
 
23
- KlassProxy.new(klass, method_name, *args, init_args: init_args, init_kwargs: init_kwargs, **kwargs)
23
+ KlassProxy.new(klass, method_name, *args, init_args: init_args, init_kwargs: init_kwargs, **kwargs, &block)
24
24
  end
25
25
  end
26
26
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module StimulusPlumbers
6
+ module Plumber
7
+ module Options
8
+ module Aria
9
+ extend ActiveSupport::Concern
10
+
11
+ def labelled_aria(label, labelledby: nil)
12
+ { label: (label unless labelledby), labelledby: labelledby }.compact
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require_relative "theme"
5
+ require_relative "stimulus"
6
+
7
+ module StimulusPlumbers
8
+ module Plumber
9
+ module Options
10
+ module Html
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ include Theme
15
+ include Stimulus
16
+ end
17
+
18
+ def merge_html_options(*hashes)
19
+ class_value = merge_theme_options(*hashes)
20
+ merged_data = merge_stimulus_data(*hashes.map { |h| h[:data] || {} })
21
+ rest = hashes.map { |h| h.except(:class, :classes, :data) }.reduce({}, :deep_merge)
22
+
23
+ result = class_value ? rest.merge(class: class_value) : rest
24
+ merged_data.present? ? result.merge(data: merged_data) : result
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require_relative "token_list"
5
+
6
+ module StimulusPlumbers
7
+ module Plumber
8
+ module Options
9
+ module Stimulus
10
+ extend ActiveSupport::Concern
11
+ include TokenList
12
+
13
+ STIMULUS_SPACEJOIN_KEYS = %i[controller action].freeze
14
+
15
+ def merge_stimulus_data(*hashes, spacejoin: STIMULUS_SPACEJOIN_KEYS)
16
+ hashes.reduce({}) do |acc, d|
17
+ acc.merge(d) do |key, old_val, new_val|
18
+ if spacejoin.include?(key.to_sym)
19
+ merge_token_list(old_val, new_val).presence || new_val
20
+ else
21
+ new_val
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require_relative "token_list"
5
+
6
+ module StimulusPlumbers
7
+ module Plumber
8
+ module Options
9
+ module Theme
10
+ extend ActiveSupport::Concern
11
+ include TokenList
12
+
13
+ def merge_theme_options(*hashes)
14
+ merge_token_list(*hashes.flat_map { |h| [h[:class], h[:classes]] }).presence
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module StimulusPlumbers
6
+ module Plumber
7
+ module Options
8
+ module TokenList
9
+ extend ActiveSupport::Concern
10
+
11
+ def merge_token_list(*parts, delimiter: " ")
12
+ tokens = parts.flat_map { |part| normalize_part(part, delimiter) }
13
+ tokens.compact.uniq.join(delimiter)
14
+ end
15
+
16
+ private
17
+
18
+ def normalize_part(value, delimiter)
19
+ case value
20
+ when String then value.present? ? value.split(delimiter) : []
21
+ when Hash then value.filter_map { |key, val| key if val }
22
+ when Array then [merge_token_list(*value).presence]
23
+ else []
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
+ require_relative "dispatcher"
4
5
 
5
6
  module StimulusPlumbers
6
7
  module Plumber
@@ -11,78 +12,172 @@ module StimulusPlumbers
11
12
  class_attribute :renderers, instance_writer: false, default: {}
12
13
  end
13
14
 
14
- module ClassMethods
15
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
16
- def renders(method_name, with: nil, &block)
17
- raise ArgumentError, "method_name must be Symbol" unless method_name.is_a?(Symbol)
18
- raise ArgumentError, "provide either with: or a block" if !with.nil? && block_given?
15
+ def set_slots
16
+ @set_slots ||= {}
17
+ end
18
+
19
+ def slot_renderable?(name)
20
+ slots = set_slots[name]
21
+ return false if slots.nil?
22
+ return false if slots.is_a?(StimulusPlumbers::Plumber::Slots) && slots.none?
23
+ return false if slots.is_a?(Array) && slots.empty?
24
+
25
+ true
26
+ end
27
+
28
+ def slot_kwargs_for(name)
29
+ case set_slots[name]
30
+ when StimulusPlumbers::Plumber::Slots then { slot: set_slots[name] }
31
+ when Array
32
+ values = set_slots[name]
33
+ { value: values.one? ? values.first : values }
34
+ else {}
35
+ end
36
+ end
37
+
38
+ def slot_block_for(name)
39
+ slots = set_slots[name]
40
+ slots if slots.is_a?(Proc)
41
+ end
19
42
 
43
+ module ClassMethods
44
+ def renders(method_name, with: nil, slots: nil, by: nil, &block)
45
+ validate!(method_name, with, slots, by, block_given?)
20
46
  with = block if block_given?
21
47
 
22
- with_proc_or_symbol = with.is_a?(Proc) || with.is_a?(Symbol)
23
- with_klazz = with.is_a?(Module) || with.is_a?(String)
24
- raise ArgumentError, "with: must be a Symbol/Proc/Class" unless with_proc_or_symbol || with_klazz
48
+ if slots.present?
49
+ by = Class.new(StimulusPlumbers::Plumber::Slots)
50
+ by.slot(*slots)
51
+ end
52
+ self.renderers = renderers.merge(method_name => { with: with, by: by })
25
53
 
26
- self.renderers = renderers.merge(method_name => with)
27
- ActiveSupport.version >= "7.2" ? generate_renderer_method(method_name) : eval_renderer_method(method_name)
54
+ generate_renderer_method(method_name)
55
+ generate_slot_method(method_name)
28
56
  end
29
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
30
57
 
31
58
  private
32
59
 
33
- def generated_renderer_methods
34
- @generated_renderer_methods ||= Module.new.tap { |mod| prepend mod }
60
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
61
+ def validate!(method_name, with, slots, by, has_block)
62
+ raise ArgumentError, "method_name must be a Symbol" unless method_name.is_a?(Symbol)
63
+ raise ArgumentError, "provide either with: or a block, not both" if with && has_block
64
+ raise ArgumentError, "slots: requires with:" if slots && with.nil? && !has_block
65
+ raise ArgumentError, "by: requires with:" if by && with.nil?
66
+ raise ArgumentError, "slots: and by: are mutually exclusive" if slots && by
67
+ raise ArgumentError, "by: must be a Class" if by && !by.is_a?(Class)
68
+
69
+ with_proc_or_symbol = with.is_a?(Proc) || with.is_a?(Symbol)
70
+ with_klazz = with.is_a?(Module) || with.is_a?(String)
71
+ raise ArgumentError, "with: must be a Symbol/Proc/Class" unless with_proc_or_symbol || with_klazz || has_block
35
72
  end
73
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
36
74
 
37
- def eval_renderer_method(method_name)
38
- generated_renderer_methods.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
39
- # def method_name(*args, **kwargs)
40
- # renderer = renderers.fetch(:method_name, {})
41
- #
42
- # unless renderer.present?
43
- # raise ArgumentError, "#method_name not found in renderer" unless defined?(super)
44
- # super
45
- # end
46
- #
47
- # dispatcher = StimulusPlumbers::Plumber::Dispatcher.build(
48
- # renderer, *args, method_name: :#{method_name}, init_args: [template], **kwargs
49
- # )
50
- # raise ArgumentError, "invalid renderer, got: \#{renderer.inspect}" unless dispatcher
51
- #
52
- # dispatcher.call(self)
53
- # end
54
- #{renderer_method_template(method_name)}
55
- RUBY
75
+ def generated_renderer_methods
76
+ @generated_renderer_methods ||= Module.new.tap { |mod| prepend mod }
56
77
  end
57
78
 
58
79
  def generate_renderer_method(method_name)
80
+ return eval_renderer_method(method_name) if ActiveSupport.version < "7.2"
81
+
59
82
  require "active_support/code_generator"
60
83
  ActiveSupport::CodeGenerator.batch(generated_renderer_methods, __FILE__, __LINE__) do |owner|
61
- owner.define_cached_method(method_name, namespace: :plumber_renderers) do |batch|
84
+ owner.define_cached_method(:"render_#{method_name}", namespace: :plumber_renderers) do |batch|
62
85
  batch << renderer_method_template(method_name)
63
86
  end
64
87
  end
65
88
  end
66
89
 
90
+ def eval_renderer_method(method_name)
91
+ generated_renderer_methods.module_eval(renderer_method_template(method_name), __FILE__, __LINE__)
92
+ end
93
+
67
94
  def renderer_method_template(method_name)
68
95
  <<-RUBY
69
- def #{method_name}(*args, **kwargs)
70
- renderer = renderers.fetch(:#{method_name}, {})
96
+ def render_#{method_name}(*args, **kwargs)
97
+ return nil unless slot_renderable?(:#{method_name})
71
98
 
72
- unless renderer.present?
73
- raise ArgumentError, "##{method_name} not found in renderer" unless defined?(super)
74
- super
99
+ renderer = renderers.fetch(:#{method_name}, {})
100
+ target = renderer[:with]
101
+ unless target
102
+ raise ArgumentError, "#render_#{method_name} not found" unless defined?(super)
103
+ return super
75
104
  end
76
105
 
77
106
  dispatcher = StimulusPlumbers::Plumber::Dispatcher.build(
78
- renderer, *args, method_name: :#{method_name}, init_args: [template], **kwargs
107
+ target, *args,
108
+ init_args: [template],
109
+ init_kwargs: {},
110
+ method_name: :render,
111
+ **kwargs,
112
+ **slot_kwargs_for(:#{method_name}),
113
+ &slot_block_for(:#{method_name})
79
114
  )
80
- raise ArgumentError, "invalid renderer, got: \#{renderer.inspect}" unless dispatcher
81
-
115
+ raise ArgumentError, "invalid renderer for :#{method_name}" unless dispatcher
82
116
  dispatcher.call(self)
83
117
  end
84
118
  RUBY
85
119
  end
120
+
121
+ def generated_slot_methods
122
+ @generated_slot_methods ||= Module.new.tap { |mod| prepend mod }
123
+ end
124
+
125
+ def generate_slot_method(method_name)
126
+ return eval_slot_method(method_name) if ActiveSupport.version < "7.2"
127
+
128
+ require "active_support/code_generator"
129
+ ActiveSupport::CodeGenerator.batch(generated_slot_methods, __FILE__, __LINE__) do |owner|
130
+ owner.define_cached_method(:"with_#{method_name}", namespace: :plumber_renderers) do |batch|
131
+ batch << slot_setter_template(method_name)
132
+ end
133
+ owner.define_cached_method(:"#{method_name}", namespace: :plumber_renderers) do |batch|
134
+ batch << slot_getter_template(method_name)
135
+ end
136
+ owner.define_cached_method(:"#{method_name}?", namespace: :plumber_renderers) do |batch|
137
+ batch << slot_predicate_template(method_name)
138
+ end
139
+ end
140
+ end
141
+
142
+ def eval_slot_method(method_name)
143
+ generated_slot_methods.module_eval(slot_setter_template(method_name), __FILE__, __LINE__)
144
+ generated_slot_methods.module_eval(slot_getter_template(method_name), __FILE__, __LINE__)
145
+ generated_slot_methods.module_eval(slot_predicate_template(method_name), __FILE__, __LINE__)
146
+ end
147
+
148
+ def slot_setter_template(method_name)
149
+ <<-RUBY
150
+ def with_#{method_name}(*values, &block)
151
+ klass = renderers.dig(:#{method_name}, :by)
152
+ if klass && block_given? && !block.arity.zero?
153
+ builder = klass.new
154
+ block.call(builder)
155
+ set_slots[:#{method_name}] = builder
156
+ elsif block_given?
157
+ set_slots[:#{method_name}] = block
158
+ else
159
+ set_slots[:#{method_name}] = values
160
+ end
161
+ nil
162
+ end
163
+ RUBY
164
+ end
165
+
166
+ def slot_getter_template(method_name)
167
+ <<-RUBY
168
+ def #{method_name}
169
+ set_slots[:#{method_name}]
170
+ end
171
+ RUBY
172
+ end
173
+
174
+ def slot_predicate_template(method_name)
175
+ <<-RUBY
176
+ def #{method_name}?
177
+ slot_renderable?(:#{method_name})
178
+ end
179
+ RUBY
180
+ end
86
181
  end
87
182
  end
88
183
  end