vident 0.13.0 → 1.0.0.alpha1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -1
- data/README.md +522 -681
- data/lib/vident/caching.rb +3 -3
- data/lib/vident/class_list_builder.rb +101 -0
- data/lib/vident/component.rb +76 -22
- data/lib/vident/component_attribute_resolver.rb +77 -0
- data/lib/vident/component_class_lists.rb +28 -0
- data/lib/vident/stimulus_action.rb +98 -0
- data/lib/vident/stimulus_action_collection.rb +11 -0
- data/lib/vident/stimulus_attribute_base.rb +63 -0
- data/lib/vident/stimulus_attributes.rb +257 -0
- data/lib/vident/stimulus_builder.rb +116 -0
- data/lib/vident/stimulus_class.rb +65 -0
- data/lib/vident/stimulus_class_collection.rb +15 -0
- data/lib/vident/stimulus_collection_base.rb +59 -0
- data/lib/vident/stimulus_component.rb +74 -0
- data/lib/vident/stimulus_controller.rb +44 -0
- data/lib/vident/stimulus_controller_collection.rb +14 -0
- data/lib/vident/stimulus_data_attribute_builder.rb +91 -0
- data/lib/vident/stimulus_dsl.rb +74 -0
- data/lib/vident/stimulus_outlet.rb +97 -0
- data/lib/vident/stimulus_outlet_collection.rb +15 -0
- data/lib/vident/stimulus_target.rb +57 -0
- data/lib/vident/stimulus_target_collection.rb +22 -0
- data/lib/vident/stimulus_value.rb +69 -0
- data/lib/vident/stimulus_value_collection.rb +15 -0
- data/lib/vident/tag_helper.rb +64 -0
- data/lib/vident/tailwind.rb +23 -0
- data/lib/vident/version.rb +1 -1
- data/lib/vident.rb +44 -3
- metadata +47 -6
- data/lib/vident/attributes/not_typed.rb +0 -81
- data/lib/vident/base.rb +0 -247
- data/lib/vident/root_component.rb +0 -309
@@ -0,0 +1,257 @@
|
|
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
|
9
|
+
def stimulus_scoped_event(event)
|
10
|
+
"#{component_name}:#{stimulus_js_name(event)}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def stimulus_scoped_event_on_window(event)
|
14
|
+
"#{component_name}:#{stimulus_js_name(event)}@window"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def stimulus_js_name(name)
|
20
|
+
name.to_s.camelize(:lower)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Parse inputs to create a StimulusController instance representing a Stimulus controller attribute
|
25
|
+
# examples:
|
26
|
+
# stimulus_controller("my_controller") => StimulusController that converts to {"controller" => "my-controller"}
|
27
|
+
# stimulus_controller("path/to/controller") => StimulusController that converts to {"controller" => "path--to--controller"}
|
28
|
+
# stimulus_controller() => StimulusController that uses implied controller name
|
29
|
+
def stimulus_controller(*args)
|
30
|
+
return args.first if args.length == 1 && args.first.is_a?(StimulusController)
|
31
|
+
StimulusController.new(*args, implied_controller: implied_controller_path)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Parse inputs to create a StimulusControllerCollection instance representing multiple Stimulus controllers
|
35
|
+
# examples:
|
36
|
+
# stimulus_controllers(:my_controller) => StimulusControllerCollection with one controller that converts to {"controller" => "my-controller"}
|
37
|
+
# stimulus_controllers(:my_controller, "path/to/another") => StimulusControllerCollection with two controllers that converts to {"controller" => "my-controller path--to--another"}
|
38
|
+
def stimulus_controllers(*controllers)
|
39
|
+
return StimulusControllerCollection.new if controllers.empty? || controllers.all?(&:blank?)
|
40
|
+
|
41
|
+
converted_controllers = controllers.map do |controller|
|
42
|
+
controller.is_a?(Array) ? stimulus_controller(*controller) : stimulus_controller(controller)
|
43
|
+
end
|
44
|
+
StimulusControllerCollection.new(converted_controllers)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Parse inputs to create a StimulusAction instance representing a Stimulus action attribute
|
48
|
+
# examples:
|
49
|
+
# stimulus_action(:my_thing) => StimulusAction that converts to "current_controller#myThing"
|
50
|
+
# stimulus_action(:click, :my_thing) => StimulusAction that converts to "click->current_controller#myThing"
|
51
|
+
# stimulus_action("click->current_controller#myThing") => StimulusAction that converts to "click->current_controller#myThing"
|
52
|
+
# stimulus_action("path/to/current", :my_thing) => StimulusAction that converts to "path--to--current_controller#myThing"
|
53
|
+
# stimulus_action(:click, "path/to/current", :my_thing) => StimulusAction that converts to "click->path--to--current_controller#myThing"
|
54
|
+
def stimulus_action(*args)
|
55
|
+
return args.first if args.length == 1 && args.first.is_a?(StimulusAction)
|
56
|
+
StimulusAction.new(*args, implied_controller:)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Parse inputs to create a StimulusActionCollection instance representing multiple Stimulus actions
|
60
|
+
def stimulus_actions(*actions)
|
61
|
+
return StimulusActionCollection.new if actions.empty? || actions.all?(&:blank?)
|
62
|
+
|
63
|
+
converted_actions = actions.map do |action|
|
64
|
+
action.is_a?(Array) ? stimulus_action(*action) : stimulus_action(action)
|
65
|
+
end
|
66
|
+
StimulusActionCollection.new(converted_actions)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Parse inputs to create a StimulusTarget instance representing a Stimulus target attribute
|
70
|
+
# examples:
|
71
|
+
# stimulus_target(:my_target) => StimulusTarget that converts to {"current_controller-target" => "myTarget"}
|
72
|
+
# stimulus_target("path/to/current", :my_target) => StimulusTarget that converts to {"path--to--current-target" => "myTarget"}
|
73
|
+
def stimulus_target(*args)
|
74
|
+
return args.first if args.length == 1 && args.first.is_a?(StimulusTarget)
|
75
|
+
StimulusTarget.new(*args, implied_controller:)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Parse inputs to create a StimulusTargetCollection instance representing multiple Stimulus targets
|
79
|
+
def stimulus_targets(*targets)
|
80
|
+
return StimulusTargetCollection.new if targets.empty? || targets.all?(&:blank?)
|
81
|
+
|
82
|
+
converted_targets = targets.map do |target|
|
83
|
+
target.is_a?(Array) ? stimulus_target(*target) : stimulus_target(target)
|
84
|
+
end
|
85
|
+
StimulusTargetCollection.new(converted_targets)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Parse inputs to create a StimulusOutlet instance representing a Stimulus outlet attribute
|
89
|
+
# examples:
|
90
|
+
# stimulus_outlet(:user_status, ".online-user") => StimulusOutlet that converts to {"current_controller-user-status-outlet" => ".online-user"}
|
91
|
+
# stimulus_outlet("path/to/current", :user_status, ".online-user") => StimulusOutlet that converts to {"path--to--current-user-status-outlet" => ".online-user"}
|
92
|
+
# stimulus_outlet(:user_status) => StimulusOutlet with auto-generated selector
|
93
|
+
# stimulus_outlet(component_instance) => StimulusOutlet from component
|
94
|
+
def stimulus_outlet(*args)
|
95
|
+
return args.first if args.length == 1 && args.first.is_a?(StimulusOutlet)
|
96
|
+
StimulusOutlet.new(*args, implied_controller:, component_id: @id)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Parse inputs to create a StimulusOutletCollection instance representing multiple Stimulus outlets
|
100
|
+
def stimulus_outlets(*outlets)
|
101
|
+
return StimulusOutletCollection.new if outlets.empty? || outlets.all?(&:blank?)
|
102
|
+
|
103
|
+
converted_outlets = []
|
104
|
+
outlets.each do |outlet|
|
105
|
+
if outlet.is_a?(Hash)
|
106
|
+
# Hash format: {name: selector, other_name: other_selector} - expands to multiple outlets
|
107
|
+
outlet.each { |name, selector| converted_outlets << stimulus_outlet(name, selector) }
|
108
|
+
elsif outlet.is_a?(Array)
|
109
|
+
# Array format: [name, selector] - splat into stimulus_outlet
|
110
|
+
converted_outlets << stimulus_outlet(*outlet)
|
111
|
+
else
|
112
|
+
converted_outlets << stimulus_outlet(outlet)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
StimulusOutletCollection.new(converted_outlets)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Parse inputs to create a StimulusValue instance representing a Stimulus value attribute
|
119
|
+
# examples:
|
120
|
+
# stimulus_value(:url, "https://example.com") => StimulusValue that converts to {"current_controller-url-value" => "https://example.com"}
|
121
|
+
# stimulus_value("path/to/current", :url, "https://example.com") => StimulusValue that converts to {"path--to--current-url-value" => "https://example.com"}
|
122
|
+
def stimulus_value(*args)
|
123
|
+
return args.first if args.length == 1 && args.first.is_a?(StimulusValue)
|
124
|
+
StimulusValue.new(*args, implied_controller:)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Parse inputs to create a StimulusValueCollection instance representing multiple Stimulus values
|
128
|
+
def stimulus_values(*values)
|
129
|
+
return StimulusValueCollection.new if values.empty? || values.all?(&:blank?)
|
130
|
+
|
131
|
+
converted_values = []
|
132
|
+
|
133
|
+
values.each do |value|
|
134
|
+
if value.is_a?(Hash)
|
135
|
+
# Hash format: {name: value, other_name: other_value} - expands to multiple values
|
136
|
+
value.each { |name, val| converted_values << stimulus_value(name, val) }
|
137
|
+
elsif value.is_a?(Array)
|
138
|
+
# Array format: [controller, name, value] or [name, value] - splat into stimulus_value
|
139
|
+
converted_values << stimulus_value(*value)
|
140
|
+
else
|
141
|
+
converted_values << stimulus_value(value)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
StimulusValueCollection.new(converted_values)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Parse inputs to create a StimulusClass instance representing a Stimulus class attribute
|
149
|
+
# examples:
|
150
|
+
# stimulus_class(:loading, "spinner active") => StimulusClass that converts to {"current_controller-loading-class" => "spinner active"}
|
151
|
+
# stimulus_class("path/to/current", :loading, ["spinner", "active"]) => StimulusClass that converts to {"path--to--current-loading-class" => "spinner active"}
|
152
|
+
def stimulus_class(*args)
|
153
|
+
return args.first if args.length == 1 && args.first.is_a?(StimulusClass)
|
154
|
+
StimulusClass.new(*args, implied_controller:)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Parse inputs to create a StimulusClassCollection instance representing multiple Stimulus classes
|
158
|
+
def stimulus_classes(*classes)
|
159
|
+
return StimulusClassCollection.new if classes.empty? || classes.all?(&:blank?)
|
160
|
+
|
161
|
+
converted_classes = []
|
162
|
+
|
163
|
+
classes.each do |cls|
|
164
|
+
if cls.is_a?(Hash)
|
165
|
+
# Hash format: {loading: "spinner active", error: "text-red-500"} - expands to multiple classes
|
166
|
+
cls.each { |name, class_list| converted_classes << stimulus_class(name, class_list) }
|
167
|
+
elsif cls.is_a?(Array)
|
168
|
+
# Array format: [controller, name, classes] or [name, classes] - splat into stimulus_class
|
169
|
+
converted_classes << stimulus_class(*cls)
|
170
|
+
else
|
171
|
+
converted_classes << stimulus_class(cls)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
StimulusClassCollection.new(converted_classes)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Methods to add to the stimulus collections
|
179
|
+
|
180
|
+
def add_stimulus_controllers(controllers)
|
181
|
+
s_controllers = stimulus_controllers(*Array.wrap(controllers))
|
182
|
+
@stimulus_controllers_collection = if @stimulus_controllers_collection
|
183
|
+
@stimulus_controllers_collection.merge(s_controllers)
|
184
|
+
else
|
185
|
+
s_controllers
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def add_stimulus_actions(actions)
|
190
|
+
s_actions = stimulus_actions(*Array.wrap(actions))
|
191
|
+
@stimulus_actions_collection = if @stimulus_actions_collection
|
192
|
+
@stimulus_actions_collection.merge(s_actions)
|
193
|
+
else
|
194
|
+
s_actions
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def add_stimulus_targets(targets)
|
199
|
+
s_targets = stimulus_targets(*Array.wrap(targets))
|
200
|
+
@stimulus_targets_collection = if @stimulus_targets_collection
|
201
|
+
@stimulus_targets_collection.merge(s_targets)
|
202
|
+
else
|
203
|
+
s_targets
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def add_stimulus_outlets(outlets)
|
208
|
+
s_outlets = stimulus_outlets(*Array.wrap(outlets))
|
209
|
+
@stimulus_outlets_collection = if @stimulus_outlets_collection
|
210
|
+
@stimulus_outlets_collection.merge(s_outlets)
|
211
|
+
else
|
212
|
+
s_outlets
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def add_stimulus_values(values)
|
217
|
+
s_values = stimulus_values(values)
|
218
|
+
@stimulus_values_collection = if @stimulus_values_collection
|
219
|
+
@stimulus_values_collection.merge(s_values)
|
220
|
+
else
|
221
|
+
s_values
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def add_stimulus_classes(named_classes)
|
226
|
+
classes = stimulus_classes(named_classes)
|
227
|
+
@stimulus_classes_collection = if @stimulus_classes_collection
|
228
|
+
@stimulus_classes_collection.merge(classes)
|
229
|
+
else
|
230
|
+
classes
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Stimulus events name for this component
|
235
|
+
def stimulus_scoped_event(event)
|
236
|
+
self.class.stimulus_scoped_event(event)
|
237
|
+
end
|
238
|
+
|
239
|
+
def stimulus_scoped_event_on_window(event)
|
240
|
+
self.class.stimulus_scoped_event_on_window(event)
|
241
|
+
end
|
242
|
+
|
243
|
+
private
|
244
|
+
|
245
|
+
def implied_controller
|
246
|
+
StimulusController.new(implied_controller: implied_controller_path)
|
247
|
+
end
|
248
|
+
|
249
|
+
# When using the DSL if you dont specify, the first controller is implied
|
250
|
+
def implied_controller_path
|
251
|
+
return @implied_controller_path if defined?(@implied_controller_path)
|
252
|
+
path = Array.wrap(@stimulus_controllers).first
|
253
|
+
raise(StandardError, "No controllers have been specified") unless path
|
254
|
+
@implied_controller_path = path
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
@@ -0,0 +1,116 @@
|
|
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_values(@actions, component_instance) unless @actions.empty?
|
57
|
+
attrs[:stimulus_targets] = resolve_values(@targets, component_instance) unless @targets.empty?
|
58
|
+
attrs[:stimulus_values] = resolve_hash_values(@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_values(@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
|
+
def resolve_values(array, component_instance)
|
99
|
+
array.map do |value|
|
100
|
+
if callable?(value)
|
101
|
+
component_instance.instance_exec(&value)
|
102
|
+
else
|
103
|
+
value
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def resolve_hash_values(hash, component_instance)
|
109
|
+
hash.transform_values { |value| callable?(value) ? component_instance.instance_exec(&value) : value }
|
110
|
+
end
|
111
|
+
|
112
|
+
def callable?(value)
|
113
|
+
value.respond_to?(:call)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
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
|