vident 0.13.1 → 1.0.0.alpha2

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.
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ module StimulusAttributes
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ # Class methods for generating scoped event names. Returns a symbol
9
+ # so that the action parser will see it as a Stimulus event.
10
+ def stimulus_scoped_event(event)
11
+ :"#{component_name}:#{stimulus_js_name(event)}"
12
+ end
13
+
14
+ def stimulus_scoped_event_on_window(event)
15
+ :"#{component_name}:#{stimulus_js_name(event)}@window"
16
+ end
17
+
18
+ private
19
+
20
+ def stimulus_js_name(name)
21
+ name.to_s.camelize(:lower)
22
+ end
23
+ end
24
+
25
+ # Parse inputs to create a StimulusController instance representing a Stimulus controller attribute
26
+ # examples:
27
+ # stimulus_controller("my_controller") => StimulusController that converts to {"controller" => "my-controller"}
28
+ # stimulus_controller("path/to/controller") => StimulusController that converts to {"controller" => "path--to--controller"}
29
+ # stimulus_controller() => StimulusController that uses implied controller name
30
+ def stimulus_controller(*args)
31
+ return args.first if args.length == 1 && args.first.is_a?(StimulusController)
32
+ StimulusController.new(*args, implied_controller: implied_controller_path)
33
+ end
34
+
35
+ # Parse inputs to create a StimulusControllerCollection instance representing multiple Stimulus controllers
36
+ # examples:
37
+ # stimulus_controllers(:my_controller) => StimulusControllerCollection with one controller that converts to {"controller" => "my-controller"}
38
+ # stimulus_controllers(:my_controller, "path/to/another") => StimulusControllerCollection with two controllers that converts to {"controller" => "my-controller path--to--another"}
39
+ def stimulus_controllers(*controllers)
40
+ return StimulusControllerCollection.new if controllers.empty? || controllers.all?(&:blank?)
41
+
42
+ converted_controllers = controllers.map do |controller|
43
+ controller.is_a?(Array) ? stimulus_controller(*controller) : stimulus_controller(controller)
44
+ end
45
+ StimulusControllerCollection.new(converted_controllers)
46
+ end
47
+
48
+ # Parse inputs to create a StimulusAction instance representing a Stimulus action attribute
49
+ # examples:
50
+ # stimulus_action(:my_thing) => StimulusAction that converts to "current_controller#myThing"
51
+ # stimulus_action(:click, :my_thing) => StimulusAction that converts to "click->current_controller#myThing"
52
+ # stimulus_action("click->current_controller#myThing") => StimulusAction that converts to "click->current_controller#myThing"
53
+ # stimulus_action("path/to/current", :my_thing) => StimulusAction that converts to "path--to--current_controller#myThing"
54
+ # stimulus_action(:click, "path/to/current", :my_thing) => StimulusAction that converts to "click->path--to--current_controller#myThing"
55
+ def stimulus_action(*args)
56
+ return args.first if args.length == 1 && args.first.is_a?(StimulusAction)
57
+ StimulusAction.new(*args, implied_controller:)
58
+ end
59
+
60
+ # Parse inputs to create a StimulusActionCollection instance representing multiple Stimulus actions
61
+ def stimulus_actions(*actions)
62
+ return StimulusActionCollection.new if actions.empty? || actions.all?(&:blank?)
63
+
64
+ converted_actions = actions.map do |action|
65
+ action.is_a?(Array) ? stimulus_action(*action) : stimulus_action(action)
66
+ end
67
+ StimulusActionCollection.new(converted_actions)
68
+ end
69
+
70
+ # Parse inputs to create a StimulusTarget instance representing a Stimulus target attribute
71
+ # examples:
72
+ # stimulus_target(:my_target) => StimulusTarget that converts to {"current_controller-target" => "myTarget"}
73
+ # stimulus_target("path/to/current", :my_target) => StimulusTarget that converts to {"path--to--current-target" => "myTarget"}
74
+ def stimulus_target(*args)
75
+ return args.first if args.length == 1 && args.first.is_a?(StimulusTarget)
76
+ StimulusTarget.new(*args, implied_controller:)
77
+ end
78
+
79
+ # Parse inputs to create a StimulusTargetCollection instance representing multiple Stimulus targets
80
+ def stimulus_targets(*targets)
81
+ return StimulusTargetCollection.new if targets.empty? || targets.all?(&:blank?)
82
+
83
+ converted_targets = targets.map do |target|
84
+ target.is_a?(Array) ? stimulus_target(*target) : stimulus_target(target)
85
+ end
86
+ StimulusTargetCollection.new(converted_targets)
87
+ end
88
+
89
+ # Parse inputs to create a StimulusOutlet instance representing a Stimulus outlet attribute
90
+ # examples:
91
+ # stimulus_outlet(:user_status, ".online-user") => StimulusOutlet that converts to {"current_controller-user-status-outlet" => ".online-user"}
92
+ # stimulus_outlet("path/to/current", :user_status, ".online-user") => StimulusOutlet that converts to {"path--to--current-user-status-outlet" => ".online-user"}
93
+ # stimulus_outlet(:user_status) => StimulusOutlet with auto-generated selector
94
+ # stimulus_outlet(component_instance) => StimulusOutlet from component
95
+ def stimulus_outlet(*args)
96
+ return args.first if args.length == 1 && args.first.is_a?(StimulusOutlet)
97
+ StimulusOutlet.new(*args, implied_controller:, component_id: @id)
98
+ end
99
+
100
+ # Parse inputs to create a StimulusOutletCollection instance representing multiple Stimulus outlets
101
+ def stimulus_outlets(*outlets)
102
+ return StimulusOutletCollection.new if outlets.empty? || outlets.all?(&:blank?)
103
+
104
+ converted_outlets = []
105
+ outlets.each do |outlet|
106
+ if outlet.is_a?(Hash)
107
+ # Hash format: {name: selector, other_name: other_selector} - expands to multiple outlets
108
+ outlet.each { |name, selector| converted_outlets << stimulus_outlet(name, selector) }
109
+ elsif outlet.is_a?(Array)
110
+ # Array format: [name, selector] - splat into stimulus_outlet
111
+ converted_outlets << stimulus_outlet(*outlet)
112
+ else
113
+ converted_outlets << stimulus_outlet(outlet)
114
+ end
115
+ end
116
+ StimulusOutletCollection.new(converted_outlets)
117
+ end
118
+
119
+ # Parse inputs to create a StimulusValue instance representing a Stimulus value attribute
120
+ # examples:
121
+ # stimulus_value(:url, "https://example.com") => StimulusValue that converts to {"current_controller-url-value" => "https://example.com"}
122
+ # stimulus_value("path/to/current", :url, "https://example.com") => StimulusValue that converts to {"path--to--current-url-value" => "https://example.com"}
123
+ def stimulus_value(*args)
124
+ return args.first if args.length == 1 && args.first.is_a?(StimulusValue)
125
+ StimulusValue.new(*args, implied_controller:)
126
+ end
127
+
128
+ # Parse inputs to create a StimulusValueCollection instance representing multiple Stimulus values
129
+ def stimulus_values(*values)
130
+ return StimulusValueCollection.new if values.empty? || values.all?(&:blank?)
131
+
132
+ converted_values = []
133
+
134
+ values.each do |value|
135
+ if value.is_a?(Hash)
136
+ # Hash format: {name: value, other_name: other_value} - expands to multiple values
137
+ value.each { |name, val| converted_values << stimulus_value(name, val) }
138
+ elsif value.is_a?(Array)
139
+ # Array format: [controller, name, value] or [name, value] - splat into stimulus_value
140
+ converted_values << stimulus_value(*value)
141
+ else
142
+ converted_values << stimulus_value(value)
143
+ end
144
+ end
145
+
146
+ StimulusValueCollection.new(converted_values)
147
+ end
148
+
149
+ # Parse inputs to create a StimulusClass instance representing a Stimulus class attribute
150
+ # examples:
151
+ # stimulus_class(:loading, "spinner active") => StimulusClass that converts to {"current_controller-loading-class" => "spinner active"}
152
+ # stimulus_class("path/to/current", :loading, ["spinner", "active"]) => StimulusClass that converts to {"path--to--current-loading-class" => "spinner active"}
153
+ def stimulus_class(*args)
154
+ return args.first if args.length == 1 && args.first.is_a?(StimulusClass)
155
+ StimulusClass.new(*args, implied_controller:)
156
+ end
157
+
158
+ # Parse inputs to create a StimulusClassCollection instance representing multiple Stimulus classes
159
+ def stimulus_classes(*classes)
160
+ return StimulusClassCollection.new if classes.empty? || classes.all?(&:blank?)
161
+
162
+ converted_classes = []
163
+
164
+ classes.each do |cls|
165
+ if cls.is_a?(Hash)
166
+ # Hash format: {loading: "spinner active", error: "text-red-500"} - expands to multiple classes
167
+ cls.each { |name, class_list| converted_classes << stimulus_class(name, class_list) }
168
+ elsif cls.is_a?(Array)
169
+ # Array format: [controller, name, classes] or [name, classes] - splat into stimulus_class
170
+ converted_classes << stimulus_class(*cls)
171
+ else
172
+ converted_classes << stimulus_class(cls)
173
+ end
174
+ end
175
+
176
+ StimulusClassCollection.new(converted_classes)
177
+ end
178
+
179
+ # Methods to add to the stimulus collections
180
+
181
+ def add_stimulus_controllers(controllers)
182
+ s_controllers = stimulus_controllers(*Array.wrap(controllers))
183
+ @stimulus_controllers_collection = if @stimulus_controllers_collection
184
+ @stimulus_controllers_collection.merge(s_controllers)
185
+ else
186
+ s_controllers
187
+ end
188
+ end
189
+
190
+ def add_stimulus_actions(actions)
191
+ s_actions = stimulus_actions(*Array.wrap(actions))
192
+ @stimulus_actions_collection = if @stimulus_actions_collection
193
+ @stimulus_actions_collection.merge(s_actions)
194
+ else
195
+ s_actions
196
+ end
197
+ end
198
+
199
+ def add_stimulus_targets(targets)
200
+ s_targets = stimulus_targets(*Array.wrap(targets))
201
+ @stimulus_targets_collection = if @stimulus_targets_collection
202
+ @stimulus_targets_collection.merge(s_targets)
203
+ else
204
+ s_targets
205
+ end
206
+ end
207
+
208
+ def add_stimulus_outlets(outlets)
209
+ s_outlets = stimulus_outlets(*Array.wrap(outlets))
210
+ @stimulus_outlets_collection = if @stimulus_outlets_collection
211
+ @stimulus_outlets_collection.merge(s_outlets)
212
+ else
213
+ s_outlets
214
+ end
215
+ end
216
+
217
+ def add_stimulus_values(values)
218
+ s_values = stimulus_values(values)
219
+ @stimulus_values_collection = if @stimulus_values_collection
220
+ @stimulus_values_collection.merge(s_values)
221
+ else
222
+ s_values
223
+ end
224
+ end
225
+
226
+ def add_stimulus_classes(named_classes)
227
+ classes = stimulus_classes(named_classes)
228
+ @stimulus_classes_collection = if @stimulus_classes_collection
229
+ @stimulus_classes_collection.merge(classes)
230
+ else
231
+ classes
232
+ end
233
+ end
234
+
235
+ # Stimulus events name for this component
236
+ def stimulus_scoped_event(event)
237
+ self.class.stimulus_scoped_event(event)
238
+ end
239
+
240
+ def stimulus_scoped_event_on_window(event)
241
+ self.class.stimulus_scoped_event_on_window(event)
242
+ end
243
+
244
+ private
245
+
246
+ def implied_controller
247
+ StimulusController.new(implied_controller: implied_controller_path)
248
+ end
249
+
250
+ # When using the DSL if you dont specify, the first controller is implied
251
+ def implied_controller_path
252
+ return @implied_controller_path if defined?(@implied_controller_path)
253
+ path = Array.wrap(@stimulus_controllers).first
254
+ raise(StandardError, "No controllers have been specified") unless path
255
+ @implied_controller_path = path
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ class StimulusBuilder
5
+ def initialize
6
+ @actions = []
7
+ @targets = []
8
+ @values = {}
9
+ @values_from_props = []
10
+ @classes = {}
11
+ @outlets = {}
12
+ end
13
+
14
+ def merge_with(other_builder)
15
+ @actions.concat(other_builder.actions_list)
16
+ @targets.concat(other_builder.targets_list)
17
+ @values.merge!(other_builder.values_hash)
18
+ @values_from_props.concat(other_builder.values_from_props_list)
19
+ @classes.merge!(other_builder.classes_hash)
20
+ @outlets.merge!(other_builder.outlets_hash)
21
+ self
22
+ end
23
+
24
+ def actions(*action_names)
25
+ @actions.concat(action_names)
26
+ self
27
+ end
28
+
29
+ def targets(*target_names)
30
+ @targets.concat(target_names)
31
+ self
32
+ end
33
+
34
+ def values(**value_hash)
35
+ @values.merge!(value_hash) unless value_hash.empty?
36
+ self
37
+ end
38
+
39
+ def values_from_props(*prop_names)
40
+ @values_from_props.concat(prop_names)
41
+ self
42
+ end
43
+
44
+ def classes(**class_mappings)
45
+ @classes.merge!(class_mappings)
46
+ self
47
+ end
48
+
49
+ def outlets(**outlet_mappings)
50
+ @outlets.merge!(outlet_mappings) unless outlet_mappings.empty?
51
+ self
52
+ end
53
+
54
+ def to_attributes(component_instance)
55
+ attrs = {}
56
+ attrs[:stimulus_actions] = resolve_attributes_filtering_nil(@actions, component_instance) unless @actions.empty?
57
+ attrs[:stimulus_targets] = resolve_attributes_filtering_nil(@targets, component_instance) unless @targets.empty?
58
+ attrs[:stimulus_values] = resolve_hash_values_allowing_nil(@values, component_instance) unless @values.empty?
59
+ attrs[:stimulus_values_from_props] = @values_from_props.dup unless @values_from_props.empty?
60
+ attrs[:stimulus_classes] = resolve_hash_classes_filtering_nil(@classes, component_instance) unless @classes.empty?
61
+ attrs[:stimulus_outlets] = @outlets.dup unless @outlets.empty?
62
+ attrs
63
+ end
64
+
65
+ def to_hash(component_instance)
66
+ to_attributes(component_instance)
67
+ end
68
+ alias_method :to_h, :to_hash
69
+
70
+ protected
71
+
72
+ def actions_list
73
+ @actions
74
+ end
75
+
76
+ def targets_list
77
+ @targets
78
+ end
79
+
80
+ def values_hash
81
+ @values
82
+ end
83
+
84
+ def values_from_props_list
85
+ @values_from_props
86
+ end
87
+
88
+ def classes_hash
89
+ @classes
90
+ end
91
+
92
+ def outlets_hash
93
+ @outlets
94
+ end
95
+
96
+ private
97
+
98
+ # For actions, targets - filter out nil values from procs AND static
99
+ def resolve_attributes_filtering_nil(array, component_instance)
100
+ result = []
101
+ array.each do |value|
102
+ if callable?(value)
103
+ resolved_value = component_instance.instance_exec(&value)
104
+ # Exclude nil from procs (nil is not valid for actions/targets)
105
+ result << resolved_value unless resolved_value.nil?
106
+ else
107
+ # Exclude static nil values (nil is not valid for actions/targets)
108
+ result << value unless value.nil?
109
+ end
110
+ end
111
+ result
112
+ end
113
+
114
+ # For values - allow nil values from procs and static (will become "null" in JavaScript)
115
+ def resolve_hash_values_allowing_nil(hash, component_instance)
116
+ hash.transform_values { |value| callable?(value) ? component_instance.instance_exec(&value) : value }
117
+ end
118
+
119
+ # For classes - filter out nil values from procs AND static
120
+ def resolve_hash_classes_filtering_nil(hash, component_instance)
121
+ result = {}
122
+ hash.each do |key, value|
123
+ if callable?(value)
124
+ resolved_value = component_instance.instance_exec(&value)
125
+ # Exclude nil from procs (nil is not valid for classes)
126
+ result[key] = resolved_value unless resolved_value.nil?
127
+ else
128
+ # Exclude static nil values (nil is not valid for classes)
129
+ result[key] = value unless value.nil?
130
+ end
131
+ end
132
+ result
133
+ end
134
+
135
+ def callable?(value)
136
+ value.respond_to?(:call)
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ class StimulusClass < StimulusAttributeBase
5
+ attr_reader :controller, :class_name, :css_classes
6
+
7
+ def to_s
8
+ @css_classes.join(" ")
9
+ end
10
+
11
+ def data_attribute_name
12
+ "#{@controller}-#{@class_name}-class"
13
+ end
14
+
15
+ def data_attribute_value
16
+ to_s
17
+ end
18
+
19
+ private
20
+
21
+ def parse_arguments(*args)
22
+ case args.size
23
+ when 2
24
+ parse_two_arguments(args[0], args[1])
25
+ when 3
26
+ parse_three_arguments(args[0], args[1], args[2])
27
+ else
28
+ raise ArgumentError, "Invalid number of arguments: #{args.size}"
29
+ end
30
+ end
31
+
32
+ def parse_two_arguments(class_name, css_classes)
33
+ if class_name.is_a?(Symbol)
34
+ # class name on implied controller + css classes
35
+ @controller = implied_controller_name
36
+ @class_name = class_name.to_s.dasherize
37
+ @css_classes = normalize_css_classes(css_classes)
38
+ else
39
+ raise ArgumentError, "Invalid argument types: #{class_name.class}, #{css_classes.class}"
40
+ end
41
+ end
42
+
43
+ def parse_three_arguments(controller, class_name, css_classes)
44
+ if controller.is_a?(String) && class_name.is_a?(Symbol)
45
+ # controller + class name + css classes
46
+ @controller = stimulize_path(controller)
47
+ @class_name = class_name.to_s.dasherize
48
+ @css_classes = normalize_css_classes(css_classes)
49
+ else
50
+ raise ArgumentError, "Invalid argument types: #{controller.class}, #{class_name.class}, #{css_classes.class}"
51
+ end
52
+ end
53
+
54
+ def normalize_css_classes(css_classes)
55
+ case css_classes
56
+ when String
57
+ css_classes.split(/\s+/).reject(&:empty?)
58
+ when Array
59
+ css_classes.map(&:to_s).reject(&:empty?)
60
+ else
61
+ raise ArgumentError, "CSS classes must be a String or Array, got #{css_classes.class}"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ class StimulusClassCollection < StimulusCollectionBase
5
+ def to_h
6
+ return {} if items.empty?
7
+
8
+ merged = {}
9
+ items.each do |css_class|
10
+ merged.merge!(css_class.to_h)
11
+ end
12
+ merged
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ class StimulusCollectionBase
5
+ def initialize(items = [])
6
+ @items = Array(items).flatten.compact
7
+ end
8
+
9
+ def <<(item)
10
+ @items << item
11
+ self
12
+ end
13
+
14
+ def to_h
15
+ raise NoMethodError, "Subclasses must implement to_h"
16
+ end
17
+
18
+ def to_a
19
+ @items.dup
20
+ end
21
+
22
+ def to_hash
23
+ to_h
24
+ end
25
+
26
+ def empty?
27
+ @items.empty?
28
+ end
29
+
30
+ def any?
31
+ !empty?
32
+ end
33
+
34
+ def merge(*other_collections)
35
+ merged = self.class.new
36
+ merged.instance_variable_set(:@items, @items.dup)
37
+
38
+ other_collections.each do |collection|
39
+ next unless collection.is_a?(self.class)
40
+ merged.instance_variable_get(:@items).concat(collection.instance_variable_get(:@items))
41
+ end
42
+
43
+ merged
44
+ end
45
+
46
+ def self.merge(*collections)
47
+ return new if collections.empty?
48
+
49
+ first_collection = collections.first
50
+ return first_collection if collections.size == 1
51
+
52
+ first_collection.merge(*collections[1..-1])
53
+ end
54
+
55
+ protected
56
+
57
+ attr_reader :items
58
+ end
59
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ module StimulusComponent
5
+ extend ActiveSupport::Concern
6
+
7
+ include StimulusAttributes
8
+
9
+ # Module utilities for working with Stimulus identifiers
10
+
11
+ def stimulus_identifier_from_path(path)
12
+ path.split("/").map { |p| p.to_s.dasherize }.join("--")
13
+ end
14
+ module_function :stimulus_identifier_from_path
15
+
16
+ # Base class for all Vident components, which provides common functionality and properties.
17
+
18
+ class_methods do
19
+ def no_stimulus_controller
20
+ @no_stimulus_controller = true
21
+ end
22
+
23
+ def stimulus_controller? = !@no_stimulus_controller
24
+
25
+ # The "path" of the Stimulus controller, which is used to generate the controller name.
26
+ def stimulus_identifier_path = name&.underscore || "anonymous_component"
27
+
28
+ # Stimulus controller identifier
29
+ def stimulus_identifier = ::Vident::StimulusComponent.stimulus_identifier_from_path(stimulus_identifier_path)
30
+
31
+ # The "name" of the component from its class name and namespace. This is used to generate an HTML class name
32
+ # that can helps identify the component type in the DOM or for styling purposes.
33
+ def component_name
34
+ @component_name ||= stimulus_identifier
35
+ end
36
+ end
37
+
38
+ # Components have the following properties
39
+ included do
40
+ extend Literal::Properties
41
+
42
+ # StimulusJS support
43
+ # # TODO: revisit inputs and how many ways of specifying the same thing...
44
+ prop :stimulus_controllers, _Array(_Union(String, Symbol, StimulusController, StimulusControllerCollection)), default: -> do
45
+ if self.class.stimulus_controller?
46
+ [default_controller_path]
47
+ else
48
+ []
49
+ end
50
+ end
51
+ prop :stimulus_actions, _Array(_Union(String, Symbol, Array, Hash, StimulusAction, StimulusActionCollection)), default: -> { [] }
52
+ prop :stimulus_targets, _Array(_Union(String, Symbol, Hash, StimulusTarget, StimulusTargetCollection)), default: -> { [] }
53
+ prop :stimulus_outlets, _Array(_Union(String, Symbol, StimulusOutlet, StimulusOutletCollection)), default: -> { [] }
54
+ prop :stimulus_outlet_host, _Nilable(Vident::Component) # A component that will host this component as an outlet
55
+ prop :stimulus_values, _Union(_Hash(Symbol, _Any), StimulusValue, StimulusValueCollection), default: -> { {} } # TODO: instead of _Any, is it _Interface(:to_s)?
56
+ prop :stimulus_classes, _Union(_Hash(Symbol, String), StimulusClass, StimulusClassCollection), default: -> { {} }
57
+ end
58
+
59
+ # If connecting an outlet to this specific component instance, use this ID
60
+ def outlet_id
61
+ @outlet_id ||= [stimulus_identifier, "##{id}"]
62
+ end
63
+
64
+ # The Stimulus controller identifier for this component
65
+ def stimulus_identifier = self.class.stimulus_identifier
66
+
67
+ # An name that can helps identify the component type in the DOM or for styling purposes (its also used as a class name on the root element)
68
+ def component_name = self.class.component_name
69
+
70
+ # The `component` class name is used to create the controller name.
71
+ # The path of the Stimulus controller when none is explicitly set
72
+ def default_controller_path = self.class.stimulus_identifier_path
73
+ end
74
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ class StimulusController < StimulusAttributeBase
5
+ attr_reader :path, :name
6
+
7
+ def to_s
8
+ name
9
+ end
10
+
11
+ def data_attribute_name
12
+ "controller"
13
+ end
14
+
15
+ def data_attribute_value
16
+ name
17
+ end
18
+
19
+ private
20
+
21
+ def implied_controller_path
22
+ @implied_controller
23
+ end
24
+
25
+ def implied_controller_name
26
+ stimulize_path(@implied_controller)
27
+ end
28
+
29
+ def parse_arguments(*args)
30
+ case args.size
31
+ when 0
32
+ # No arguments: use implied controller path
33
+ @path = implied_controller_path
34
+ @name = implied_controller_name
35
+ when 1
36
+ # Single argument: controller path
37
+ @path = args[0]
38
+ @name = stimulize_path(args[0])
39
+ else
40
+ raise ArgumentError, "Invalid number of arguments: #{args.size}. Expected 0 or 1 argument."
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ class StimulusControllerCollection < StimulusCollectionBase
5
+ def to_h
6
+ return {} if items.empty?
7
+
8
+ controller_values = items.map(&:to_s).reject(&:empty?)
9
+ return {} if controller_values.empty?
10
+
11
+ {controller: controller_values.join(" ")}
12
+ end
13
+ end
14
+ end