vident 0.13.1 → 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 +24 -0
- data/README.md +521 -665
- 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 +46 -5
- data/lib/vident/attributes/not_typed.rb +0 -81
- data/lib/vident/base.rb +0 -247
- data/lib/vident/root_component.rb +0 -309
@@ -1,81 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Vident
|
4
|
-
module Attributes
|
5
|
-
module NotTyped
|
6
|
-
extend ActiveSupport::Concern
|
7
|
-
|
8
|
-
def attributes
|
9
|
-
@__attributes ||= {}
|
10
|
-
end
|
11
|
-
|
12
|
-
def prepare_attributes(attributes)
|
13
|
-
@__attributes ||= {}
|
14
|
-
attribute_names.each do |attr_name|
|
15
|
-
options = self.class.attribute_options
|
16
|
-
default = options&.dig(attr_name, :default)
|
17
|
-
allow_nil = options[attr_name] ? options[attr_name].fetch(:allow_nil, true) : true
|
18
|
-
|
19
|
-
if attributes&.include? attr_name
|
20
|
-
value = attributes[attr_name]
|
21
|
-
@__attributes[attr_name] = (value.nil? && default) ? default : value
|
22
|
-
else
|
23
|
-
@__attributes[attr_name] = default
|
24
|
-
end
|
25
|
-
raise ArgumentError, "Attribute #{attr_name} cannot be nil" if @__attributes[attr_name].nil? && !allow_nil
|
26
|
-
instance_variable_set(self.class.attribute_ivar_names[attr_name], @__attributes[attr_name])
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def attribute_names
|
31
|
-
self.class.attribute_names
|
32
|
-
end
|
33
|
-
|
34
|
-
def attribute(key)
|
35
|
-
attributes[key]
|
36
|
-
end
|
37
|
-
|
38
|
-
def to_hash
|
39
|
-
attributes.dup
|
40
|
-
end
|
41
|
-
|
42
|
-
class_methods do
|
43
|
-
def inherited(subclass)
|
44
|
-
subclass.instance_variable_set(:@attribute_ivar_names, @attribute_ivar_names.clone)
|
45
|
-
subclass.instance_variable_set(:@attribute_names, @attribute_names.clone)
|
46
|
-
subclass.instance_variable_set(:@attribute_options, @attribute_options.clone)
|
47
|
-
super
|
48
|
-
end
|
49
|
-
|
50
|
-
attr_reader :attribute_ivar_names, :attribute_names, :attribute_options
|
51
|
-
|
52
|
-
def attribute(name, **options)
|
53
|
-
@attribute_names ||= []
|
54
|
-
@attribute_names << name
|
55
|
-
@attribute_ivar_names ||= {}
|
56
|
-
@attribute_ivar_names[name] = :"@#{name}"
|
57
|
-
@attribute_options ||= {}
|
58
|
-
@attribute_options[name] = options
|
59
|
-
define_attribute_delegate(name) if delegates?(options)
|
60
|
-
end
|
61
|
-
|
62
|
-
def delegates?(options)
|
63
|
-
options[:delegates] != false
|
64
|
-
end
|
65
|
-
|
66
|
-
def define_attribute_delegate(attr_name)
|
67
|
-
# Define reader & presence check method
|
68
|
-
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
69
|
-
def #{attr_name}
|
70
|
-
@#{attr_name}
|
71
|
-
end
|
72
|
-
|
73
|
-
def #{attr_name}?
|
74
|
-
send(:#{attr_name}).present?
|
75
|
-
end
|
76
|
-
RUBY
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
data/lib/vident/base.rb
DELETED
@@ -1,247 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Vident
|
4
|
-
module Base
|
5
|
-
extend ActiveSupport::Concern
|
6
|
-
|
7
|
-
class_methods do
|
8
|
-
def no_stimulus_controller
|
9
|
-
@no_stimulus_controller = true
|
10
|
-
end
|
11
|
-
|
12
|
-
def stimulus_controller?
|
13
|
-
!@no_stimulus_controller
|
14
|
-
end
|
15
|
-
|
16
|
-
# The "name" of the component from its class name and namespace. This is used to generate a HTML class name
|
17
|
-
# that can helps identify the component type in the DOM or for styling purposes.
|
18
|
-
def component_name
|
19
|
-
@component_name ||= stimulus_identifier
|
20
|
-
end
|
21
|
-
|
22
|
-
def slots?
|
23
|
-
registered_slots.present?
|
24
|
-
end
|
25
|
-
|
26
|
-
# Dont check collection params, we use kwargs
|
27
|
-
def validate_collection_parameter!(validate_default: false)
|
28
|
-
end
|
29
|
-
|
30
|
-
# stimulus controller identifier
|
31
|
-
def stimulus_identifier
|
32
|
-
::Vident::Base.stimulus_identifier_from_path(identifier_name_path)
|
33
|
-
end
|
34
|
-
|
35
|
-
def identifier_name_path
|
36
|
-
name.underscore
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
# Define reader & presence check method, for performance use ivar directly
|
42
|
-
def define_attribute_delegate(attr_name)
|
43
|
-
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
44
|
-
def #{attr_name}
|
45
|
-
#{@attribute_ivar_names[attr_name]}
|
46
|
-
end
|
47
|
-
|
48
|
-
def #{attr_name}?
|
49
|
-
#{@attribute_ivar_names[attr_name]}.present?
|
50
|
-
end
|
51
|
-
RUBY
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def prepare_attributes(attributes)
|
56
|
-
raise NotImplementedError
|
57
|
-
end
|
58
|
-
|
59
|
-
# Override this method to perform any initialisation before attributes are set
|
60
|
-
def before_initialize(_attrs)
|
61
|
-
end
|
62
|
-
|
63
|
-
# Override this method to perform any initialisation after attributes are set
|
64
|
-
def after_initialize
|
65
|
-
end
|
66
|
-
|
67
|
-
def clone(overrides = {})
|
68
|
-
new_set = to_hash.merge(**overrides)
|
69
|
-
self.class.new(**new_set)
|
70
|
-
end
|
71
|
-
|
72
|
-
def inspect(klass_name = "Component")
|
73
|
-
attr_text = attributes.map { |k, v| "#{k}=#{v.inspect}" }.join(", ")
|
74
|
-
"#<#{self.class.name}<Vident::#{klass_name}> #{attr_text}>"
|
75
|
-
end
|
76
|
-
|
77
|
-
# Generate a unique ID for a component, can be overridden as required. Makes it easier to setup things like ARIA
|
78
|
-
# attributes which require elements to reference by ID. Note this overrides the `id` accessor
|
79
|
-
def id
|
80
|
-
@id.presence || random_id
|
81
|
-
end
|
82
|
-
|
83
|
-
# If connecting an outlet to this specific component instance, use this ID
|
84
|
-
def outlet_id
|
85
|
-
@outlet_id ||= [stimulus_identifier, "##{id}"]
|
86
|
-
end
|
87
|
-
|
88
|
-
# Methods to use in component views
|
89
|
-
# ---------------------------------
|
90
|
-
|
91
|
-
delegate :params, to: :helpers
|
92
|
-
|
93
|
-
# HTML and attribute definition and creation
|
94
|
-
|
95
|
-
# Generate action/target/etc Stimulus attribute string that can be used externally to this component
|
96
|
-
delegate :action, :target, :named_classes, to: :root
|
97
|
-
|
98
|
-
# This can be overridden to return an array of extra class names
|
99
|
-
def element_classes
|
100
|
-
end
|
101
|
-
|
102
|
-
# A HTML class name that can helps identify the component type in the DOM or for styling purposes.
|
103
|
-
def component_class_name
|
104
|
-
self.class.component_name
|
105
|
-
end
|
106
|
-
alias_method :js_event_name_prefix, :component_class_name
|
107
|
-
|
108
|
-
# Generates the full list of HTML classes for the component
|
109
|
-
def render_classes(erb_defined_classes = nil)
|
110
|
-
# TODO: avoid pointless creation of arrays
|
111
|
-
base_classes = [component_class_name] + Array.wrap(element_classes)
|
112
|
-
base_classes += Array.wrap(erb_defined_classes) if erb_defined_classes
|
113
|
-
classes_on_component = attribute(:html_options)&.fetch(:class, nil)
|
114
|
-
base_classes += Array.wrap(classes_on_component) if classes_on_component
|
115
|
-
produce_style_classes(base_classes)
|
116
|
-
end
|
117
|
-
|
118
|
-
def stimulus_identifier
|
119
|
-
self.class.stimulus_identifier
|
120
|
-
end
|
121
|
-
|
122
|
-
# The `component` class name is used to create the controller name.
|
123
|
-
# The path of the Stimulus controller when none is explicitly set
|
124
|
-
def default_controller_path
|
125
|
-
self.class.identifier_name_path
|
126
|
-
end
|
127
|
-
|
128
|
-
def stimulus_identifier_from_path(path)
|
129
|
-
path.split("/").map { |p| p.to_s.dasherize }.join("--")
|
130
|
-
end
|
131
|
-
module_function :stimulus_identifier_from_path
|
132
|
-
|
133
|
-
private
|
134
|
-
|
135
|
-
def root_element_attributes
|
136
|
-
{}
|
137
|
-
end
|
138
|
-
|
139
|
-
def merge_element_attributes!(element_attributes, attributes)
|
140
|
-
attributes.each_with_object(element_attributes) do |(k, v), h|
|
141
|
-
if h[k].is_a?(Hash)
|
142
|
-
h[k].merge!(v)
|
143
|
-
elsif h[k].is_a?(Array)
|
144
|
-
h[k] << v
|
145
|
-
else
|
146
|
-
h[k] = v
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
def root_component_attributes(**attributes)
|
152
|
-
@root_component_attributes ||= {}
|
153
|
-
merge_element_attributes!(@root_component_attributes, attributes)
|
154
|
-
end
|
155
|
-
|
156
|
-
def stimulus_options_for_root_component
|
157
|
-
attributes = root_element_attributes
|
158
|
-
merge_element_attributes!(attributes, @root_component_attributes) if @root_component_attributes.present?
|
159
|
-
stimulus_options_for_component(attributes)
|
160
|
-
end
|
161
|
-
|
162
|
-
# Prepare the stimulus attributes for a StimulusComponent
|
163
|
-
def stimulus_options_for_component(options)
|
164
|
-
# Add pending actions
|
165
|
-
all_actions = attribute(:actions) + Array.wrap(options[:actions])
|
166
|
-
all_actions += @pending_actions if @pending_actions&.any?
|
167
|
-
|
168
|
-
# Add pending targets
|
169
|
-
all_targets = attribute(:targets) + Array.wrap(options[:targets])
|
170
|
-
all_targets += @pending_targets if @pending_targets&.any?
|
171
|
-
|
172
|
-
# Merge pending named classes
|
173
|
-
named_classes_option = merge_stimulus_option(options, :named_classes)
|
174
|
-
if @pending_named_classes&.any?
|
175
|
-
named_classes_option = named_classes_option.merge(@pending_named_classes)
|
176
|
-
end
|
177
|
-
|
178
|
-
{
|
179
|
-
id: respond_to?(:id) ? id : (attribute(:id) || options[:id]),
|
180
|
-
element_tag: attribute(:element_tag) || options[:element_tag] || :div,
|
181
|
-
html_options: prepare_html_options(options[:html_options]),
|
182
|
-
controllers: (
|
183
|
-
self.class.stimulus_controller? ? [default_controller_path] : []
|
184
|
-
) + Array.wrap(options[:controllers]) + attribute(:controllers),
|
185
|
-
actions: all_actions,
|
186
|
-
targets: all_targets,
|
187
|
-
outlets: attribute(:outlets) + Array.wrap(options[:outlets]),
|
188
|
-
outlet_host: attribute(:outlet_host),
|
189
|
-
named_classes: named_classes_option,
|
190
|
-
values: prepare_stimulus_option(options, :values)
|
191
|
-
}
|
192
|
-
end
|
193
|
-
|
194
|
-
def prepare_html_options(erb_options)
|
195
|
-
# Options should override in this order:
|
196
|
-
# - defined on component class methods (lowest priority)
|
197
|
-
# - defined by passing to component erb
|
198
|
-
# - defined by passing to component constructor (highest priority)
|
199
|
-
options = erb_options&.except(:class) || {}
|
200
|
-
classes_from_view = Array.wrap(erb_options[:class]) if erb_options&.key?(:class)
|
201
|
-
options[:class] = render_classes(classes_from_view)
|
202
|
-
options.merge!(attribute(:html_options).except(:class)) if attribute(:html_options)
|
203
|
-
options
|
204
|
-
end
|
205
|
-
|
206
|
-
# TODO: deprecate the ability to set via method on class (responds_to?) and just use component attributes
|
207
|
-
# or attributes passed to parent_element
|
208
|
-
def prepare_stimulus_option(options, name)
|
209
|
-
resolved = respond_to?(name) ? Array.wrap(send(name)) : []
|
210
|
-
resolved.concat(Array.wrap(attribute(name)))
|
211
|
-
resolved.concat(Array.wrap(options[name]))
|
212
|
-
resolved
|
213
|
-
end
|
214
|
-
|
215
|
-
def merge_stimulus_option(options, name)
|
216
|
-
(attribute(name) || {}).merge(options[name] || {})
|
217
|
-
end
|
218
|
-
|
219
|
-
def produce_style_classes(class_names)
|
220
|
-
dedupe_view_component_classes(class_names)
|
221
|
-
end
|
222
|
-
|
223
|
-
def template_path
|
224
|
-
self.class.template_path
|
225
|
-
end
|
226
|
-
|
227
|
-
def random_id
|
228
|
-
@random_id ||= "#{component_class_name}-#{StableId.next_id_in_sequence}"
|
229
|
-
end
|
230
|
-
|
231
|
-
CLASSNAME_SEPARATOR = " "
|
232
|
-
|
233
|
-
# Join all the various class definisions possible and dedupe
|
234
|
-
def dedupe_view_component_classes(html_classes)
|
235
|
-
html_classes.reject!(&:blank?)
|
236
|
-
|
237
|
-
# Join, then dedupe.
|
238
|
-
# This ensures that entries from the classes array such as "a b", "a", "b" are correctly deduped.
|
239
|
-
# Note we are trying to do this with less allocations to avoid GC churn
|
240
|
-
# classes = classes.join(" ").split(" ").uniq
|
241
|
-
html_classes.map! { |x| x.include?(CLASSNAME_SEPARATOR) ? x.split(CLASSNAME_SEPARATOR) : x }
|
242
|
-
.flatten!
|
243
|
-
html_classes.uniq!
|
244
|
-
html_classes.present? ? html_classes.join(CLASSNAME_SEPARATOR) : nil
|
245
|
-
end
|
246
|
-
end
|
247
|
-
end
|
@@ -1,309 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Vident
|
4
|
-
module RootComponent
|
5
|
-
def initialize(
|
6
|
-
controllers: nil,
|
7
|
-
actions: nil,
|
8
|
-
targets: nil,
|
9
|
-
outlets: nil,
|
10
|
-
outlet_host: nil,
|
11
|
-
named_classes: nil, # https://stimulus.hotwired.dev/reference/css-classes
|
12
|
-
values: nil,
|
13
|
-
element_tag: nil,
|
14
|
-
id: nil,
|
15
|
-
html_options: nil
|
16
|
-
)
|
17
|
-
@element_tag = element_tag
|
18
|
-
@html_options = html_options
|
19
|
-
@id = id
|
20
|
-
@controllers = Array.wrap(controllers)
|
21
|
-
@actions = actions
|
22
|
-
@targets = targets
|
23
|
-
@outlets = outlets
|
24
|
-
@named_classes = named_classes
|
25
|
-
@values = values
|
26
|
-
|
27
|
-
outlet_host.connect_outlet(self) if outlet_host.respond_to?(:connect_outlet)
|
28
|
-
end
|
29
|
-
|
30
|
-
def connect_outlet(outlet)
|
31
|
-
@outlets ||= []
|
32
|
-
@outlets << outlet
|
33
|
-
end
|
34
|
-
|
35
|
-
# The view component's helpers for setting stimulus data-* attributes on this component.
|
36
|
-
|
37
|
-
# TODO: rename
|
38
|
-
# Create a Stimulus action string, and returns it
|
39
|
-
# examples:
|
40
|
-
# action(:my_thing) => "current_controller#myThing"
|
41
|
-
# action(:click, :my_thing) => "click->current_controller#myThing"
|
42
|
-
# action("click->current_controller#myThing") => "click->current_controller#myThing"
|
43
|
-
# action("path/to/current", :my_thing) => "path--to--current_controller#myThing"
|
44
|
-
# action(:click, "path/to/current", :my_thing) => "click->path--to--current_controller#myThing"
|
45
|
-
def action(*args)
|
46
|
-
part1, part2, part3 = args
|
47
|
-
(args.size == 1) ? parse_action_arg(part1) : parse_multiple_action_args(part1, part2, part3)
|
48
|
-
end
|
49
|
-
|
50
|
-
def action_data_attribute(*actions)
|
51
|
-
{action: parse_actions(actions).join(" ")}
|
52
|
-
end
|
53
|
-
|
54
|
-
# TODO: rename & make stimulus Target class instance and returns it, which can convert to String
|
55
|
-
# Create a Stimulus Target and returns it
|
56
|
-
# examples:
|
57
|
-
# target(:my_target) => {controller: 'current_controller' name: 'myTarget'}
|
58
|
-
# target("path/to/current", :my_target) => {controller: 'path--to--current_controller', name: 'myTarget'}
|
59
|
-
def target(name, part2 = nil)
|
60
|
-
if part2.nil?
|
61
|
-
{controller: implied_controller_name, name: js_name(name)}
|
62
|
-
else
|
63
|
-
{controller: stimulize_path(name), name: js_name(part2)}
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def target_data_attribute(name)
|
68
|
-
build_target_data_attributes([target(name)])
|
69
|
-
end
|
70
|
-
|
71
|
-
def build_outlet_selector(outlet_selector)
|
72
|
-
prefix = @id ? "##{@id} " : ""
|
73
|
-
"#{prefix}[data-controller~=#{outlet_selector}]"
|
74
|
-
end
|
75
|
-
|
76
|
-
def outlet(css_selector: nil)
|
77
|
-
controller = implied_controller_name
|
78
|
-
if css_selector.nil?
|
79
|
-
[controller, build_outlet_selector(controller)]
|
80
|
-
else
|
81
|
-
[controller, css_selector]
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
# Getter for a named classes list so can be used in view to set initial state on SSR
|
86
|
-
# Returns a String of classes that can be used in a `class` attribute.
|
87
|
-
def named_classes(*names)
|
88
|
-
names.map { |name| convert_classes_list_to_string(@named_classes[name]) }.join(" ")
|
89
|
-
end
|
90
|
-
|
91
|
-
# Helpers for generating the Stimulus data-* attributes directly
|
92
|
-
|
93
|
-
# Return the HTML `data-controller` attribute for the given controllers
|
94
|
-
def with_controllers(*controllers_to_set)
|
95
|
-
"data-controller=\"#{controller_list(controllers_to_set)}\"".html_safe
|
96
|
-
end
|
97
|
-
|
98
|
-
# Return the HTML `data-target` attribute for the given targets
|
99
|
-
def as_targets(*targets)
|
100
|
-
attrs = build_target_data_attributes(parse_targets(targets))
|
101
|
-
attrs.map { |dt, n| "data-#{dt}=\"#{n}\"" }.join(" ").html_safe
|
102
|
-
end
|
103
|
-
alias_method :as_target, :as_targets
|
104
|
-
|
105
|
-
# Return the HTML `data-action` attribute for the given actions
|
106
|
-
def with_actions(*actions_to_set)
|
107
|
-
"data-action='#{parse_actions(actions_to_set).join(" ")}'".html_safe
|
108
|
-
end
|
109
|
-
alias_method :with_action, :with_actions
|
110
|
-
|
111
|
-
# Return the HTML `data-` attribute for the given outlets
|
112
|
-
def with_outlets(*outlets)
|
113
|
-
attrs = build_outlet_data_attributes(outlets)
|
114
|
-
attrs.map { |dt, n| "data-#{dt}=\"#{n}\"" }.join(" ").html_safe
|
115
|
-
end
|
116
|
-
alias_method :with_outlet, :with_outlets
|
117
|
-
|
118
|
-
private
|
119
|
-
|
120
|
-
# An implicit Stimulus controller name is built from the implicit controller path
|
121
|
-
def implied_controller_name
|
122
|
-
stimulize_path(implied_controller_path)
|
123
|
-
end
|
124
|
-
|
125
|
-
# When using the DSL if you dont specify, the first controller is implied
|
126
|
-
def implied_controller_path
|
127
|
-
@controllers&.first || raise(StandardError, "No controllers have been specified")
|
128
|
-
end
|
129
|
-
|
130
|
-
# A complete list of Stimulus controllers for this component
|
131
|
-
def controller_list(controllers_to_set)
|
132
|
-
controllers_to_set&.map { |c| stimulize_path(c) }&.join(" ")
|
133
|
-
end
|
134
|
-
|
135
|
-
# Complete list of actions ready to be use in the data-action attribute
|
136
|
-
def action_list(actions_to_parse)
|
137
|
-
return nil unless actions_to_parse&.size&.positive?
|
138
|
-
parse_actions(actions_to_parse).join(" ")
|
139
|
-
end
|
140
|
-
|
141
|
-
# Complete list of targets ready to be use in the data attributes
|
142
|
-
def target_list
|
143
|
-
return {} unless @targets&.size&.positive?
|
144
|
-
build_target_data_attributes(parse_targets(@targets))
|
145
|
-
end
|
146
|
-
|
147
|
-
def named_classes_list
|
148
|
-
return {} unless @named_classes&.size&.positive?
|
149
|
-
build_named_classes_data_attributes(@named_classes)
|
150
|
-
end
|
151
|
-
|
152
|
-
def values_list
|
153
|
-
return {} unless @values&.size&.positive?
|
154
|
-
build_values_attributes
|
155
|
-
end
|
156
|
-
|
157
|
-
# stimulus "data-*" attributes map for this component
|
158
|
-
def tag_data_attributes
|
159
|
-
{controller: controller_list(@controllers), action: action_list(@actions)}
|
160
|
-
.merge!(target_list)
|
161
|
-
.merge!(outlet_list)
|
162
|
-
.merge!(named_classes_list)
|
163
|
-
.merge!(values_list)
|
164
|
-
.compact_blank!
|
165
|
-
end
|
166
|
-
|
167
|
-
def outlet_list
|
168
|
-
return {} unless @outlets&.size&.positive?
|
169
|
-
build_outlet_data_attributes(@outlets)
|
170
|
-
end
|
171
|
-
|
172
|
-
def parse_outlet(outlet_config)
|
173
|
-
if outlet_config.is_a?(String)
|
174
|
-
[outlet_config, build_outlet_selector(outlet_config)]
|
175
|
-
elsif outlet_config.is_a?(Symbol)
|
176
|
-
outlet_config = outlet_config.to_s.tr("_", "-")
|
177
|
-
[outlet_config, build_outlet_selector(outlet_config)]
|
178
|
-
elsif outlet_config.is_a?(Array)
|
179
|
-
outlet_config[..1]
|
180
|
-
elsif outlet_config.respond_to?(:stimulus_identifier) # Is a Component
|
181
|
-
[outlet_config.stimulus_identifier, build_outlet_selector(outlet_config.stimulus_identifier)]
|
182
|
-
elsif outlet_config.send(:implied_controller_name) # Is a RootComponent ?
|
183
|
-
[outlet_config.send(:implied_controller_name), build_outlet_selector(outlet_config.send(:implied_controller_name))]
|
184
|
-
else
|
185
|
-
raise ArgumentError, "Invalid outlet config: #{outlet_config}"
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
def build_outlet_data_attributes(outlets)
|
190
|
-
outlets.each_with_object({}) do |outlet_config, obj|
|
191
|
-
identifier, css_selector = parse_outlet(outlet_config)
|
192
|
-
obj[:"#{implied_controller_name}-#{identifier}-outlet"] = css_selector
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
# Actions can be specified as a symbol, in which case they imply an action on the primary
|
197
|
-
# controller, or as a string in which case it implies an action that is already fully qualified
|
198
|
-
# stimulus action.
|
199
|
-
# 1 Symbol: :my_action => "my_controller#myAction"
|
200
|
-
# 1 String: "my_controller#myAction"
|
201
|
-
# 2 Symbols: [:click, :my_action] => "click->my_controller#myAction"
|
202
|
-
# 1 String, 1 Symbol: ["path/to/controller", :my_action] => "path--to--controller#myAction"
|
203
|
-
# 1 Symbol, 1 String, 1 Symbol: [:hover, "path/to/controller", :my_action] => "hover->path--to--controller#myAction"
|
204
|
-
|
205
|
-
def parse_action_arg(part1)
|
206
|
-
if part1.is_a?(Symbol)
|
207
|
-
# 1 symbol arg, name of method on this controller
|
208
|
-
"#{implied_controller_name}##{js_name(part1)}"
|
209
|
-
elsif part1.is_a?(String)
|
210
|
-
# 1 string arg, fully qualified action
|
211
|
-
part1
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
def parse_multiple_action_args(part1, part2, part3)
|
216
|
-
if part3.nil? && part1.is_a?(Symbol)
|
217
|
-
# 2 symbol args = event + action
|
218
|
-
"#{part1}->#{implied_controller_name}##{js_name(part2)}"
|
219
|
-
elsif part3.nil?
|
220
|
-
# 1 string arg, 1 symbol = controller + action
|
221
|
-
"#{stimulize_path(part1)}##{js_name(part2)}"
|
222
|
-
else
|
223
|
-
# 1 symbol, 1 string, 1 symbol = as above but with event
|
224
|
-
"#{part1}->#{stimulize_path(part2)}##{js_name(part3)}"
|
225
|
-
end
|
226
|
-
end
|
227
|
-
|
228
|
-
# Parse actions, targets and attributes that are passed in as symbols or strings
|
229
|
-
|
230
|
-
def parse_targets(targets)
|
231
|
-
targets.map { |n| parse_target(n) }
|
232
|
-
end
|
233
|
-
|
234
|
-
def parse_target(raw_target)
|
235
|
-
return raw_target if raw_target.is_a?(String)
|
236
|
-
if raw_target.is_a?(Hash)
|
237
|
-
raw_target[:name] = js_name(raw_target[:name]) if raw_target[:name].is_a?(Symbol)
|
238
|
-
return raw_target
|
239
|
-
end
|
240
|
-
return target(*raw_target) if raw_target.is_a?(Array)
|
241
|
-
target(raw_target)
|
242
|
-
end
|
243
|
-
|
244
|
-
def build_target_data_attributes(targets)
|
245
|
-
targets.map { |t| [:"#{t[:controller]}-target", t[:name]] }.to_h
|
246
|
-
end
|
247
|
-
|
248
|
-
def parse_actions(actions)
|
249
|
-
actions.map! { |a| a.is_a?(String) ? a : action(*a) }
|
250
|
-
end
|
251
|
-
|
252
|
-
def parse_value_attributes(attrs, controller: nil)
|
253
|
-
attrs.transform_keys do |value_name|
|
254
|
-
:"#{controller || implied_controller_name}-#{value_name.to_s.dasherize}-value"
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
def build_values_attributes
|
259
|
-
@values.each_with_object({}) do |m, obj|
|
260
|
-
if m.is_a?(Hash)
|
261
|
-
obj.merge!(parse_value_attributes(m))
|
262
|
-
elsif m.is_a?(Array)
|
263
|
-
controller_path = m.first
|
264
|
-
data = m.last
|
265
|
-
obj.merge!(parse_value_attributes(data, controller: stimulize_path(controller_path)))
|
266
|
-
end
|
267
|
-
end
|
268
|
-
end
|
269
|
-
|
270
|
-
def build_values_data_attributes(values)
|
271
|
-
values.map { |name, value| [:"#{name}-value", value] }.to_h
|
272
|
-
end
|
273
|
-
|
274
|
-
def parse_named_classes_hash(named_classes)
|
275
|
-
named_classes.map do |name, classes|
|
276
|
-
logical_name = name.to_s.dasherize
|
277
|
-
classes_str = convert_classes_list_to_string(classes)
|
278
|
-
if classes.is_a?(Hash)
|
279
|
-
{controller: stimulize_path(classes[:controller_path]), name: logical_name, classes: classes_str}
|
280
|
-
else
|
281
|
-
{controller: implied_controller_name, name: logical_name, classes: classes_str}
|
282
|
-
end
|
283
|
-
end
|
284
|
-
end
|
285
|
-
|
286
|
-
def build_named_classes_data_attributes(named_classes)
|
287
|
-
parse_named_classes_hash(named_classes)
|
288
|
-
.map { |c| [:"#{c[:controller]}-#{c[:name]}-class", c[:classes]] }
|
289
|
-
.to_h
|
290
|
-
end
|
291
|
-
|
292
|
-
def convert_classes_list_to_string(classes)
|
293
|
-
return "" if classes.nil?
|
294
|
-
return classes if classes.is_a?(String)
|
295
|
-
return classes.join(" ") if classes.is_a?(Array)
|
296
|
-
classes[:classes].is_a?(Array) ? classes[:classes].join(" ") : classes[:classes]
|
297
|
-
end
|
298
|
-
|
299
|
-
# Convert a file path to a stimulus controller name
|
300
|
-
def stimulize_path(path)
|
301
|
-
path.split("/").map { |p| p.to_s.dasherize }.join("--")
|
302
|
-
end
|
303
|
-
|
304
|
-
# Convert a Ruby 'snake case' string to a JavaScript camel case strings
|
305
|
-
def js_name(name)
|
306
|
-
name.to_s.camelize(:lower)
|
307
|
-
end
|
308
|
-
end
|
309
|
-
end
|