vident 0.14.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.
@@ -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,322 +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
- def controller_attribute(*controllers_to_set)
38
- {"data-controller" => controller_list(controllers_to_set)&.html_safe}
39
- end
40
-
41
- def target_attributes(*targets)
42
- attrs = build_target_data_attributes(parse_targets(targets))
43
- attrs.transform_keys { |dt| "data-#{dt}" }
44
- end
45
-
46
- def action_attribute(*actions_to_set)
47
- {"data-action" => parse_actions(actions_to_set).join(" ").html_safe}
48
- end
49
-
50
- # TODO: rename
51
- # Create a Stimulus action string, and returns it
52
- # examples:
53
- # action(:my_thing) => "current_controller#myThing"
54
- # action(:click, :my_thing) => "click->current_controller#myThing"
55
- # action("click->current_controller#myThing") => "click->current_controller#myThing"
56
- # action("path/to/current", :my_thing) => "path--to--current_controller#myThing"
57
- # action(:click, "path/to/current", :my_thing) => "click->path--to--current_controller#myThing"
58
- def action(*args)
59
- part1, part2, part3 = args
60
- (args.size == 1) ? parse_action_arg(part1) : parse_multiple_action_args(part1, part2, part3)
61
- end
62
-
63
- def action_data_attribute(*actions)
64
- {action: parse_actions(actions).join(" ")}
65
- end
66
-
67
- # TODO: rename & make stimulus Target class instance and returns it, which can convert to String
68
- # Create a Stimulus Target and returns it
69
- # examples:
70
- # target(:my_target) => {controller: 'current_controller' name: 'myTarget'}
71
- # target("path/to/current", :my_target) => {controller: 'path--to--current_controller', name: 'myTarget'}
72
- def target(name, part2 = nil)
73
- if part2.nil?
74
- {controller: implied_controller_name, name: js_name(name)}
75
- else
76
- {controller: stimulize_path(name), name: js_name(part2)}
77
- end
78
- end
79
-
80
- def target_data_attribute(name)
81
- build_target_data_attributes([target(name)])
82
- end
83
-
84
- def build_outlet_selector(outlet_selector)
85
- prefix = @id ? "##{@id} " : ""
86
- "#{prefix}[data-controller~=#{outlet_selector}]"
87
- end
88
-
89
- def outlet(css_selector: nil)
90
- controller = implied_controller_name
91
- if css_selector.nil?
92
- [controller, build_outlet_selector(controller)]
93
- else
94
- [controller, css_selector]
95
- end
96
- end
97
-
98
- # Getter for a named classes list so can be used in view to set initial state on SSR
99
- # Returns a String of classes that can be used in a `class` attribute.
100
- def named_classes(*names)
101
- names.map { |name| convert_classes_list_to_string(@named_classes[name]) }.join(" ")
102
- end
103
-
104
- # Helpers for generating the Stimulus data-* attributes directly
105
-
106
- # Return the HTML `data-controller` attribute for the given controllers
107
- def with_controllers(*controllers_to_set)
108
- "data-controller=\"#{controller_list(controllers_to_set)}\"".html_safe
109
- end
110
-
111
- # Return the HTML `data-target` attribute for the given targets
112
- def as_targets(*targets)
113
- attrs = build_target_data_attributes(parse_targets(targets))
114
- attrs.map { |dt, n| "data-#{dt}=\"#{n}\"" }.join(" ").html_safe
115
- end
116
- alias_method :as_target, :as_targets
117
-
118
- # Return the HTML `data-action` attribute for the given actions
119
- def with_actions(*actions_to_set)
120
- "data-action='#{parse_actions(actions_to_set).join(" ")}'".html_safe
121
- end
122
- alias_method :with_action, :with_actions
123
-
124
- # Return the HTML `data-` attribute for the given outlets
125
- def with_outlets(*outlets)
126
- attrs = build_outlet_data_attributes(outlets)
127
- attrs.map { |dt, n| "data-#{dt}=\"#{n}\"" }.join(" ").html_safe
128
- end
129
- alias_method :with_outlet, :with_outlets
130
-
131
- private
132
-
133
- # An implicit Stimulus controller name is built from the implicit controller path
134
- def implied_controller_name
135
- stimulize_path(implied_controller_path)
136
- end
137
-
138
- # When using the DSL if you dont specify, the first controller is implied
139
- def implied_controller_path
140
- @controllers&.first || raise(StandardError, "No controllers have been specified")
141
- end
142
-
143
- # A complete list of Stimulus controllers for this component
144
- def controller_list(controllers_to_set)
145
- controllers_to_set&.map { |c| stimulize_path(c) }&.join(" ")
146
- end
147
-
148
- # Complete list of actions ready to be use in the data-action attribute
149
- def action_list(actions_to_parse)
150
- return nil unless actions_to_parse&.size&.positive?
151
- parse_actions(actions_to_parse).join(" ")
152
- end
153
-
154
- # Complete list of targets ready to be use in the data attributes
155
- def target_list
156
- return {} unless @targets&.size&.positive?
157
- build_target_data_attributes(parse_targets(@targets))
158
- end
159
-
160
- def named_classes_list
161
- return {} unless @named_classes&.size&.positive?
162
- build_named_classes_data_attributes(@named_classes)
163
- end
164
-
165
- def values_list
166
- return {} unless @values&.size&.positive?
167
- build_values_attributes
168
- end
169
-
170
- # stimulus "data-*" attributes map for this component
171
- def tag_data_attributes
172
- {controller: controller_list(@controllers), action: action_list(@actions)}
173
- .merge!(target_list)
174
- .merge!(outlet_list)
175
- .merge!(named_classes_list)
176
- .merge!(values_list)
177
- .compact_blank!
178
- end
179
-
180
- def outlet_list
181
- return {} unless @outlets&.size&.positive?
182
- build_outlet_data_attributes(@outlets)
183
- end
184
-
185
- def parse_outlet(outlet_config)
186
- if outlet_config.is_a?(String)
187
- [outlet_config, build_outlet_selector(outlet_config)]
188
- elsif outlet_config.is_a?(Symbol)
189
- outlet_config = outlet_config.to_s.tr("_", "-")
190
- [outlet_config, build_outlet_selector(outlet_config)]
191
- elsif outlet_config.is_a?(Array)
192
- outlet_config[..1]
193
- elsif outlet_config.respond_to?(:stimulus_identifier) # Is a Component
194
- [outlet_config.stimulus_identifier, build_outlet_selector(outlet_config.stimulus_identifier)]
195
- elsif outlet_config.send(:implied_controller_name) # Is a RootComponent ?
196
- [outlet_config.send(:implied_controller_name), build_outlet_selector(outlet_config.send(:implied_controller_name))]
197
- else
198
- raise ArgumentError, "Invalid outlet config: #{outlet_config}"
199
- end
200
- end
201
-
202
- def build_outlet_data_attributes(outlets)
203
- outlets.each_with_object({}) do |outlet_config, obj|
204
- identifier, css_selector = parse_outlet(outlet_config)
205
- obj[:"#{implied_controller_name}-#{identifier}-outlet"] = css_selector
206
- end
207
- end
208
-
209
- # Actions can be specified as a symbol, in which case they imply an action on the primary
210
- # controller, or as a string in which case it implies an action that is already fully qualified
211
- # stimulus action.
212
- # 1 Symbol: :my_action => "my_controller#myAction"
213
- # 1 String: "my_controller#myAction"
214
- # 2 Symbols: [:click, :my_action] => "click->my_controller#myAction"
215
- # 1 String, 1 Symbol: ["path/to/controller", :my_action] => "path--to--controller#myAction"
216
- # 1 Symbol, 1 String, 1 Symbol: [:hover, "path/to/controller", :my_action] => "hover->path--to--controller#myAction"
217
-
218
- def parse_action_arg(part1)
219
- if part1.is_a?(Symbol)
220
- # 1 symbol arg, name of method on this controller
221
- "#{implied_controller_name}##{js_name(part1)}"
222
- elsif part1.is_a?(String)
223
- # 1 string arg, fully qualified action
224
- part1
225
- end
226
- end
227
-
228
- def parse_multiple_action_args(part1, part2, part3)
229
- if part3.nil? && part1.is_a?(Symbol)
230
- # 2 symbol args = event + action
231
- "#{part1}->#{implied_controller_name}##{js_name(part2)}"
232
- elsif part3.nil?
233
- # 1 string arg, 1 symbol = controller + action
234
- "#{stimulize_path(part1)}##{js_name(part2)}"
235
- else
236
- # 1 symbol, 1 string, 1 symbol = as above but with event
237
- "#{part1}->#{stimulize_path(part2)}##{js_name(part3)}"
238
- end
239
- end
240
-
241
- # Parse actions, targets and attributes that are passed in as symbols or strings
242
-
243
- def parse_targets(targets)
244
- targets.map { |n| parse_target(n) }
245
- end
246
-
247
- def parse_target(raw_target)
248
- return raw_target if raw_target.is_a?(String)
249
- if raw_target.is_a?(Hash)
250
- raw_target[:name] = js_name(raw_target[:name]) if raw_target[:name].is_a?(Symbol)
251
- return raw_target
252
- end
253
- return target(*raw_target) if raw_target.is_a?(Array)
254
- target(raw_target)
255
- end
256
-
257
- def build_target_data_attributes(targets)
258
- targets.map { |t| [:"#{t[:controller]}-target", t[:name]] }.to_h
259
- end
260
-
261
- def parse_actions(actions)
262
- actions.map! { |a| a.is_a?(String) ? a : action(*a) }
263
- end
264
-
265
- def parse_value_attributes(attrs, controller: nil)
266
- attrs.transform_keys do |value_name|
267
- :"#{controller || implied_controller_name}-#{value_name.to_s.dasherize}-value"
268
- end
269
- end
270
-
271
- def build_values_attributes
272
- @values.each_with_object({}) do |m, obj|
273
- if m.is_a?(Hash)
274
- obj.merge!(parse_value_attributes(m))
275
- elsif m.is_a?(Array)
276
- controller_path = m.first
277
- data = m.last
278
- obj.merge!(parse_value_attributes(data, controller: stimulize_path(controller_path)))
279
- end
280
- end
281
- end
282
-
283
- def build_values_data_attributes(values)
284
- values.map { |name, value| [:"#{name}-value", value] }.to_h
285
- end
286
-
287
- def parse_named_classes_hash(named_classes)
288
- named_classes.map do |name, classes|
289
- logical_name = name.to_s.dasherize
290
- classes_str = convert_classes_list_to_string(classes)
291
- if classes.is_a?(Hash)
292
- {controller: stimulize_path(classes[:controller_path]), name: logical_name, classes: classes_str}
293
- else
294
- {controller: implied_controller_name, name: logical_name, classes: classes_str}
295
- end
296
- end
297
- end
298
-
299
- def build_named_classes_data_attributes(named_classes)
300
- parse_named_classes_hash(named_classes)
301
- .map { |c| [:"#{c[:controller]}-#{c[:name]}-class", c[:classes]] }
302
- .to_h
303
- end
304
-
305
- def convert_classes_list_to_string(classes)
306
- return "" if classes.nil?
307
- return classes if classes.is_a?(String)
308
- return classes.join(" ") if classes.is_a?(Array)
309
- classes[:classes].is_a?(Array) ? classes[:classes].join(" ") : classes[:classes]
310
- end
311
-
312
- # Convert a file path to a stimulus controller name
313
- def stimulize_path(path)
314
- path.split("/").map { |p| p.to_s.dasherize }.join("--")
315
- end
316
-
317
- # Convert a Ruby 'snake case' string to a JavaScript camel case strings
318
- def js_name(name)
319
- name.to_s.camelize(:lower)
320
- end
321
- end
322
- end