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.
@@ -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