loco_motion-rails 0.0.6
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +880 -0
- data/app/components/daisy/actions/button_component.html.haml +7 -0
- data/app/components/daisy/actions/button_component.rb +52 -0
- data/app/components/daisy/actions/dropdown_component.html.haml +13 -0
- data/app/components/daisy/actions/dropdown_component.rb +48 -0
- data/app/components/daisy/actions/modal_component.html.haml +31 -0
- data/app/components/daisy/actions/modal_component.rb +92 -0
- data/app/components/daisy/actions/swap_component.html.haml +17 -0
- data/app/components/daisy/actions/swap_component.rb +56 -0
- data/app/components/daisy/actions/theme_controller_component.html.haml +5 -0
- data/app/components/daisy/actions/theme_controller_component.rb +8 -0
- data/app/components/daisy/data_display/accordion_component.html.haml +4 -0
- data/app/components/daisy/data_display/accordion_component.rb +82 -0
- data/app/components/daisy/data_display/avatar_component.html.haml +9 -0
- data/app/components/daisy/data_display/avatar_component.rb +60 -0
- data/app/components/daisy/data_display/badge_component.html.haml +2 -0
- data/app/components/daisy/data_display/badge_component.rb +27 -0
- data/app/components/daisy/data_display/card_component.html.haml +17 -0
- data/app/components/daisy/data_display/card_component.rb +53 -0
- data/app/components/daisy/data_display/carousel_component.html.haml +3 -0
- data/app/components/daisy/data_display/carousel_component.rb +17 -0
- data/app/components/daisy/data_display/chat_component.html.haml +14 -0
- data/app/components/daisy/data_display/chat_component.rb +38 -0
- data/app/components/daisy/data_display/collapse_component.html.haml +13 -0
- data/app/components/daisy/data_display/collapse_component.rb +37 -0
- data/app/components/daisy/data_display/countdown_component.html.haml +24 -0
- data/app/components/daisy/data_display/countdown_component.rb +70 -0
- data/app/components/daisy/data_display/countdown_controller.js +78 -0
- data/app/components/daisy/data_display/diff_component.html.haml +6 -0
- data/app/components/daisy/data_display/diff_component.rb +25 -0
- data/app/components/daisy/data_display/kbd_component.html.haml +2 -0
- data/app/components/daisy/data_display/kbd_component.rb +21 -0
- data/app/components/daisy/data_display/stat_component.html.haml +27 -0
- data/app/components/daisy/data_display/stat_component.rb +41 -0
- data/app/components/daisy/data_display/table_component.html.haml +14 -0
- data/app/components/daisy/data_display/table_component.rb +148 -0
- data/app/components/daisy/data_display/timeline_component.html.haml +7 -0
- data/app/components/daisy/data_display/timeline_component.rb +8 -0
- data/app/components/daisy/data_display/timeline_event_component.html.haml +28 -0
- data/app/components/daisy/data_display/timeline_event_component.rb +47 -0
- data/app/components/daisy/feedback/alert_component.html.haml +8 -0
- data/app/components/daisy/feedback/alert_component.rb +19 -0
- data/app/components/daisy/layout/join_component.rb +15 -0
- data/app/components/daisy/navigation/bottom_nav_component.rb +59 -0
- data/app/components/daisy/navigation/breadcrumbs_component.html.haml +7 -0
- data/app/components/daisy/navigation/breadcrumbs_component.rb +15 -0
- data/app/components/daisy/navigation/link_component.html.haml +4 -0
- data/app/components/daisy/navigation/link_component.rb +34 -0
- data/app/components/daisy/navigation/menu_component.html.haml +3 -0
- data/app/components/daisy/navigation/menu_component.rb +49 -0
- data/app/components/daisy/navigation/navbar_component.html.haml +4 -0
- data/app/components/daisy/navigation/navbar_component.rb +12 -0
- data/app/components/daisy/navigation/steps_component.rb +40 -0
- data/app/components/daisy/navigation/tabs_component.html.haml +4 -0
- data/app/components/daisy/navigation/tabs_component.rb +107 -0
- data/app/components/hero/icon_component.rb +18 -0
- data/lib/daisy/helpers.rb +61 -0
- data/lib/daisy.rb +19 -0
- data/lib/loco_motion/base_component.rb +371 -0
- data/lib/loco_motion/basic_component.rb +18 -0
- data/lib/loco_motion/component_config.rb +165 -0
- data/lib/loco_motion/engine.rb +8 -0
- data/lib/loco_motion/errors.rb +33 -0
- data/lib/loco_motion.rb +48 -0
- metadata +408 -0
@@ -0,0 +1,371 @@
|
|
1
|
+
class LocoMotion::BaseComponent < ViewComponent::Base
|
2
|
+
|
3
|
+
SELF_CLOSING_TAGS = %i[area base br col embed hr img input keygen link meta param source track wbr].freeze
|
4
|
+
|
5
|
+
include Heroicons::IconsHelper
|
6
|
+
|
7
|
+
class_attribute :component_name
|
8
|
+
class_attribute :component_parts, default: { component: {} }
|
9
|
+
class_attribute :valid_modifiers, default: []
|
10
|
+
class_attribute :valid_sizes, default: []
|
11
|
+
|
12
|
+
#
|
13
|
+
# Return the current configruation of this component.
|
14
|
+
#
|
15
|
+
# @return LocoMotion::ComponentConfig
|
16
|
+
#
|
17
|
+
attr_reader :config
|
18
|
+
|
19
|
+
#
|
20
|
+
# Allow users to alter the config through the component itself
|
21
|
+
#
|
22
|
+
delegate :set_tag_name, :add_css, :add_html, :add_stimulus_controller, :modifiers,
|
23
|
+
to: :config
|
24
|
+
|
25
|
+
#
|
26
|
+
# Create a new instance of a component.
|
27
|
+
#
|
28
|
+
def initialize(*args, **kws, &block)
|
29
|
+
super
|
30
|
+
|
31
|
+
# Create our config object
|
32
|
+
@config = LocoMotion::ComponentConfig.new(self, **kws, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Override the default slot to render the BasicComponent if no component is
|
37
|
+
# provided.
|
38
|
+
#
|
39
|
+
def self.renders_one(*args)
|
40
|
+
# If they don't pass extra options, default to BasicComponent
|
41
|
+
args&.size == 1 ? super(*args + [LocoMotion::BasicComponent]) : super
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Override the default many slot to render the BasicComponent if no component
|
46
|
+
# is provided.
|
47
|
+
#
|
48
|
+
def self.renders_many(*args)
|
49
|
+
# If they don't pass extra options, default to BasicComponent
|
50
|
+
args&.size == 1 ? super(*args + [LocoMotion::BasicComponent]) : super
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Sets the component name used in CSS generation.
|
55
|
+
#
|
56
|
+
# @param component_name [Symbol,String] The name of the component.
|
57
|
+
#
|
58
|
+
def self.set_component_name(component_name)
|
59
|
+
self.component_name = component_name
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Defines a new part of this component which can customize CSS, HTML and more.
|
64
|
+
#
|
65
|
+
# @param part_name [Symbol] The name of the part.
|
66
|
+
# @param part_defaults [Hash] Any default config options such as `tag_name`.
|
67
|
+
#
|
68
|
+
def self.define_part(part_name, part_defaults = {})
|
69
|
+
# Note that since we're using Rails' class_attribute method for these, we
|
70
|
+
# must take care not to alter the original object but rather use a setter
|
71
|
+
# (the `=` in this case) to set the new value so Rails knows not to override
|
72
|
+
# the parent value.
|
73
|
+
#
|
74
|
+
# For example, we cannot use `merge!` or `[part_name] = ` here.
|
75
|
+
self.component_parts = component_parts.merge({ part_name => part_defaults })
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Convenience method for defining multiple parts at once with no defaults.
|
80
|
+
#
|
81
|
+
# @param part_names [Array<Symbol>] The names of the parts you wish to define.
|
82
|
+
#
|
83
|
+
def self.define_parts(*part_names)
|
84
|
+
(part_names || []).each do |part_name|
|
85
|
+
define_part(part_name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# Defines a single modifier of this component. Modifiers control certain
|
91
|
+
# rendering aspects of the component.
|
92
|
+
#
|
93
|
+
# @param modifier_name [Symbol] The name of the modifier.
|
94
|
+
#
|
95
|
+
def self.define_modifier(modifier_name)
|
96
|
+
define_modifiers(modifier_name)
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Define multiple modifiers for this component. Modifiers control certain
|
101
|
+
# rendering aspects of the component.
|
102
|
+
#
|
103
|
+
# @param modifier_names [Array[Symbol]] An array of the modifier names you wish
|
104
|
+
# to define.
|
105
|
+
#
|
106
|
+
def self.define_modifiers(*modifier_names)
|
107
|
+
# Note that since we're using Rails' class_attribute method for these, we
|
108
|
+
# must take care not to alter the original object but rather use a setter
|
109
|
+
# (the `+=` in this case) to set the new value so Rails knows not to
|
110
|
+
# override the parent value.
|
111
|
+
#
|
112
|
+
# For example, we cannot use `<<` or `concat` here.
|
113
|
+
self.valid_modifiers ||= []
|
114
|
+
self.valid_modifiers += modifier_names
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Define a single size of this component. Sizes control how big or small this
|
119
|
+
# component will render.
|
120
|
+
#
|
121
|
+
# @param size_name [Symbol] The name of the size you wish to define.
|
122
|
+
#
|
123
|
+
def self.define_size(size_name)
|
124
|
+
define_sizes(size_name)
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# Define multiple sizes for this component. Sizes control how big or small
|
129
|
+
# this component will render.
|
130
|
+
#
|
131
|
+
# @param size_names [Array[Symbol]] An array of the sizes you wish to define.
|
132
|
+
#
|
133
|
+
def self.define_sizes(*size_names)
|
134
|
+
# Note that since we're using Rails' class_attribute method for these, we
|
135
|
+
# must take care not to alter the original object but rather use a setter
|
136
|
+
# (the `+=` in this case) to set the new value so Rails knows not to
|
137
|
+
# override the parent value.
|
138
|
+
#
|
139
|
+
# For example, we cannot use `<<` or `concat` here.
|
140
|
+
self.valid_sizes ||= []
|
141
|
+
self.valid_sizes += size_names
|
142
|
+
end
|
143
|
+
|
144
|
+
#
|
145
|
+
# Allows you to bulid a customized version of this component without having to
|
146
|
+
# define a new class.
|
147
|
+
#
|
148
|
+
def self.build(*build_args, **build_kws, &build_block)
|
149
|
+
klass = Class.new(self)
|
150
|
+
|
151
|
+
# Unless already defined, delegate the name method to the superclass so
|
152
|
+
# ViewComponent can find the sidecar partials and render them when no call
|
153
|
+
# method is defined.
|
154
|
+
unless klass.method_defined?(:name)
|
155
|
+
klass.instance_eval do
|
156
|
+
def name
|
157
|
+
superclass.name
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Override the initialize method to combine the build and instance args
|
163
|
+
klass.class_eval do
|
164
|
+
original_initialize = method_defined?(:initialize) ? instance_method(:initialize) : nil
|
165
|
+
|
166
|
+
define_method(:initialize) do |*instance_args, **instance_kws, &instance_block|
|
167
|
+
if original_initialize
|
168
|
+
original_initialize.bind(self).call
|
169
|
+
else
|
170
|
+
super(*instance_args, **instance_kws, &instance_block)
|
171
|
+
end
|
172
|
+
|
173
|
+
@config.smart_merge!(**build_kws)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Finally, execute any block they passed in to allow for customizations
|
178
|
+
klass.class_eval(&build_block) if block_given?
|
179
|
+
|
180
|
+
klass
|
181
|
+
end
|
182
|
+
|
183
|
+
#
|
184
|
+
# Returns a reference to this component. Useful for passing a parent component
|
185
|
+
# into child components.
|
186
|
+
#
|
187
|
+
# @return [BaseComponent] A reference to this component.
|
188
|
+
#
|
189
|
+
def component_ref
|
190
|
+
self
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
#
|
195
|
+
# Sets the parent component of this component. Enables child components to ask
|
196
|
+
# questions of their parent and access parent config.
|
197
|
+
#
|
198
|
+
def set_loco_parent(parent)
|
199
|
+
@loco_parent = parent
|
200
|
+
end
|
201
|
+
attr_reader :loco_parent
|
202
|
+
|
203
|
+
#
|
204
|
+
# Renders the given part.
|
205
|
+
#
|
206
|
+
def part(part_name, &block)
|
207
|
+
|
208
|
+
# Validate the part_name
|
209
|
+
@config.validate_part(part_name)
|
210
|
+
|
211
|
+
# Grab the rendered tag name
|
212
|
+
tag_name = rendered_tag_name(part_name)
|
213
|
+
|
214
|
+
if block_given?
|
215
|
+
content_tag(tag_name, **rendered_html(part_name), &block)
|
216
|
+
else
|
217
|
+
# The `tag()` helper will allow you to pass any tag without a block, but
|
218
|
+
# this isn't valid HTML. In particular, it will render a "self-closing"
|
219
|
+
# <div /> tag which doesn't actually close the div.
|
220
|
+
#
|
221
|
+
# Therefore, we need to pass some kind of block to ensure it closes. We've
|
222
|
+
# choosen a comment to keep the output as clean as possible while still
|
223
|
+
# informing a developer what is happening.
|
224
|
+
if SELF_CLOSING_TAGS.include?(tag_name.to_sym)
|
225
|
+
tag(tag_name, **rendered_html(part_name))
|
226
|
+
else
|
227
|
+
content_tag(tag_name, **rendered_html(part_name)) do
|
228
|
+
"<!-- Empty Part Block //-->".html_safe
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
#
|
235
|
+
# Returns the user-provided or component-default HTML tag-name.
|
236
|
+
#
|
237
|
+
# @param part_name [Symbol] The part whose tag-name you desire.
|
238
|
+
#
|
239
|
+
# @return [Symbol,String] The HTML tag-name for the requested comopnent part.
|
240
|
+
#
|
241
|
+
def rendered_tag_name(part_name)
|
242
|
+
part = @config.get_part(part_name)
|
243
|
+
|
244
|
+
part[:user_tag_name] || part[:default_tag_name]
|
245
|
+
end
|
246
|
+
|
247
|
+
#
|
248
|
+
# Builds a string suitable for the HTML element's `class` attribute for the
|
249
|
+
# requested component part.
|
250
|
+
#
|
251
|
+
# @param part_name [Symbol] The component part whose CSS you desire.
|
252
|
+
#
|
253
|
+
# @return [String] A string of CSS names.
|
254
|
+
#
|
255
|
+
def rendered_css(part_name)
|
256
|
+
default_css = @config.get_part(part_name)[:default_css]
|
257
|
+
user_css = @config.get_part(part_name)[:user_css]
|
258
|
+
|
259
|
+
cssify([default_css, user_css])
|
260
|
+
end
|
261
|
+
|
262
|
+
#
|
263
|
+
# Builds a Hash of all of the HTML attributes for the requested component
|
264
|
+
# part.
|
265
|
+
#
|
266
|
+
# @param part_name [Symbol] The component part whose HTML you desire.
|
267
|
+
#
|
268
|
+
# @return [Hash] A combination of all generated, component default, and
|
269
|
+
# user-specified HTML attributes for the part.
|
270
|
+
#
|
271
|
+
def rendered_html(part_name)
|
272
|
+
default_html = @config.get_part(part_name)[:default_html] || {}
|
273
|
+
user_html = @config.get_part(part_name)[:user_html] || {}
|
274
|
+
|
275
|
+
generated_html = {
|
276
|
+
class: rendered_css(part_name),
|
277
|
+
data: rendered_data(part_name)
|
278
|
+
}.deep_merge(default_html).deep_merge(user_html)
|
279
|
+
end
|
280
|
+
|
281
|
+
#
|
282
|
+
# Builds the HTML `data` attribute.
|
283
|
+
#
|
284
|
+
# @param part_name [Symbol] The component part whose HTML `data` attribute
|
285
|
+
# you desire.
|
286
|
+
#
|
287
|
+
# @return [Hash] A hash of objects to be rendered in the `data` attribute.
|
288
|
+
#
|
289
|
+
def rendered_data(part_name)
|
290
|
+
generated_data = {}
|
291
|
+
|
292
|
+
stimulus_controllers = rendered_stimulus_controllers(part_name)
|
293
|
+
|
294
|
+
generated_data[:controller] = stimulus_controllers if stimulus_controllers.present?
|
295
|
+
|
296
|
+
generated_data
|
297
|
+
end
|
298
|
+
|
299
|
+
#
|
300
|
+
# Builds a list of Stimulus controllers for the HTML `data-controller`
|
301
|
+
# attribute.
|
302
|
+
#
|
303
|
+
# @param part_name [Symbol] The component part whose Stimulus controllers you
|
304
|
+
# desire.
|
305
|
+
#
|
306
|
+
# @ return [String] A space-separated list of Stimulus controllers.
|
307
|
+
#
|
308
|
+
def rendered_stimulus_controllers(part_name)
|
309
|
+
default_controllers = @config.get_part(part_name)[:default_stimulus_controllers]
|
310
|
+
user_controllers = @config.get_part(part_name)[:user_stimulus_controllers]
|
311
|
+
|
312
|
+
strip_spaces([default_controllers, user_controllers].join(" "))
|
313
|
+
end
|
314
|
+
|
315
|
+
#
|
316
|
+
# Convert strings, symbols, and arrays of those into a single CSS-like string.
|
317
|
+
#
|
318
|
+
def cssify(content)
|
319
|
+
css = [content].flatten.compact
|
320
|
+
|
321
|
+
strip_spaces(css.join(" "))
|
322
|
+
end
|
323
|
+
|
324
|
+
#
|
325
|
+
# Strip extra whitespace from a given string.
|
326
|
+
#
|
327
|
+
# @param str [String] The string you wish to strip.
|
328
|
+
#
|
329
|
+
# @return [String] A string with minimal possible whitespace.
|
330
|
+
#
|
331
|
+
def strip_spaces(str)
|
332
|
+
str.gsub(/ +/, " ").strip
|
333
|
+
end
|
334
|
+
|
335
|
+
#
|
336
|
+
# Retrieve the requested component option, or the desired default if no option
|
337
|
+
# was provided.
|
338
|
+
#
|
339
|
+
# @param key [Symbol] The name of the keyword argument option you wish to
|
340
|
+
# retrieve.
|
341
|
+
# @param default [Object] Any value that you wish to use as a default should
|
342
|
+
# the option be undefined. Defaults to `nil`.
|
343
|
+
#
|
344
|
+
def config_option(key, default = nil)
|
345
|
+
value = @config.options[key]
|
346
|
+
|
347
|
+
value.nil? ? default : value
|
348
|
+
end
|
349
|
+
|
350
|
+
# Provide some nice output for debugging or other purposes.
|
351
|
+
#
|
352
|
+
def inspect
|
353
|
+
parts = component_parts.map do |part_name, part_defaults|
|
354
|
+
{
|
355
|
+
part_name: part_name,
|
356
|
+
tag_name: rendered_tag_name(part_name),
|
357
|
+
css: rendered_css(part_name),
|
358
|
+
html: rendered_html(part_name)
|
359
|
+
}
|
360
|
+
end
|
361
|
+
|
362
|
+
[
|
363
|
+
"#<#{self.class.name}",
|
364
|
+
"@component_name=#{(component_name || :unnamed).inspect}",
|
365
|
+
"@valid_modifiers=#{valid_modifiers.inspect}",
|
366
|
+
"@valid_sizes=#{valid_sizes.inspect}",
|
367
|
+
"@config=#{@config.inspect}",
|
368
|
+
"@component_parts=#{parts.inspect}",
|
369
|
+
].join(" ") + ">"
|
370
|
+
end
|
371
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#
|
2
|
+
# The BasicComponent class is used for all slots that don't provide a component
|
3
|
+
# so that users can pass in all of the same CSS and HTML options that a standard
|
4
|
+
# component would have.
|
5
|
+
#
|
6
|
+
class LocoMotion::BasicComponent < LocoMotion.configuration.base_component_class
|
7
|
+
|
8
|
+
def call
|
9
|
+
part(:component) do
|
10
|
+
content
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.name
|
15
|
+
"BasicComponent"
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
class LocoMotion::ComponentConfig
|
2
|
+
attr_reader :component, :parts, :options, :modifiers, :size
|
3
|
+
|
4
|
+
def initialize(component, **kws, &block)
|
5
|
+
@component = component
|
6
|
+
@options = kws
|
7
|
+
|
8
|
+
@parts = {}
|
9
|
+
@modifiers = (kws[:modifiers] || [kws[:modifier]]).compact
|
10
|
+
@size = kws[:size]
|
11
|
+
|
12
|
+
build
|
13
|
+
validate
|
14
|
+
end
|
15
|
+
|
16
|
+
def build
|
17
|
+
# Allow users to pass css/html for a specific part (i.e. modal_dialog)
|
18
|
+
@component.component_parts.each do |part, defaults|
|
19
|
+
@parts[part] = {
|
20
|
+
default_css: [],
|
21
|
+
default_html: {},
|
22
|
+
default_tag_name: defaults[:tag_name] || :div,
|
23
|
+
default_stimulus_controllers: [],
|
24
|
+
|
25
|
+
user_css: @options["#{part}_css".to_sym] || [],
|
26
|
+
user_html: @options["#{part}_html".to_sym] || {},
|
27
|
+
user_tag_name: @options["#{part}_tag_name".to_sym],
|
28
|
+
user_stimulus_controllers: @options["#{part}_controllers".to_sym] || [],
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Allow useres to pass some shortened attributes for the component part
|
33
|
+
merge_user_options!(**@options)
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Add specific component user options if they pass shortened attributes.
|
38
|
+
#
|
39
|
+
def merge_user_options!(**kws)
|
40
|
+
@parts[:component][:user_tag_name] = kws[:tag_name] if kws[:tag_name]
|
41
|
+
@parts[:component][:user_css].push(kws[:css]) if kws[:css]
|
42
|
+
@parts[:component][:user_html].deep_merge!(kws[:html]) if kws[:html]
|
43
|
+
@parts[:component][:user_stimulus_controllers].push(kws[:controllers]) if kws[:controllers]
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Merge additional options into the defaults config by combining the new
|
48
|
+
# options with the existing options, rather than overwriting (where possible).
|
49
|
+
#
|
50
|
+
# HTML will be deep merged, CSS will be appended, and tag_name will be
|
51
|
+
# overwritten.
|
52
|
+
#
|
53
|
+
def smart_merge!(**kws)
|
54
|
+
@component.component_parts.each do |part, defaults|
|
55
|
+
set_tag_name(part, kws["#{part}_tag_name".to_sym])
|
56
|
+
add_css(part, kws["#{part}_css".to_sym])
|
57
|
+
add_html(part, kws["#{part}_html".to_sym])
|
58
|
+
|
59
|
+
controllers = kws["#{part}_controllers".to_sym] || []
|
60
|
+
|
61
|
+
controllers.each do |controller_name|
|
62
|
+
add_stimulus_controller(part, controller_name)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Make sure to merge any user-provided options as well
|
67
|
+
merge_user_options!(**kws)
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Returns the part for the reqeust part name or an empty hash if none was
|
72
|
+
# found.
|
73
|
+
#
|
74
|
+
def get_part(part_name)
|
75
|
+
@parts[part_name] || {}
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Sets the default tag name for the requested component part.
|
80
|
+
#
|
81
|
+
def set_tag_name(part_name, tag_name)
|
82
|
+
@parts[part_name][:default_tag_name] = tag_name if tag_name
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# Adds default CSS to the requested component part.
|
87
|
+
#
|
88
|
+
def add_css(part_name, css)
|
89
|
+
@parts[part_name][:default_css] << css if css
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Adds default HTML to the requested component part.
|
94
|
+
#
|
95
|
+
def add_html(part_name, html)
|
96
|
+
@parts[part_name][:default_html] = @parts[part_name][:default_html].deep_merge(html) if html
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Add a default Stimulus (Javascript) controller to the requested component part.
|
101
|
+
#
|
102
|
+
def add_stimulus_controller(part_name, controller_name)
|
103
|
+
@parts[part_name] ||= {}
|
104
|
+
@parts[part_name][:default_stimulus_controllers] ||= []
|
105
|
+
@parts[part_name][:default_stimulus_controllers] << controller_name
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Validate the component config and throw errors if there are issues.
|
110
|
+
#
|
111
|
+
def validate
|
112
|
+
validate_modifiers
|
113
|
+
end
|
114
|
+
|
115
|
+
#
|
116
|
+
# Validate that all of the modifiers are correct.
|
117
|
+
#
|
118
|
+
def validate_modifiers
|
119
|
+
# Check to make sure they have passed a valid / defined modifier
|
120
|
+
(@modifiers || []).each do |modifier|
|
121
|
+
if modifier.present? && !@component.valid_modifiers.include?(modifier)
|
122
|
+
raise LocoMotion::InvalidModifierError.new(modifier, @component)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# Validates that the requested part is valid for the component.
|
129
|
+
#
|
130
|
+
def validate_part(part_name)
|
131
|
+
raise LocoMotion::UnknownPartError.new(part_name, @component) unless valid_parts.include?(part_name)
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# Return a list of valid parts for the component.
|
136
|
+
#
|
137
|
+
def valid_parts
|
138
|
+
@parts.keys
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
# Render a Hash version of the config.
|
143
|
+
#
|
144
|
+
def to_h
|
145
|
+
{
|
146
|
+
options: @options,
|
147
|
+
parts: @parts,
|
148
|
+
modifiers: @modifiers,
|
149
|
+
size: @size
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
#
|
154
|
+
# For now, just return the Hash version for inspect.
|
155
|
+
#
|
156
|
+
def inspect
|
157
|
+
[
|
158
|
+
"#<#{self.class.name}",
|
159
|
+
"@options=#{@options.inspect}",
|
160
|
+
"@parts=#{@parts.inspect}",
|
161
|
+
"@modifiers=#{@modifiers.inspect}",
|
162
|
+
"@size=#{@size.inspect}",
|
163
|
+
].join(" ") + ">"
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module LocoMotion
|
2
|
+
class UnknownPartError < StandardError
|
3
|
+
def initialize(part, component, custom_message = nil)
|
4
|
+
no_parts_explanation = "No parts are defined on the component."
|
5
|
+
default_explanation = "Valid parts are #{component.config.valid_parts.map(&:inspect).to_sentence}."
|
6
|
+
|
7
|
+
has_parts = component.config.valid_parts.present?
|
8
|
+
|
9
|
+
default_message = [
|
10
|
+
"Unknown part #{part.inspect}.",
|
11
|
+
"#{has_parts ? default_explanation : no_parts_explanation}"
|
12
|
+
].join(' ')
|
13
|
+
|
14
|
+
super(custom_message || default_message)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class InvalidModifierError < StandardError
|
19
|
+
def initialize(modifier, component, custom_message = nil)
|
20
|
+
no_modifiers_explanation = "No modifiers are defined on the component."
|
21
|
+
default_explanation = "Valid modifiers are #{component.valid_modifiers.map(&:inspect).to_sentence}."
|
22
|
+
|
23
|
+
has_modifiers = component.valid_modifiers.present?
|
24
|
+
|
25
|
+
default_message = [
|
26
|
+
"Unknown modifier #{modifier.inspect}.",
|
27
|
+
"#{has_modifiers ? default_explanation : no_modifiers_explanation}"
|
28
|
+
].join(' ')
|
29
|
+
|
30
|
+
super(custom_message || default_message)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/loco_motion.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "rails"
|
2
|
+
require "haml-rails"
|
3
|
+
require "heroicons-rails"
|
4
|
+
|
5
|
+
require Gem::Specification.find_by_name("heroicons-rails").gem_dir + "/app/helpers/heroicons/icons_helper.rb"
|
6
|
+
|
7
|
+
require "view_component"
|
8
|
+
require "loco_motion/engine"
|
9
|
+
require "loco_motion/errors"
|
10
|
+
require "loco_motion/component_config"
|
11
|
+
require "loco_motion/base_component"
|
12
|
+
|
13
|
+
require "daisy"
|
14
|
+
|
15
|
+
#
|
16
|
+
# Module containing all features related to the LocoMotion gem.
|
17
|
+
#
|
18
|
+
module LocoMotion
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def configure
|
22
|
+
yield(configuration)
|
23
|
+
end
|
24
|
+
|
25
|
+
def configuration
|
26
|
+
@configuration ||= Configuration.new
|
27
|
+
end
|
28
|
+
|
29
|
+
# Mostly used for internal testing; not needed in Rails
|
30
|
+
def require_components
|
31
|
+
Dir.glob(File.dirname(__FILE__) + '/../app/components/**/*.rb').each do |file|
|
32
|
+
require file
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def define_render_helper(name, component)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Configuration
|
41
|
+
attr_accessor :base_component_class
|
42
|
+
|
43
|
+
def initialize
|
44
|
+
@base_component_class = LocoMotion::BaseComponent
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|