loco_motion-rails 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|