vident 1.0.1 → 2.0.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +49 -18
  4. data/lib/vident/caching.rb +4 -110
  5. data/lib/vident/capabilities/caching.rb +98 -0
  6. data/lib/vident/capabilities/child_element_rendering.rb +92 -0
  7. data/lib/vident/capabilities/class_list_building.rb +23 -0
  8. data/lib/vident/capabilities/declarable.rb +39 -0
  9. data/lib/vident/capabilities/identifiable.rb +54 -0
  10. data/lib/vident/capabilities/inspectable.rb +17 -0
  11. data/lib/vident/capabilities/root_element_rendering.rb +31 -0
  12. data/lib/vident/capabilities/stimulus_data_emitting.rb +98 -0
  13. data/lib/vident/capabilities/stimulus_declaring.rb +79 -0
  14. data/lib/vident/capabilities/stimulus_draft.rb +51 -0
  15. data/lib/vident/capabilities/stimulus_mutation.rb +60 -0
  16. data/lib/vident/capabilities/stimulus_parsing.rb +144 -0
  17. data/lib/vident/capabilities/tailwind.rb +18 -0
  18. data/lib/vident/component.rb +14 -76
  19. data/lib/vident/engine.rb +6 -5
  20. data/lib/vident/error.rb +16 -0
  21. data/lib/vident/internals/action_builder.rb +97 -0
  22. data/lib/vident/internals/attribute_writer.rb +17 -0
  23. data/lib/vident/internals/class_list_builder.rb +62 -0
  24. data/lib/vident/internals/declaration.rb +13 -0
  25. data/lib/vident/internals/declarations.rb +64 -0
  26. data/lib/vident/internals/draft.rb +47 -0
  27. data/lib/vident/internals/dsl.rb +172 -0
  28. data/lib/vident/internals/plan.rb +9 -0
  29. data/lib/vident/internals/registry.rb +37 -0
  30. data/lib/vident/internals/resolver.rb +316 -0
  31. data/lib/vident/internals/target_builder.rb +23 -0
  32. data/lib/vident/stable_id.rb +3 -3
  33. data/lib/vident/stimulus/action.rb +127 -0
  34. data/lib/vident/stimulus/base.rb +26 -0
  35. data/lib/vident/stimulus/class_map.rb +57 -0
  36. data/lib/vident/stimulus/collection.rb +40 -0
  37. data/lib/vident/stimulus/combinable.rb +30 -0
  38. data/lib/vident/stimulus/controller.rb +45 -0
  39. data/lib/vident/stimulus/naming.rb +9 -9
  40. data/lib/vident/stimulus/null.rb +7 -0
  41. data/lib/vident/stimulus/outlet.rb +93 -0
  42. data/lib/vident/stimulus/param.rb +56 -0
  43. data/lib/vident/stimulus/target.rb +48 -0
  44. data/lib/vident/stimulus/value.rb +57 -0
  45. data/lib/vident/stimulus_null.rb +4 -8
  46. data/lib/vident/tailwind.rb +4 -17
  47. data/lib/vident/types.rb +28 -0
  48. data/lib/vident/version.rb +1 -6
  49. data/lib/vident.rb +44 -36
  50. data/skills/vident/SKILL.md +133 -21
  51. data/skills/vident/api-reference.md +662 -0
  52. data/skills/vident/examples.md +505 -0
  53. metadata +40 -28
  54. data/lib/vident/child_element_helper.rb +0 -64
  55. data/lib/vident/class_list_builder.rb +0 -112
  56. data/lib/vident/component_attribute_resolver.rb +0 -87
  57. data/lib/vident/component_class_lists.rb +0 -34
  58. data/lib/vident/stimulus/primitive.rb +0 -38
  59. data/lib/vident/stimulus.rb +0 -31
  60. data/lib/vident/stimulus_action.rb +0 -133
  61. data/lib/vident/stimulus_action_collection.rb +0 -11
  62. data/lib/vident/stimulus_attribute_base.rb +0 -67
  63. data/lib/vident/stimulus_attributes.rb +0 -129
  64. data/lib/vident/stimulus_builder.rb +0 -119
  65. data/lib/vident/stimulus_class.rb +0 -59
  66. data/lib/vident/stimulus_class_collection.rb +0 -11
  67. data/lib/vident/stimulus_collection_base.rb +0 -51
  68. data/lib/vident/stimulus_component.rb +0 -75
  69. data/lib/vident/stimulus_controller.rb +0 -41
  70. data/lib/vident/stimulus_controller_collection.rb +0 -14
  71. data/lib/vident/stimulus_data_attribute_builder.rb +0 -32
  72. data/lib/vident/stimulus_helper.rb +0 -66
  73. data/lib/vident/stimulus_outlet.rb +0 -90
  74. data/lib/vident/stimulus_outlet_collection.rb +0 -11
  75. data/lib/vident/stimulus_param.rb +0 -42
  76. data/lib/vident/stimulus_param_collection.rb +0 -11
  77. data/lib/vident/stimulus_target.rb +0 -47
  78. data/lib/vident/stimulus_target_collection.rb +0 -18
  79. data/lib/vident/stimulus_value.rb +0 -39
  80. data/lib/vident/stimulus_value_collection.rb +0 -11
@@ -1,112 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "set"
4
-
5
- module Vident
6
- class ClassListBuilder
7
- CLASSNAME_SEPARATOR = " "
8
-
9
- # If the HTML "class" option is provided, it is taken in order of precedence of source.
10
- # The order of precedence is:
11
- # lowest | root_element_classes => whatever is returned
12
- # ....... | root_element_attributes => the `html_options[:class]` value
13
- # ....... | root_element(class: ...) => the `class` value of the arguments passed to the root element
14
- # highest | render MyComponent.new(html_options: { class: ... }) => the `html_options[:class]` value
15
- # The "classes" prop on the component on the other hand is used to add additional classes to the component.
16
- # eg: render MyComponent.new(classes: "my-additional-class another-class")
17
- def initialize(tailwind_merger: nil, component_name: nil, root_element_attributes_classes: nil, root_element_classes: nil, root_element_html_class: nil, html_class: nil, additional_classes: nil)
18
- @class_list = component_name ? [component_name] : []
19
- @class_list.concat(Array.wrap(root_element_classes)) if root_element_classes && !root_element_attributes_classes && !root_element_html_class && !html_class
20
- @class_list.concat(Array.wrap(root_element_attributes_classes)) if root_element_attributes_classes && !root_element_html_class && !html_class
21
- @class_list.concat(Array.wrap(root_element_html_class)) if root_element_html_class && !html_class
22
- @class_list.concat(Array.wrap(html_class)) if html_class
23
- @class_list.concat(Array.wrap(additional_classes)) if additional_classes
24
- @class_list.compact!
25
-
26
- @tailwind_merger = tailwind_merger
27
-
28
- if @tailwind_merger && !defined?(::TailwindMerge::Merger)
29
- raise LoadError, "TailwindMerge gem is required when using tailwind_merger:. Add 'gem \"tailwind_merge\"' to your Gemfile."
30
- end
31
- end
32
-
33
- # Main method to build a final class list from multiple sources
34
- # @param class_lists [Array<String, Array, StimulusClass, nil>] Multiple class sources to merge
35
- # @param stimulus_class_names [Array<Symbol, String>] Optional names of stimulus classes to include
36
- # @return [String, nil] Final space-separated class string or nil if no classes
37
- def build(extra_classes = nil, stimulus_class_names: [])
38
- class_list = @class_list + Array.wrap(extra_classes).compact
39
- flattened_classes = flatten_and_normalize_classes(class_list, stimulus_class_names)
40
- return nil if flattened_classes.empty?
41
-
42
- deduplicated_classes = dedupe_classes(flattened_classes)
43
- return nil if deduplicated_classes.blank?
44
-
45
- class_string = deduplicated_classes.join(CLASSNAME_SEPARATOR)
46
-
47
- if @tailwind_merger
48
- dedupe_with_tailwind(class_string)
49
- else
50
- class_string
51
- end
52
- end
53
-
54
- private
55
-
56
- # Flatten and normalize all input class sources
57
- def flatten_and_normalize_classes(class_lists, stimulus_class_names)
58
- stimulus_class_names_set = stimulus_class_names.map { |name| name.to_s.dasherize }.to_set
59
-
60
- class_lists.compact.flat_map do |class_source|
61
- case class_source
62
- when String
63
- class_source.split(CLASSNAME_SEPARATOR).reject(&:empty?)
64
- when Array
65
- class_source.flat_map { |item| normalize_single_class_item(item, stimulus_class_names_set) }
66
- else
67
- normalize_single_class_item(class_source, stimulus_class_names_set)
68
- end
69
- end.compact
70
- end
71
-
72
- # Normalize a single class item (could be string, StimulusClass, object with to_s, etc.)
73
- def normalize_single_class_item(item, stimulus_class_names_set)
74
- return [] if item.blank?
75
-
76
- # Handle StimulusClass instances
77
- if stimulus_class_instance?(item)
78
- # Only include if the class name matches one of the requested names
79
- # If stimulus_class_names_set is empty, exclude all stimulus classes
80
- if stimulus_class_names_set.present? && stimulus_class_names_set.include?(item.class_name)
81
- class_value = item.to_s
82
- class_value.include?(CLASSNAME_SEPARATOR) ?
83
- class_value.split(CLASSNAME_SEPARATOR).reject(&:empty?) :
84
- [class_value]
85
- else
86
- []
87
- end
88
- else
89
- # Handle regular strings and other objects
90
- item_string = item.to_s
91
- item_string.include?(CLASSNAME_SEPARATOR) ?
92
- item_string.split(CLASSNAME_SEPARATOR).reject(&:empty?) :
93
- [item_string]
94
- end
95
- end
96
-
97
- # Check if an item is a StimulusClass instance
98
- def stimulus_class_instance?(item)
99
- item.respond_to?(:class_name) && item.respond_to?(:to_s)
100
- end
101
-
102
- # Deduplicate classes while preserving order (first occurrence wins)
103
- def dedupe_classes(class_array)
104
- class_array.reject(&:blank?).uniq
105
- end
106
-
107
- # Merge classes using Tailwind CSS merge
108
- def dedupe_with_tailwind(class_string)
109
- @tailwind_merger.merge(class_string)
110
- end
111
- end
112
- end
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- module ComponentAttributeResolver
5
- include Stimulus::Naming
6
-
7
- private
8
-
9
- # Prepare attributes set at initialization, which will later be merged together before rendering.
10
- def prepare_component_attributes
11
- prepare_stimulus_collections
12
-
13
- # Add stimulus attributes from DSL first (lower precedence)
14
- add_stimulus_attributes_from_dsl
15
-
16
- # Process root_element_attributes (higher precedence)
17
- extra = root_element_attributes
18
- @html_options = (extra[:html_options] || {}).merge(@html_options) if extra.key?(:html_options)
19
- @root_element_attributes_classes = extra[:classes]
20
- @root_element_attributes_id = extra[:id] || id
21
- @element_tag = extra[:element_tag] if extra.key?(:element_tag)
22
-
23
- Stimulus::PRIMITIVES.each do |primitive|
24
- send(mutator_method(primitive), extra[primitive.key]) if extra.key?(primitive.key)
25
- end
26
- end
27
-
28
- def resolve_root_element_attributes_before_render(root_element_html_options = nil)
29
- extra = root_element_html_options || {}
30
-
31
- # Options set on component at render time take precedence over attributes set by methods on the component
32
- # or attributes passed to root_element in the template
33
- final_attributes = {
34
- data: stimulus_data_attributes # Lowest precedence
35
- }
36
- if root_element_html_options.present? # Mid precedence
37
- root_element_tag_html_options_merge(final_attributes, root_element_html_options)
38
- end
39
- if @html_options.present? # Highest precedence
40
- root_element_tag_html_options_merge(final_attributes, @html_options)
41
- end
42
- final_attributes[:class] = render_classes(extra[:class])
43
- final_attributes[:id] = (extra[:id] || @root_element_attributes_id) unless final_attributes.key?(:id)
44
- final_attributes
45
- end
46
-
47
- def root_element_tag_html_options_merge(final_attributes, other_html_options)
48
- if other_html_options[:data].present?
49
- final_attributes[:data].merge!(other_html_options[:data])
50
- end
51
- final_attributes.merge!(other_html_options.except(:data))
52
- end
53
-
54
- # Run every DSL attribute through its `add_stimulus_*` mutator. `values_from_props`
55
- # is a sidecar on values, resolved at instance render time.
56
- def add_stimulus_attributes_from_dsl
57
- dsl_attrs = self.class.stimulus_dsl_attributes(self)
58
- return if dsl_attrs.empty?
59
-
60
- Stimulus::PRIMITIVES.each do |primitive|
61
- value = dsl_attrs[primitive.key]
62
- send(mutator_method(primitive), value) if value
63
- end
64
-
65
- if dsl_attrs[:stimulus_values_from_props]
66
- resolved_values = resolve_values_from_props(dsl_attrs[:stimulus_values_from_props])
67
- add_stimulus_values(resolved_values) unless resolved_values.empty?
68
- end
69
- end
70
-
71
- # Seed the collection ivars from each prop's raw value.
72
- def prepare_stimulus_collections
73
- Stimulus::PRIMITIVES.each do |primitive|
74
- raw = instance_variable_get(prop_ivar(primitive))
75
- collection = send(primitive.key, *Array.wrap(raw))
76
- instance_variable_set(collection_ivar(primitive), collection)
77
- end
78
-
79
- @stimulus_outlet_host.add_stimulus_outlets(self) if @stimulus_outlet_host
80
- end
81
-
82
- def stimulus_data_attributes
83
- collections = Stimulus::PRIMITIVES.to_h { |primitive| [primitive.name, instance_variable_get(collection_ivar(primitive))] }
84
- StimulusDataAttributeBuilder.new(**collections).build
85
- end
86
- end
87
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- module ComponentClassLists
5
- # Generates the full list of HTML classes for the component
6
- def render_classes(root_element_html_class = nil) = class_list_builder(root_element_html_class).build
7
-
8
- # Getter for a stimulus classes list so can be used in view to set initial state on SSR
9
- # Returns a String of classes that can be used in a `class` attribute.
10
- def class_list_for_stimulus_classes(*names)
11
- ClassListBuilder.new(tailwind_merger:).build(
12
- @stimulus_classes_collection&.to_a,
13
- stimulus_class_names: names
14
- ) || ""
15
- end
16
-
17
- private
18
-
19
- # Not memoised: the per-thread TailwindMerger is the only expensive piece
20
- # and it's already cached; the builder itself just copies a few ivars.
21
- # Memoising here would latch the first caller's `root_element_html_class:`.
22
- def class_list_builder(root_element_html_class = nil)
23
- ClassListBuilder.new(
24
- tailwind_merger:,
25
- component_name:,
26
- root_element_attributes_classes: @root_element_attributes_classes,
27
- root_element_classes:,
28
- root_element_html_class:,
29
- additional_classes: @classes,
30
- html_class: @html_options&.fetch(:class, nil)
31
- )
32
- end
33
- end
34
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- module Stimulus
5
- # A Stimulus primitive kind: name, plus the Value/Collection classes that
6
- # back it. Two concrete subclasses distinguish how the primitive behaves
7
- # when a Hash is passed to the plural parser:
8
- #
9
- # - `Keyed` — `{a: 1, b: 2}` expands to one value object per pair.
10
- # Used for values / params / classes / outlets.
11
- # - `Positional` — `{...}` is a single-arg descriptor (e.g. Action's
12
- # `{event:, method:, ...}` short form).
13
- # Used for controllers / actions / targets.
14
- class Primitive < ::Data.define(:name, :value_class, :collection_class)
15
- # Short forms. `name` (Data field) is the plural — `:values`; `plural`
16
- # is an alias for symmetry with `singular`.
17
- alias_method :plural, :name
18
- def singular = name.to_s.singularize.to_sym
19
-
20
- # The primitive's key in Vident's attribute namespace. Used both as the
21
- # parser method name (`def stimulus_values(...)`) and as the hash key
22
- # in DSL attrs / component props / `root_element_attributes` — the same
23
- # Symbol serves all three roles.
24
- def key = :"stimulus_#{name}"
25
- def singular_key = :"stimulus_#{singular}"
26
-
27
- def keyed? = raise NotImplementedError
28
- end
29
-
30
- class KeyedPrimitive < Primitive
31
- def keyed? = true
32
- end
33
-
34
- class PositionalPrimitive < Primitive
35
- def keyed? = false
36
- end
37
- end
38
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- module Stimulus
5
- # Registry of primitive kinds. Add an entry (paired with a Value/Collection
6
- # class pair) to extend; plural parsers, mutators, and the data-attribute
7
- # builder pick it up. Array order = data attribute emission order.
8
- PRIMITIVES = [
9
- PositionalPrimitive.new(:controllers, StimulusController, StimulusControllerCollection),
10
- PositionalPrimitive.new(:actions, StimulusAction, StimulusActionCollection),
11
- PositionalPrimitive.new(:targets, StimulusTarget, StimulusTargetCollection),
12
- KeyedPrimitive.new(:outlets, StimulusOutlet, StimulusOutletCollection),
13
- KeyedPrimitive.new(:values, StimulusValue, StimulusValueCollection),
14
- KeyedPrimitive.new(:params, StimulusParam, StimulusParamCollection),
15
- KeyedPrimitive.new(:classes, StimulusClass, StimulusClassCollection)
16
- ].freeze
17
-
18
- PRIMITIVES_BY_NAME = PRIMITIVES.to_h { |primitive| [primitive.name, primitive] }.freeze
19
-
20
- class << self
21
- def primitive(name)
22
- PRIMITIVES_BY_NAME[name] or
23
- raise ArgumentError, "Unknown stimulus primitive #{name.inspect}; valid: #{PRIMITIVES_BY_NAME.keys.inspect}"
24
- end
25
-
26
- def each(&block) = PRIMITIVES.each(&block)
27
-
28
- def names = PRIMITIVES.map(&:name)
29
- end
30
- end
31
- end
@@ -1,133 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- class StimulusAction < StimulusAttributeBase
5
- # https://stimulus.hotwired.dev/reference/actions#options
6
- VALID_OPTIONS = [:once, :prevent, :stop, :passive, :"!passive", :capture, :self].freeze
7
-
8
- # Typed descriptor for modifiers (`:once`/`:prevent`/etc., keyboard filter,
9
- # `@window`) that the plain Array form can't express. Hash input to the
10
- # parsers (`{event:, method:, ...}`) is desugared into one of these.
11
- class Descriptor < ::Literal::Data
12
- prop :method, _Union(Symbol, String)
13
- prop :event, _Nilable(_Union(Symbol, String)), default: nil
14
- prop :controller, _Nilable(String), default: nil
15
- prop :options, _Array(Symbol), default: -> { [] }
16
- prop :keyboard, _Nilable(String), default: nil
17
- prop :window, _Boolean, default: false
18
- end
19
-
20
- attr_reader :event, :controller, :action, :options, :keyboard, :window
21
-
22
- def initialize(*args, implied_controller: nil)
23
- @options = []
24
- @keyboard = nil
25
- @window = false
26
- super
27
- end
28
-
29
- def to_s
30
- head =
31
- if @event
32
- ev = @event.to_s
33
- ev = "#{ev}.#{@keyboard}" if @keyboard
34
- ev = "#{ev}#{@options.map { |o| ":#{o}" }.join}" if @options.any?
35
- ev = "#{ev}@window" if @window
36
- "#{ev}->"
37
- else
38
- ""
39
- end
40
- "#{head}#{@controller}##{@action}"
41
- end
42
-
43
- def data_attribute_name = "action"
44
-
45
- def data_attribute_value = to_s
46
-
47
- private
48
-
49
- def parse_arguments(*args)
50
- part1, part2, part3 = args
51
-
52
- case args.size
53
- when 1
54
- parse_single_argument(part1)
55
- when 2
56
- parse_two_arguments(part1, part2)
57
- when 3
58
- parse_three_arguments(part1, part2, part3)
59
- else
60
- raise ArgumentError, "Invalid number of 'action' arguments: #{args.size}"
61
- end
62
- end
63
-
64
- def parse_single_argument(arg)
65
- case arg
66
- when Descriptor then apply_descriptor(arg)
67
- when Hash then apply_descriptor(Descriptor.new(**arg))
68
- when Symbol
69
- @event = nil
70
- @controller = implied_controller_name
71
- @action = js_name(arg)
72
- when String then parse_qualified_action_string(arg)
73
- else raise ArgumentError, "Invalid 'action' argument type (1): #{arg.class}"
74
- end
75
- end
76
-
77
- # (:event, :method) or ("controller/path", :method)
78
- def parse_two_arguments(part1, part2)
79
- if part1.is_a?(Symbol) && part2.is_a?(Symbol)
80
- @event = part1.to_s
81
- @controller = implied_controller_name
82
- @action = js_name(part2)
83
- elsif part1.is_a?(String) && part2.is_a?(Symbol)
84
- @event = nil
85
- @controller = stimulize_path(part1)
86
- @action = js_name(part2)
87
- else
88
- raise ArgumentError, "Invalid 'action' argument types (2): #{part1.class}, #{part2.class}"
89
- end
90
- end
91
-
92
- # (:event, "controller/path", :method)
93
- def parse_three_arguments(part1, part2, part3)
94
- if part1.is_a?(Symbol) && part2.is_a?(String) && part3.is_a?(Symbol)
95
- @event = part1.to_s
96
- @controller = stimulize_path(part2)
97
- @action = js_name(part3)
98
- else
99
- raise ArgumentError, "Invalid 'action' argument types (3): #{part1.class}, #{part2.class}, #{part3.class}"
100
- end
101
- end
102
-
103
- def apply_descriptor(d)
104
- invalid = d.options - VALID_OPTIONS
105
- unless invalid.empty?
106
- raise ArgumentError,
107
- "Invalid action option(s) #{invalid.inspect}. Valid: #{VALID_OPTIONS.inspect}"
108
- end
109
-
110
- @event = d.event&.to_s
111
- @controller = d.controller ? stimulize_path(d.controller) : implied_controller_name
112
- @action = d.method.is_a?(Symbol) ? js_name(d.method) : d.method.to_s
113
- @options = d.options
114
- @keyboard = d.keyboard
115
- @window = d.window
116
- end
117
-
118
- def parse_qualified_action_string(action_string)
119
- if action_string.include?("->")
120
- event_part, controller_action = action_string.split("->", 2)
121
- @event = event_part
122
- controller_part, action_part = controller_action.split("#", 2)
123
- @controller = controller_part
124
- @action = action_part
125
- else
126
- @event = nil
127
- controller_part, action_part = action_string.split("#", 2)
128
- @controller = controller_part
129
- @action = action_part
130
- end
131
- end
132
- end
133
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- class StimulusActionCollection < StimulusCollectionBase
5
- def to_h
6
- return {} if items.empty?
7
-
8
- {action: items.map(&:to_s).join(" ")}
9
- end
10
- end
11
- end
@@ -1,67 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/core_ext/string/inflections"
4
-
5
- require "json"
6
-
7
- module Vident
8
- class StimulusAttributeBase
9
- # `"admin/users"` → `"admin--users"`; accepts Symbol or String.
10
- def self.stimulize_path(path)
11
- path.to_s.split("/").map(&:dasherize).join("--")
12
- end
13
-
14
- # `:my_thing` → `"myThing"`
15
- def self.js_name(name)
16
- name.to_s.camelize(:lower)
17
- end
18
-
19
- attr_reader :implied_controller
20
-
21
- def initialize(*args, implied_controller: nil)
22
- @implied_controller = implied_controller
23
- parse_arguments(*args)
24
- end
25
-
26
- def inspect = "#<#{self.class.name} #{to_h}>"
27
-
28
- def to_s = raise(NoMethodError, "Subclasses must implement to_s")
29
-
30
- def to_h = {data_attribute_name => data_attribute_value}
31
-
32
- alias_method :to_hash, :to_h
33
-
34
- def data_attribute_name = raise(NoMethodError, "Subclasses must implement data_attribute_name")
35
-
36
- def data_attribute_value = raise(NoMethodError, "Subclasses must implement data_attribute_value")
37
-
38
- def implied_controller_path
39
- raise ArgumentError, "implied_controller is required to get implied controller path" unless implied_controller
40
- implied_controller.path
41
- end
42
-
43
- def implied_controller_name
44
- raise ArgumentError, "implied_controller is required to get implied controller name" unless implied_controller
45
- implied_controller.name
46
- end
47
-
48
- private
49
-
50
- def stimulize_path(path) = self.class.stimulize_path(path)
51
-
52
- def js_name(name) = self.class.js_name(name)
53
-
54
- # Arrays/Hashes serialise as JSON; everything else via `to_s` (which is how
55
- # `Vident::StimulusNull` emits the literal `"null"`).
56
- def serialize_value(value)
57
- case value
58
- when Array, Hash then value.to_json
59
- else value.to_s
60
- end
61
- end
62
-
63
- def parse_arguments(*args)
64
- raise NotImplementedError, "Subclasses must implement parse_arguments"
65
- end
66
- end
67
- end
@@ -1,129 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- module StimulusAttributes
5
- extend ActiveSupport::Concern
6
- # `extend` + `include` so Naming helpers are callable both in the module
7
- # body (outside define_method args) and inside define_method blocks
8
- # (at instance call-time).
9
- extend Stimulus::Naming
10
- include Stimulus::Naming
11
-
12
- class_methods do
13
- # Symbol so the action parser treats it as a Stimulus event type.
14
- def stimulus_scoped_event(event)
15
- :"#{component_name}:#{stimulus_js_name(event)}"
16
- end
17
-
18
- def stimulus_scoped_event_on_window(event)
19
- :"#{component_name}:#{stimulus_js_name(event)}@window"
20
- end
21
-
22
- private
23
-
24
- def stimulus_js_name(name) = name.to_s.camelize(:lower)
25
- end
26
-
27
- def stimulus_controller(*args)
28
- return args.first if args.length == 1 && args.first.is_a?(StimulusController)
29
- StimulusController.new(*args, implied_controller: implied_controller_path)
30
- end
31
-
32
- # Plural parsers `stimulus_<kind>s(*args)` — generated from the primitives
33
- # registry below. Each accepts: pre-built Value (pass-through), pre-built
34
- # Collection (unwrapped; a single one is returned as-is), Array (splatted
35
- # into the singular builder), Hash (expanded per-pair for
36
- # `hash_expands: true`, single-arg descriptor otherwise), else passed to
37
- # the singular builder. Methods defined this way: `stimulus_controllers`,
38
- # `stimulus_actions`, `stimulus_targets`, `stimulus_outlets`,
39
- # `stimulus_values`, `stimulus_params`, `stimulus_classes`.
40
- Stimulus::PRIMITIVES.each do |primitive|
41
- define_method(primitive.key) do |*args|
42
- collection_class = primitive.collection_class
43
- return collection_class.new if args.empty? || args.all?(&:blank?)
44
- return args.first if args.length == 1 && args.first.is_a?(collection_class)
45
-
46
- singular = primitive.singular_key
47
- converted = []
48
- args.each do |arg|
49
- case arg
50
- when primitive.value_class then converted << arg
51
- when collection_class then converted.concat(arg.to_a)
52
- when Hash
53
- if primitive.keyed?
54
- arg.each { |name, val| converted << send(singular, name, val) }
55
- else
56
- converted << send(singular, arg)
57
- end
58
- when Array then converted << send(singular, *arg)
59
- else converted << send(singular, arg)
60
- end
61
- end
62
- collection_class.new(converted)
63
- end
64
- end
65
-
66
- def stimulus_action(*args)
67
- return args.first if args.length == 1 && args.first.is_a?(StimulusAction)
68
- StimulusAction.new(*args, implied_controller:)
69
- end
70
-
71
- def stimulus_target(*args)
72
- return args.first if args.length == 1 && args.first.is_a?(StimulusTarget)
73
- StimulusTarget.new(*args, implied_controller:)
74
- end
75
-
76
- # `component_id: @id` scopes the auto-generated selector to this component
77
- # instance (e.g. `#<host-id> [data-controller~=<outlet>]`).
78
- def stimulus_outlet(*args)
79
- return args.first if args.length == 1 && args.first.is_a?(StimulusOutlet)
80
- StimulusOutlet.new(*args, implied_controller:, component_id: @id)
81
- end
82
-
83
- def stimulus_value(*args)
84
- return args.first if args.length == 1 && args.first.is_a?(StimulusValue)
85
- StimulusValue.new(*args, implied_controller:)
86
- end
87
-
88
- def stimulus_param(*args)
89
- return args.first if args.length == 1 && args.first.is_a?(StimulusParam)
90
- StimulusParam.new(*args, implied_controller:)
91
- end
92
-
93
- def stimulus_class(*args)
94
- return args.first if args.length == 1 && args.first.is_a?(StimulusClass)
95
- StimulusClass.new(*args, implied_controller:)
96
- end
97
-
98
- # Mutators `add_stimulus_<kind>s` — build from input, merge into the
99
- # per-kind collection ivar. Methods defined: `add_stimulus_controllers`,
100
- # `add_stimulus_actions`, `add_stimulus_targets`, `add_stimulus_outlets`,
101
- # `add_stimulus_values`, `add_stimulus_params`, `add_stimulus_classes`.
102
- Stimulus::PRIMITIVES.each do |primitive|
103
- define_method(mutator_method(primitive)) do |input|
104
- added = send(primitive.key, *Array.wrap(input))
105
- existing = instance_variable_get(collection_ivar(primitive))
106
- instance_variable_set(collection_ivar(primitive), existing ? existing.merge(added) : added)
107
- end
108
- end
109
-
110
- def stimulus_scoped_event(event) = self.class.stimulus_scoped_event(event)
111
-
112
- def stimulus_scoped_event_on_window(event) = self.class.stimulus_scoped_event_on_window(event)
113
-
114
- private
115
-
116
- def implied_controller
117
- StimulusController.new(implied_controller: implied_controller_path)
118
- end
119
-
120
- # The first registered controller path becomes the implied controller for
121
- # unqualified DSL entries (e.g. `actions :click` → `implied#click`).
122
- def implied_controller_path
123
- return @implied_controller_path if defined?(@implied_controller_path)
124
- path = Array.wrap(@stimulus_controllers).first
125
- raise(StandardError, "No controllers have been specified") unless path
126
- @implied_controller_path = path
127
- end
128
- end
129
- end