vident 1.0.0.beta2 → 1.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -1
  3. data/README.md +171 -17
  4. data/lib/generators/vident/install/install_generator.rb +53 -0
  5. data/lib/generators/vident/install/templates/vident.rb +20 -0
  6. data/lib/vident/caching.rb +3 -9
  7. data/lib/vident/child_element_helper.rb +22 -21
  8. data/lib/vident/component.rb +3 -10
  9. data/lib/vident/component_attribute_resolver.rb +21 -36
  10. data/lib/vident/component_class_lists.rb +4 -3
  11. data/lib/vident/stable_id.rb +48 -17
  12. data/lib/vident/stimulus/naming.rb +19 -0
  13. data/lib/vident/stimulus/primitive.rb +38 -0
  14. data/lib/vident/stimulus.rb +31 -0
  15. data/lib/vident/stimulus_action.rb +58 -23
  16. data/lib/vident/stimulus_attribute_base.rb +27 -23
  17. data/lib/vident/stimulus_attributes.rb +56 -185
  18. data/lib/vident/stimulus_builder.rb +66 -87
  19. data/lib/vident/stimulus_class.rb +3 -9
  20. data/lib/vident/stimulus_class_collection.rb +1 -5
  21. data/lib/vident/stimulus_collection_base.rb +4 -12
  22. data/lib/vident/stimulus_component.rb +8 -7
  23. data/lib/vident/stimulus_controller.rb +10 -13
  24. data/lib/vident/stimulus_data_attribute_builder.rb +15 -74
  25. data/lib/vident/stimulus_helper.rb +4 -12
  26. data/lib/vident/stimulus_null.rb +21 -0
  27. data/lib/vident/stimulus_outlet.rb +3 -9
  28. data/lib/vident/stimulus_outlet_collection.rb +1 -5
  29. data/lib/vident/stimulus_param.rb +42 -0
  30. data/lib/vident/stimulus_param_collection.rb +11 -0
  31. data/lib/vident/stimulus_target.rb +7 -17
  32. data/lib/vident/stimulus_target_collection.rb +2 -6
  33. data/lib/vident/stimulus_value.rb +14 -44
  34. data/lib/vident/stimulus_value_collection.rb +1 -5
  35. data/lib/vident/tailwind.rb +0 -2
  36. data/lib/vident/version.rb +1 -1
  37. data/lib/vident.rb +7 -12
  38. data/skills/vident/SKILL.md +628 -0
  39. metadata +10 -1
@@ -3,10 +3,14 @@
3
3
  module Vident
4
4
  module StimulusAttributes
5
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
6
11
 
7
12
  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.
13
+ # Symbol so the action parser treats it as a Stimulus event type.
10
14
  def stimulus_scoped_event(event)
11
15
  :"#{component_name}:#{stimulus_js_name(event)}"
12
16
  end
@@ -17,229 +21,95 @@ module Vident
17
21
 
18
22
  private
19
23
 
20
- def stimulus_js_name(name)
21
- name.to_s.camelize(:lower)
22
- end
24
+ def stimulus_js_name(name) = name.to_s.camelize(:lower)
23
25
  end
24
26
 
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
27
  def stimulus_controller(*args)
31
28
  return args.first if args.length == 1 && args.first.is_a?(StimulusController)
32
29
  StimulusController.new(*args, implied_controller: implied_controller_path)
33
30
  end
34
31
 
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)
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)
44
63
  end
45
- StimulusControllerCollection.new(converted_controllers)
46
64
  end
47
65
 
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
66
  def stimulus_action(*args)
56
67
  return args.first if args.length == 1 && args.first.is_a?(StimulusAction)
57
68
  StimulusAction.new(*args, implied_controller:)
58
69
  end
59
70
 
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
71
  def stimulus_target(*args)
75
72
  return args.first if args.length == 1 && args.first.is_a?(StimulusTarget)
76
73
  StimulusTarget.new(*args, implied_controller:)
77
74
  end
78
75
 
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
76
+ # `component_id: @id` scopes the auto-generated selector to this component
77
+ # instance (e.g. `#<host-id> [data-controller~=<outlet>]`).
95
78
  def stimulus_outlet(*args)
96
79
  return args.first if args.length == 1 && args.first.is_a?(StimulusOutlet)
97
80
  StimulusOutlet.new(*args, implied_controller:, component_id: @id)
98
81
  end
99
82
 
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
83
  def stimulus_value(*args)
124
84
  return args.first if args.length == 1 && args.first.is_a?(StimulusValue)
125
85
  StimulusValue.new(*args, implied_controller:)
126
86
  end
127
87
 
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)
88
+ def stimulus_param(*args)
89
+ return args.first if args.length == 1 && args.first.is_a?(StimulusParam)
90
+ StimulusParam.new(*args, implied_controller:)
147
91
  end
148
92
 
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
93
  def stimulus_class(*args)
154
94
  return args.first if args.length == 1 && args.first.is_a?(StimulusClass)
155
95
  StimulusClass.new(*args, implied_controller:)
156
96
  end
157
97
 
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
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)
196
107
  end
197
108
  end
198
109
 
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
110
+ def stimulus_scoped_event(event) = self.class.stimulus_scoped_event(event)
207
111
 
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
112
+ def stimulus_scoped_event_on_window(event) = self.class.stimulus_scoped_event_on_window(event)
243
113
 
244
114
  private
245
115
 
@@ -247,7 +117,8 @@ module Vident
247
117
  StimulusController.new(implied_controller: implied_controller_path)
248
118
  end
249
119
 
250
- # When using the DSL if you dont specify, the first controller is implied
120
+ # The first registered controller path becomes the implied controller for
121
+ # unqualified DSL entries (e.g. `actions :click` → `implied#click`).
251
122
  def implied_controller_path
252
123
  return @implied_controller_path if defined?(@implied_controller_path)
253
124
  path = Array.wrap(@stimulus_controllers).first
@@ -2,139 +2,118 @@
2
2
 
3
3
  module Vident
4
4
  class StimulusBuilder
5
+ # Primitives the DSL block tracks. Controllers are set via the component's
6
+ # `stimulus_controllers:` prop, not the DSL, so they're skipped here.
7
+ # Storage shape per primitive is an Array for positional kinds (actions,
8
+ # targets) and a Hash for keyed kinds (values, params, classes, outlets).
9
+ DSL_PRIMITIVES = Stimulus::PRIMITIVES.reject { |primitive| primitive.name == :controllers }.freeze
10
+
5
11
  def initialize
6
- @actions = []
7
- @targets = []
8
- @values = {}
12
+ @entries = DSL_PRIMITIVES.to_h { |primitive| [primitive.name, primitive.keyed? ? {} : []] }
9
13
  @values_from_props = []
10
- @classes = {}
11
- @outlets = {}
12
14
  end
13
15
 
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)
16
+ def merge_with(other)
17
+ DSL_PRIMITIVES.each do |primitive|
18
+ mine = @entries[primitive.name]
19
+ theirs = other.entries_for(primitive.name)
20
+ primitive.keyed? ? mine.merge!(theirs) : mine.concat(theirs)
21
+ end
22
+ @values_from_props.concat(other.values_from_props_list)
23
+ self
24
+ end
25
+
26
+ def actions(*names)
27
+ @entries[:actions].concat(names)
21
28
  self
22
29
  end
23
30
 
24
- def actions(*action_names)
25
- @actions.concat(action_names)
31
+ def targets(*names)
32
+ @entries[:targets].concat(names)
26
33
  self
27
34
  end
28
35
 
29
- def targets(*target_names)
30
- @targets.concat(target_names)
36
+ def values(**hash)
37
+ @entries[:values].merge!(hash) unless hash.empty?
31
38
  self
32
39
  end
33
40
 
34
- def values(**value_hash)
35
- @values.merge!(value_hash) unless value_hash.empty?
41
+ def params(**hash)
42
+ @entries[:params].merge!(hash) unless hash.empty?
36
43
  self
37
44
  end
38
45
 
39
- def values_from_props(*prop_names)
40
- @values_from_props.concat(prop_names)
46
+ def classes(**hash)
47
+ @entries[:classes].merge!(hash) unless hash.empty?
41
48
  self
42
49
  end
43
50
 
44
- def classes(**class_mappings)
45
- @classes.merge!(class_mappings)
51
+ def values_from_props(*names)
52
+ @values_from_props.concat(names)
46
53
  self
47
54
  end
48
55
 
49
- def outlets(positional = nil, **outlet_mappings)
50
- @outlets.merge!(positional) if positional.is_a?(Hash)
51
- @outlets.merge!(outlet_mappings) unless outlet_mappings.empty?
56
+ # `outlets({"admin--users" => ".sel"})` accepts a positional Hash for
57
+ # identifiers that can't be Ruby kwarg keys (contain `--`).
58
+ def outlets(positional = nil, **hash)
59
+ bucket = @entries[:outlets]
60
+ bucket.merge!(positional) if positional.is_a?(Hash)
61
+ bucket.merge!(hash) unless hash.empty?
52
62
  self
53
63
  end
54
64
 
55
65
  def to_attributes(component_instance)
56
66
  attrs = {}
57
- attrs[:stimulus_actions] = resolve_attributes_filtering_nil(@actions, component_instance) unless @actions.empty?
58
- attrs[:stimulus_targets] = resolve_attributes_filtering_nil(@targets, component_instance) unless @targets.empty?
59
- attrs[:stimulus_values] = resolve_hash_values_allowing_nil(@values, component_instance) unless @values.empty?
67
+ DSL_PRIMITIVES.each do |primitive|
68
+ entries = @entries[primitive.name]
69
+ next if entries.empty?
70
+ attrs[primitive.key] = resolve_entries(primitive, entries, component_instance)
71
+ end
60
72
  attrs[:stimulus_values_from_props] = @values_from_props.dup unless @values_from_props.empty?
61
- attrs[:stimulus_classes] = resolve_hash_classes_filtering_nil(@classes, component_instance) unless @classes.empty?
62
- attrs[:stimulus_outlets] = @outlets.dup unless @outlets.empty?
63
73
  attrs
64
74
  end
65
75
 
66
- def to_hash(component_instance)
67
- to_attributes(component_instance)
68
- end
76
+ def to_hash(component_instance) = to_attributes(component_instance)
69
77
  alias_method :to_h, :to_hash
70
78
 
71
79
  protected
72
80
 
73
- def actions_list
74
- @actions
75
- end
76
-
77
- def targets_list
78
- @targets
79
- end
80
-
81
- def values_hash
82
- @values
83
- end
84
-
85
- def values_from_props_list
86
- @values_from_props
87
- end
88
-
89
- def classes_hash
90
- @classes
91
- end
81
+ def entries_for(name) = @entries[name]
92
82
 
93
- def outlets_hash
94
- @outlets
95
- end
83
+ def values_from_props_list = @values_from_props
96
84
 
97
85
  private
98
86
 
99
- # For actions, targets - filter out nil values from procs AND static
100
- def resolve_attributes_filtering_nil(array, component_instance)
101
- result = []
102
- array.each do |value|
103
- if callable?(value)
104
- resolved_value = component_instance.instance_exec(&value)
105
- # Exclude nil from procs (nil is not valid for actions/targets)
106
- result << resolved_value unless resolved_value.nil?
107
- else
108
- # Exclude static nil values (nil is not valid for actions/targets)
109
- result << value unless value.nil?
110
- end
87
+ # Outlets don't support procs static merge only. The other keyed kinds
88
+ # and the positional (Array-shaped) kinds resolve procs in the component
89
+ # instance and drop nil results.
90
+ def resolve_entries(primitive, entries, component_instance)
91
+ return entries.dup if primitive.name == :outlets
92
+
93
+ if primitive.keyed?
94
+ resolve_hash_filtering_nil(entries, component_instance)
95
+ else
96
+ resolve_array_filtering_nil(entries, component_instance)
111
97
  end
112
- result
113
98
  end
114
99
 
115
- # For values - allow nil values from procs and static (will become "null" in JavaScript)
116
- def resolve_hash_values_allowing_nil(hash, component_instance)
117
- hash.transform_values { |value| callable?(value) ? component_instance.instance_exec(&value) : value }
100
+ def resolve_array_filtering_nil(array, component_instance)
101
+ array.each_with_object([]) do |value, out|
102
+ resolved = callable?(value) ? component_instance.instance_exec(&value) : value
103
+ out << resolved unless resolved.nil?
104
+ end
118
105
  end
119
106
 
120
- # For classes - filter out nil values from procs AND static
121
- def resolve_hash_classes_filtering_nil(hash, component_instance)
122
- result = {}
123
- hash.each do |key, value|
124
- if callable?(value)
125
- resolved_value = component_instance.instance_exec(&value)
126
- # Exclude nil from procs (nil is not valid for classes)
127
- result[key] = resolved_value unless resolved_value.nil?
128
- else
129
- # Exclude static nil values (nil is not valid for classes)
130
- result[key] = value unless value.nil?
131
- end
107
+ # Dropping nil matters because Stimulus's Boolean value parser reads an
108
+ # empty data attribute as `true` — so `-> { flag? || nil }` would silently
109
+ # flip a Boolean value on. Omitting the entry keeps the attribute off.
110
+ def resolve_hash_filtering_nil(hash, component_instance)
111
+ hash.each_with_object({}) do |(key, value), out|
112
+ resolved = callable?(value) ? component_instance.instance_exec(&value) : value
113
+ out[key] = resolved unless resolved.nil?
132
114
  end
133
- result
134
115
  end
135
116
 
136
- def callable?(value)
137
- value.respond_to?(:call)
138
- end
117
+ def callable?(value) = value.respond_to?(:call)
139
118
  end
140
119
  end
@@ -4,17 +4,11 @@ module Vident
4
4
  class StimulusClass < StimulusAttributeBase
5
5
  attr_reader :controller, :class_name, :css_classes
6
6
 
7
- def to_s
8
- @css_classes.join(" ")
9
- end
7
+ def to_s = @css_classes.join(" ")
10
8
 
11
- def data_attribute_name
12
- "#{@controller}-#{@class_name}-class"
13
- end
9
+ def data_attribute_name = "#{@controller}-#{@class_name}-class"
14
10
 
15
- def data_attribute_value
16
- to_s
17
- end
11
+ def data_attribute_value = to_s
18
12
 
19
13
  private
20
14
 
@@ -5,11 +5,7 @@ module Vident
5
5
  def to_h
6
6
  return {} if items.empty?
7
7
 
8
- merged = {}
9
- items.each do |css_class|
10
- merged.merge!(css_class.to_h)
11
- end
12
- merged
8
+ items.each_with_object({}) { |css_class, merged| merged.merge!(css_class.to_h) }
13
9
  end
14
10
  end
15
11
  end
@@ -15,21 +15,13 @@ module Vident
15
15
  raise NoMethodError, "Subclasses must implement to_h"
16
16
  end
17
17
 
18
- def to_a
19
- @items.dup
20
- end
18
+ def to_a = @items.dup
21
19
 
22
- def to_hash
23
- to_h
24
- end
20
+ def to_hash = to_h
25
21
 
26
- def empty?
27
- @items.empty?
28
- end
22
+ def empty? = @items.empty?
29
23
 
30
- def any?
31
- !empty?
32
- end
24
+ def any? = !empty?
33
25
 
34
26
  def merge(*other_collections)
35
27
  merged = self.class.new
@@ -6,10 +6,10 @@ module Vident
6
6
 
7
7
  include StimulusAttributes
8
8
 
9
- # Module utilities for working with Stimulus identifiers
10
-
9
+ # Thin back-compat alias; see `Vident::StimulusAttributeBase.stimulize_path`
10
+ # for the canonical implementation.
11
11
  def stimulus_identifier_from_path(path)
12
- path.split("/").map { |p| p.to_s.dasherize }.join("--")
12
+ StimulusAttributeBase.stimulize_path(path)
13
13
  end
14
14
  module_function :stimulus_identifier_from_path
15
15
 
@@ -26,7 +26,7 @@ module Vident
26
26
  def stimulus_identifier_path = name&.underscore || "anonymous_component"
27
27
 
28
28
  # Stimulus controller identifier
29
- def stimulus_identifier = ::Vident::StimulusComponent.stimulus_identifier_from_path(stimulus_identifier_path)
29
+ def stimulus_identifier = StimulusComponent.stimulus_identifier_from_path(stimulus_identifier_path)
30
30
 
31
31
  # The "name" of the component from its class name and namespace. This is used to generate an HTML class name
32
32
  # that can helps identify the component type in the DOM or for styling purposes.
@@ -48,12 +48,13 @@ module Vident
48
48
  []
49
49
  end
50
50
  end
51
- prop :stimulus_actions, _Array(_Union(String, Symbol, Array, Hash, StimulusAction, StimulusActionCollection)), default: -> { [] }
51
+ prop :stimulus_actions, _Array(_Union(String, Symbol, Array, Hash, StimulusAction, StimulusAction::Descriptor, StimulusActionCollection)), default: -> { [] }
52
52
  prop :stimulus_targets, _Array(_Union(String, Symbol, Array, Hash, StimulusTarget, StimulusTargetCollection)), default: -> { [] }
53
53
  prop :stimulus_outlets, _Array(_Union(String, Symbol, StimulusOutlet, StimulusOutletCollection)), default: -> { [] }
54
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: -> { {} }
55
+ prop :stimulus_values, _Union(_Hash(Symbol, _Any), Array, StimulusValue, StimulusValueCollection), default: -> { {} } # TODO: instead of _Any, is it _Interface(:to_s)?
56
+ prop :stimulus_params, _Union(_Hash(Symbol, _Any), Array, StimulusParam, StimulusParamCollection), default: -> { {} }
57
+ prop :stimulus_classes, _Union(_Hash(Symbol, String), Array, StimulusClass, StimulusClassCollection), default: -> { {} }
57
58
  end
58
59
 
59
60
  # If connecting an outlet to this specific component instance, use this ID
@@ -4,38 +4,35 @@ module Vident
4
4
  class StimulusController < StimulusAttributeBase
5
5
  attr_reader :path, :name
6
6
 
7
- def to_s
8
- name
9
- end
7
+ def to_s = name
10
8
 
11
- def data_attribute_name
12
- "controller"
13
- end
9
+ def data_attribute_name = "controller"
14
10
 
15
- def data_attribute_value
16
- name
17
- end
11
+ def data_attribute_value = name
18
12
 
19
13
  private
20
14
 
15
+ # `@implied_controller` on this class is a raw path String (not a
16
+ # StimulusController instance as on the base), so the base class's
17
+ # `.path` / `.name` accessors don't apply and we override.
21
18
  def implied_controller_path
19
+ raise ArgumentError, "implied_controller is required to get implied controller path" unless @implied_controller
22
20
  @implied_controller
23
21
  end
24
22
 
25
23
  def implied_controller_name
24
+ raise ArgumentError, "implied_controller is required to get implied controller name" unless @implied_controller
26
25
  stimulize_path(@implied_controller)
27
26
  end
28
27
 
29
28
  def parse_arguments(*args)
30
29
  case args.size
31
30
  when 0
32
- # No arguments: use implied controller path
33
31
  @path = implied_controller_path
34
32
  @name = implied_controller_name
35
33
  when 1
36
- # Single argument: controller path
37
- @path = args[0]
38
- @name = stimulize_path(args[0])
34
+ @path = args[0].to_s
35
+ @name = stimulize_path(@path)
39
36
  else
40
37
  raise ArgumentError, "Invalid number of arguments: #{args.size}. Expected 0 or 1 argument."
41
38
  end