stimulus_plumbers 0.2.9 → 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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +5 -4
  4. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.es.js +246 -269
  5. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.umd.js +1 -1
  6. data/lib/stimulus_plumbers/components/action_list/section.rb +6 -5
  7. data/lib/stimulus_plumbers/components/action_list.rb +3 -3
  8. data/lib/stimulus_plumbers/components/avatar.rb +4 -3
  9. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_month.rb +62 -41
  10. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_week.rb +13 -10
  11. data/lib/stimulus_plumbers/components/calendar.rb +36 -13
  12. data/lib/stimulus_plumbers/components/card/section.rb +3 -3
  13. data/lib/stimulus_plumbers/components/card.rb +3 -3
  14. data/lib/stimulus_plumbers/components/combobox/autocomplete.rb +8 -14
  15. data/lib/stimulus_plumbers/components/combobox/date.rb +6 -2
  16. data/lib/stimulus_plumbers/components/combobox/dropdown.rb +6 -2
  17. data/lib/stimulus_plumbers/components/combobox/popover.rb +9 -5
  18. data/lib/stimulus_plumbers/components/combobox/time.rb +1 -1
  19. data/lib/stimulus_plumbers/components/combobox/trigger.rb +19 -10
  20. data/lib/stimulus_plumbers/components/combobox.rb +33 -23
  21. data/lib/stimulus_plumbers/components/date_picker/navigation.rb +14 -22
  22. data/lib/stimulus_plumbers/components/divider.rb +16 -0
  23. data/lib/stimulus_plumbers/components/icon.rb +14 -20
  24. data/lib/stimulus_plumbers/components/popover/builder.rb +2 -2
  25. data/lib/stimulus_plumbers/components/popover.rb +11 -6
  26. data/lib/stimulus_plumbers/configuration.rb +3 -18
  27. data/lib/stimulus_plumbers/engine.rb +2 -2
  28. data/lib/stimulus_plumbers/form/builder.rb +40 -44
  29. data/lib/stimulus_plumbers/form/field.rb +96 -45
  30. data/lib/stimulus_plumbers/form/fields/error.rb +2 -2
  31. data/lib/stimulus_plumbers/form/fields/fieldset.rb +54 -0
  32. data/lib/stimulus_plumbers/form/fields/group.rb +2 -2
  33. data/lib/stimulus_plumbers/form/fields/hint.rb +2 -2
  34. data/lib/stimulus_plumbers/form/fields/input_group.rb +25 -0
  35. data/lib/stimulus_plumbers/form/fields/inputs/choice.rb +69 -0
  36. data/lib/stimulus_plumbers/form/fields/inputs/datetime.rb +81 -0
  37. data/lib/stimulus_plumbers/form/fields/inputs/file.rb +22 -0
  38. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +59 -0
  39. data/lib/stimulus_plumbers/form/fields/inputs/search.rb +102 -0
  40. data/lib/stimulus_plumbers/form/fields/inputs/select/grouped.rb +56 -0
  41. data/lib/stimulus_plumbers/form/fields/inputs/select/timezone.rb +59 -0
  42. data/lib/stimulus_plumbers/form/fields/inputs/select/weekday.rb +45 -0
  43. data/lib/stimulus_plumbers/form/fields/inputs/select.rb +91 -0
  44. data/lib/stimulus_plumbers/form/fields/inputs/submit.rb +25 -0
  45. data/lib/stimulus_plumbers/form/fields/inputs/text.rb +37 -0
  46. data/lib/stimulus_plumbers/form/fields/inputs/text_area.rb +22 -0
  47. data/lib/stimulus_plumbers/form/fields/label.rb +13 -9
  48. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +1 -1
  49. data/lib/stimulus_plumbers/helpers/combobox_helper.rb +29 -36
  50. data/lib/stimulus_plumbers/helpers/divider_helper.rb +11 -0
  51. data/lib/stimulus_plumbers/helpers.rb +2 -0
  52. data/lib/stimulus_plumbers/plumber/base.rb +1 -1
  53. data/lib/stimulus_plumbers/plumber/dispatcher/callable_inspector.rb +19 -0
  54. data/lib/stimulus_plumbers/plumber/dispatcher/instance_exec.rb +35 -0
  55. data/lib/stimulus_plumbers/plumber/dispatcher/klass_proxy.rb +34 -0
  56. data/lib/stimulus_plumbers/plumber/dispatcher/method_call.rb +36 -0
  57. data/lib/stimulus_plumbers/plumber/dispatcher.rb +4 -87
  58. data/lib/stimulus_plumbers/plumber/html_options.rb +6 -5
  59. data/lib/stimulus_plumbers/plumber/renderer.rb +2 -2
  60. data/lib/stimulus_plumbers/themes/base.rb +26 -5
  61. data/lib/stimulus_plumbers/themes/configuration.rb +38 -0
  62. data/lib/stimulus_plumbers/themes/schema/form/ranges.rb +14 -0
  63. data/lib/stimulus_plumbers/themes/schema/icon.rb +32 -0
  64. data/lib/stimulus_plumbers/themes/schema/ranges.rb +1 -1
  65. data/lib/stimulus_plumbers/themes/schema.rb +17 -5
  66. data/lib/stimulus_plumbers/version.rb +1 -1
  67. data/lib/stimulus_plumbers.rb +4 -2
  68. metadata +25 -21
  69. data/lib/stimulus_plumbers/form/fields/choice.rb +0 -25
  70. data/lib/stimulus_plumbers/form/fields/combobox.rb +0 -41
  71. data/lib/stimulus_plumbers/form/fields/file.rb +0 -16
  72. data/lib/stimulus_plumbers/form/fields/password.rb +0 -55
  73. data/lib/stimulus_plumbers/form/fields/renderer.rb +0 -52
  74. data/lib/stimulus_plumbers/form/fields/search.rb +0 -54
  75. data/lib/stimulus_plumbers/form/fields/select.rb +0 -33
  76. data/lib/stimulus_plumbers/form/fields/submit.rb +0 -23
  77. data/lib/stimulus_plumbers/form/fields/text.rb +0 -33
  78. data/lib/stimulus_plumbers/form/fields/text_area.rb +0 -16
  79. data/lib/stimulus_plumbers/themes/tailwind/action_list.rb +0 -33
  80. data/lib/stimulus_plumbers/themes/tailwind/avatar.rb +0 -52
  81. data/lib/stimulus_plumbers/themes/tailwind/button.rb +0 -89
  82. data/lib/stimulus_plumbers/themes/tailwind/calendar.rb +0 -80
  83. data/lib/stimulus_plumbers/themes/tailwind/card.rb +0 -24
  84. data/lib/stimulus_plumbers/themes/tailwind/combobox.rb +0 -75
  85. data/lib/stimulus_plumbers/themes/tailwind/form.rb +0 -108
  86. data/lib/stimulus_plumbers/themes/tailwind/layout.rb +0 -25
  87. data/lib/stimulus_plumbers/themes/tailwind_theme.rb +0 -31
@@ -12,7 +12,7 @@ module StimulusPlumbers
12
12
  }
13
13
  end
14
14
 
15
- def render(format: :h12, step: 1, value: nil, **_kwargs)
15
+ def render(format: :h12, step: 1, value: nil)
16
16
  @format = format
17
17
  @step = [1, step.to_i].max
18
18
  @time = parse_time(value)
@@ -11,25 +11,34 @@ module StimulusPlumbers
11
11
  readonly: true,
12
12
  aria_autocomplete: nil,
13
13
  aria_label: nil,
14
+ aria: {},
15
+ id: nil,
14
16
  data: {},
15
- **_rest
17
+ **kwargs
16
18
  )
17
- base_data = {
19
+ stimulus_data = {
18
20
  "#{stimulus_controller}_target": "trigger",
19
21
  input_format_target: "input",
20
22
  action: "focus->#{stimulus_controller}#open keydown.esc->#{stimulus_controller}#close"
21
23
  }
22
24
 
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
25
+ trigger_aria = { haspopup: haspopup, expanded: "false", controls: popover_id }
26
+ trigger_aria[:autocomplete] = aria_autocomplete if aria_autocomplete
27
+ trigger_aria[:label] = aria_label if aria_label
26
28
 
27
29
  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)
30
+ **merge_html_options(
31
+ { classes: theme.resolve(:combobox_trigger).fetch(:classes, "") },
32
+ {
33
+ id: id,
34
+ type: "text",
35
+ readonly: (readonly ? true : nil),
36
+ role: "combobox",
37
+ aria: trigger_aria.deep_merge(aria),
38
+ data: merge_data_options(stimulus_data, data)
39
+ },
40
+ kwargs
41
+ )
33
42
  )
34
43
  end
35
44
  end
@@ -7,52 +7,62 @@ module StimulusPlumbers
7
7
  FORMAT_CONTROLLER = "input-format"
8
8
  FORMAT_ACTION = "input-combobox:changed->input-format#format"
9
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?
10
+ def self.popover_id_for(trigger_id)
11
+ [trigger_id, "popover"].compact.join("_")
12
+ end
19
13
 
20
- html_options = merge_html_options({ data: base_data }, kwargs)
14
+ def render(trigger: {}, input: {}, popover: {}, **kwargs, &block)
15
+ popover_id = self.class.popover_id_for(trigger[:id])
16
+ initial_value = input[:value]
17
+ haspopup = popover.delete(:haspopup) { popover[:role] || "dialog" }
18
+ html_options = merge_html_options({ data: build_stimulus_data(initial_value) }, kwargs)
21
19
 
22
20
  template.content_tag(:div, **html_options) do
23
21
  template.safe_join(
24
22
  [
25
- trigger(popover_id, options),
26
- hidden_input(options.fetch(:input, {})),
27
- popover(popover_id, options)
23
+ combobox_trigger(popover_id, trigger, haspopup),
24
+ hidden_input(input),
25
+ combobox_popover(popover_id, popover, &block)
28
26
  ]
29
27
  )
30
28
  end
31
29
  end
32
30
 
33
- def trigger(popover_id, options)
34
- haspopup = options.dig(:popover, :haspopup) || options.dig(:popover, :role) || "dialog"
31
+ private
32
+
33
+ def build_stimulus_data(initial_value)
34
+ {
35
+ controller: "#{STIMULUS_CONTROLLER} #{FORMAT_CONTROLLER}",
36
+ action: FORMAT_ACTION
37
+ }.tap do |data|
38
+ data[:input_combobox_value_value] = initial_value if initial_value.present?
39
+ end
40
+ end
41
+
42
+ def combobox_trigger(popover_id, trigger, haspopup)
35
43
  Combobox::Trigger.new(template).render(
36
44
  stimulus_controller: STIMULUS_CONTROLLER,
37
45
  popover_id: popover_id,
38
46
  haspopup: haspopup,
39
- **options.fetch(:trigger, {})
47
+ **trigger
40
48
  )
41
49
  end
42
50
 
43
- def popover(popover_id, options)
51
+ def combobox_popover(popover_id, popover, &block)
44
52
  Combobox::Popover.new(template).render(
45
53
  stimulus_controller: STIMULUS_CONTROLLER,
46
54
  id: popover_id,
47
- **options.fetch(:popover, {})
55
+ **popover,
56
+ &block
48
57
  )
49
58
  end
50
59
 
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)
60
+ def hidden_input(input)
61
+ stimulus_data = merge_html_options(
62
+ { "#{STIMULUS_CONTROLLER}_target": "value" },
63
+ input.fetch(:data, {})
64
+ )
65
+ template.tag.input(type: "hidden", name: input[:name], value: input[:value], data: stimulus_data)
56
66
  end
57
67
  end
58
68
  end
@@ -19,30 +19,22 @@ module StimulusPlumbers
19
19
 
20
20
  def navigators(stimulus_controller, step)
21
21
  [
22
- Navigator.new(template).render(
23
- icon_options: { name: "arrow-left" },
24
- aria: { label: ["previous", step].join(" ").titleize },
25
- data: { "#{stimulus_controller}-target" => "previous" }
26
- ),
27
- Navigator.new(template).render(
28
- aria: { label: "Day" },
29
- data: { "#{stimulus_controller}-target" => "day" }
30
- ),
31
- Navigator.new(template).render(
32
- aria: { label: "Month" },
33
- data: { "#{stimulus_controller}-target" => "month" }
34
- ),
35
- Navigator.new(template).render(
36
- aria: { label: "Year" },
37
- data: { "#{stimulus_controller}-target" => "year" }
38
- ),
39
- Navigator.new(template).render(
40
- icon_options: { name: "arrow-right" },
41
- aria: { label: ["next", step].join(" ").titleize },
42
- data: { "#{stimulus_controller}-target" => "next" }
43
- )
22
+ navigator(stimulus_controller, target: "previous", icon: "arrow-left", label: ["previous", step].join(" ").titleize),
23
+ navigator(stimulus_controller, target: "day", label: "Day"),
24
+ navigator(stimulus_controller, target: "month", label: "Month"),
25
+ navigator(stimulus_controller, target: "year", label: "Year"),
26
+ navigator(stimulus_controller, target: "next", icon: "arrow-right", label: ["next", step].join(" ").titleize)
44
27
  ]
45
28
  end
29
+
30
+ def navigator(stimulus_controller, target:, label:, icon: nil)
31
+ opts = {
32
+ aria: { label: label },
33
+ data: { "#{stimulus_controller}-target" => target }
34
+ }
35
+ opts[:icon_options] = { name: icon } if icon
36
+ Navigator.new(template).render(**opts)
37
+ end
46
38
  end
47
39
  end
48
40
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Divider < Plumber::Base
6
+ def render(**kwargs)
7
+ html_options = merge_html_options(
8
+ { classes: theme.resolve(:divider).fetch(:classes, "") },
9
+ kwargs
10
+ )
11
+
12
+ template.tag.hr(**html_options)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -3,44 +3,38 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
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
6
  def render(name:, **kwargs)
14
7
  html_options = merge_html_options(
15
8
  { classes: theme.resolve(:icon).fetch(:classes, "") },
16
9
  kwargs
17
10
  )
18
11
 
19
- if ICONS[name]
20
- svg_icon(ICONS[name], html_options)
12
+ icon_data = Themes::Schema::Icon.resolve(theme.icons[name])
13
+ if icon_data
14
+ svg_icon(icon_data, html_options)
21
15
  else
22
- template.content_tag(:span, nil, **html_options)
16
+ template.tag.span(**html_options)
23
17
  end
24
18
  end
25
19
 
26
20
  private
27
21
 
28
- def svg_icon(path, html_options)
22
+ def svg_icon(icon_data, html_options)
29
23
  template.content_tag(
30
24
  :svg,
31
25
  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",
26
+ fill: icon_data[:fill],
27
+ viewBox: icon_data[:view_box],
28
+ width: icon_data[:width],
29
+ height: icon_data[:height],
30
+ stroke: icon_data[:stroke],
31
+ "stroke-width": icon_data[:stroke_width],
38
32
  **html_options
39
33
  ) do
40
34
  template.tag.path(
41
- "stroke-linecap": "round",
42
- "stroke-linejoin": "round",
43
- d: path
35
+ "stroke-linecap": icon_data[:stroke_linecap],
36
+ "stroke-linejoin": icon_data[:stroke_linejoin],
37
+ d: icon_data[:d]
44
38
  )
45
39
  end
46
40
  end
@@ -8,8 +8,8 @@ module StimulusPlumbers
8
8
 
9
9
  def initialize(template)
10
10
  @template = template
11
- @activator_html = "".html_safe
12
- @content_html = "".html_safe
11
+ @activator_html = nil
12
+ @content_html = nil
13
13
  end
14
14
 
15
15
  def activator(&block)
@@ -13,12 +13,17 @@ module StimulusPlumbers
13
13
  template.capture(builder, &block)
14
14
 
15
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])
16
+ template.safe_join([builder.activator_html, wrap_content(interactive, builder)])
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def wrap_content(interactive, builder)
23
+ if interactive
24
+ template.content_tag(:template, builder.content_html)
25
+ else
26
+ builder.content_html
22
27
  end
23
28
  end
24
29
  end
@@ -1,19 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "themes/base"
4
- require_relative "themes/tailwind_theme"
4
+ require_relative "themes/configuration"
5
5
 
6
6
  module StimulusPlumbers
7
7
  class Configuration
8
- DEFAULT_LOG_FORMATTER = ->(message) { "[StimulusPlumbers] #{message}" }
9
- THEME_KLASS_FORMATTER = ->(type) { "StimulusPlumbers::Themes::#{type.to_s.classify}Theme" }
8
+ DEFAULT_LOG_FORMATTER = ->(message) { "[StimulusPlumbers] #{message}" }
10
9
 
11
10
  def theme
12
- @theme ||= build_theme(:tailwind)
13
- end
14
-
15
- def theme=(value)
16
- @theme = build_theme(value)
11
+ @theme ||= Themes::Configuration.new
17
12
  end
18
13
 
19
14
  def log_formatter
@@ -25,15 +20,5 @@ module StimulusPlumbers
25
20
 
26
21
  @log_formatter = callable
27
22
  end
28
-
29
- private
30
-
31
- def build_theme(type)
32
- return type if type.is_a?(Themes::Base)
33
-
34
- klass_name = THEME_KLASS_FORMATTER.call(type)
35
- klass_name.safe_constantize&.new or
36
- raise ArgumentError, "Unknown theme #{type.inspect}: #{klass_name} is not defined."
37
- end
38
23
  end
39
24
  end
@@ -8,8 +8,8 @@ module StimulusPlumbers
8
8
 
9
9
  config.autoload_paths << File.expand_path("../stimulus-plumbers", __dir__)
10
10
 
11
- initializer "stimulus_plumbers.assets" do |app|
12
- app.config.assets.paths << root.join("app/assets/javascripts")
11
+ initializer "stimulus_plumbers.assets", after: :set_default_precompile do |app|
12
+ app.config.assets.precompile += %w[stimulus_plumbers/tokens.css] if app.config.respond_to?(:assets)
13
13
  end
14
14
 
15
15
  initializer "stimulus_plumbers.helpers" do
@@ -3,63 +3,59 @@
3
3
  require "action_view/version"
4
4
 
5
5
  require_relative "field"
6
- require_relative "fields/choice"
7
- require_relative "fields/combobox"
8
- require_relative "fields/file"
9
- require_relative "fields/password"
10
- require_relative "fields/renderer"
11
- require_relative "fields/search"
12
- require_relative "fields/select"
13
- require_relative "fields/text"
14
- require_relative "fields/text_area"
15
- require_relative "fields/submit"
6
+ require_relative "fields/fieldset"
7
+ require_relative "fields/inputs/choice"
8
+ require_relative "fields/inputs/datetime"
9
+ require_relative "fields/inputs/file"
10
+ require_relative "fields/inputs/password"
11
+ require_relative "fields/inputs/search"
12
+ require_relative "fields/inputs/select"
13
+ require_relative "fields/inputs/select/grouped"
14
+ require_relative "fields/inputs/select/timezone"
15
+ require_relative "fields/inputs/select/weekday"
16
+ require_relative "fields/inputs/submit"
17
+ require_relative "fields/inputs/text"
18
+ require_relative "fields/inputs/text_area"
16
19
  require_relative "../plumber/html_options"
17
20
 
18
21
  module StimulusPlumbers
19
22
  module Form
20
23
  class Builder < ActionView::Helpers::FormBuilder
21
24
  include Plumber::HtmlOptions
22
- include Fields::Choice
23
- include Fields::Combobox
24
- include Fields::File
25
- include Fields::Password
26
- include Fields::Search
27
- include Fields::Select
28
- include Fields::Submit
29
- include Fields::Text
30
- include Fields::TextArea
25
+ include Fields::Inputs::Choice
26
+ include Fields::Inputs::Datetime
27
+ include Fields::Inputs::File
28
+ include Fields::Inputs::Password
29
+ include Fields::Inputs::Search
30
+ include Fields::Inputs::Select
31
+ include Fields::Inputs::Select::Grouped
32
+ include Fields::Inputs::Select::Timezone
33
+ include Fields::Inputs::Select::Weekday
34
+ include Fields::Inputs::Submit
35
+ include Fields::Inputs::Text
36
+ include Fields::Inputs::TextArea
31
37
 
32
38
  private
33
39
 
34
- def build_field(attribute, form_field_opts, input_id: field_id(attribute))
35
- Field.new(
36
- object: object,
37
- attribute: attribute,
38
- input_id: input_id,
39
- label: form_field_opts[:label],
40
- details: form_field_opts[:details],
41
- error: form_field_opts[:error],
42
- required: form_field_opts.fetch(:required, false),
43
- label_visibility: form_field_opts.fetch(:label_visibility, :visible),
44
- layout: form_field_opts.fetch(:layout, :stacked)
45
- )
40
+ def render_fieldset(attribute, field, &block)
41
+ Fields::Fieldset.new(@template).render(object, attribute, field_id(attribute), field, &block)
46
42
  end
47
43
 
48
- def render_field(field, input_html)
49
- Fields::Renderer.new(@template, theme, field).call(input_html)
44
+ def render_input_group(error:, leading: nil, trailing: nil, **wrapper_opts, &block)
45
+ Fields::InputGroup.new(@template).render(leading: leading, trailing: trailing, error: error, **wrapper_opts, &block)
50
46
  end
51
47
 
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
48
+ def render_combobox(attribute, input_id:, opts:, err:, **wrapper_opts, &block)
49
+ combobox_opts = opts.deep_merge(
50
+ input: { name: field_name(attribute) },
51
+ trigger: { id: input_id }
58
52
  )
59
- end
60
53
 
61
- def extract_options(options)
62
- [options.except(*Field::OPTIONS), options.slice(*Field::OPTIONS)]
54
+ Components::Combobox.new(@template).render(
55
+ **combobox_opts,
56
+ **merge_html_options(wrapper_opts, field_theme(:form_combobox, error: err)),
57
+ &block
58
+ )
63
59
  end
64
60
 
65
61
  def field_theme(key, **variants)
@@ -67,11 +63,11 @@ module StimulusPlumbers
67
63
  end
68
64
 
69
65
  def theme
70
- StimulusPlumbers.config.theme
66
+ StimulusPlumbers.config.theme.current
71
67
  end
72
68
 
73
69
  # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
74
- if ActionView.version < "7.0"
70
+ if ActionView.version < Gem::Version.new("7.0")
75
71
  # field_id was added in Rails 7.0, backports it to Rails 6.1.
76
72
  # https://github.com/rails/rails/blob/2d670320f7b02ae879545d5202f0633841b8f196/actionview/lib/action_view/helpers/form_helper.rb#L1777
77
73
  # https://github.com/rails/rails/blob/2d670320f7b02ae879545d5202f0633841b8f196/actionview/lib/action_view/helpers/form_tag_helper.rb#L101
@@ -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