Class: LocoMotion::BaseComponent

Inherits:
ViewComponent::Base
  • Object
show all
Includes:
RailsHeroicon::Helper
Defined in:
lib/loco_motion/base_component.rb

Direct Known Subclasses

Daisy::Actions::ButtonComponent, Daisy::Actions::DropdownComponent, Daisy::Actions::ModalComponent, Daisy::Actions::SwapComponent, Daisy::Actions::ThemeControllerComponent, Daisy::Actions::ThemePreviewComponent, Daisy::DataDisplay::AccordionComponent, Daisy::DataDisplay::AvatarComponent, Daisy::DataDisplay::BadgeComponent, Daisy::DataDisplay::CardComponent, Daisy::DataDisplay::CarouselComponent, Daisy::DataDisplay::ChatComponent, Daisy::DataDisplay::CollapseComponent, Daisy::DataDisplay::CountdownComponent, Daisy::DataDisplay::DiffComponent, Daisy::DataDisplay::FigureComponent, Daisy::DataDisplay::KbdComponent, Daisy::DataDisplay::ListComponent, Daisy::DataDisplay::ListItemComponent, Daisy::DataDisplay::StatComponent, Daisy::DataDisplay::StatusComponent, Daisy::DataDisplay::TableComponent, Daisy::DataDisplay::TimelineComponent, Daisy::DataDisplay::TimelineEventComponent, Daisy::DataInput::CallyComponent, Daisy::DataInput::CallyComponent::MonthComponent, Daisy::DataInput::CallyInputComponent, Daisy::DataInput::CheckboxComponent, Daisy::DataInput::FieldsetComponent, Daisy::DataInput::FileInputComponent, Daisy::DataInput::FilterComponent, Daisy::DataInput::LabelComponent, Daisy::DataInput::RadioButtonComponent, Daisy::DataInput::RangeComponent, Daisy::DataInput::RatingComponent, Daisy::DataInput::SelectComponent, Daisy::DataInput::TextAreaComponent, Daisy::DataInput::TextInputComponent, Daisy::Feedback::AlertComponent, Daisy::Feedback::LoadingComponent, Daisy::Feedback::ProgressComponent, Daisy::Feedback::RadialProgressComponent, Daisy::Feedback::SkeletonComponent, Daisy::Feedback::ToastComponent, Daisy::Feedback::TooltipComponent, Daisy::Layout::DividerComponent, Daisy::Layout::DrawerComponent, Daisy::Layout::DrawerSidebarComponent, Daisy::Layout::FooterComponent, Daisy::Layout::HeroComponent, Daisy::Layout::IndicatorComponent, Daisy::Layout::JoinComponent, Daisy::Layout::StackComponent, Daisy::Mockup::BrowserComponent, Daisy::Mockup::CodeComponent, Daisy::Mockup::CodeLineComponent, Daisy::Mockup::DeviceComponent, Daisy::Mockup::FrameComponent, Daisy::Navigation::BreadcrumbItemComponent, Daisy::Navigation::BreadcrumbsComponent, Daisy::Navigation::DockComponent, Daisy::Navigation::DockSectionComponent, Daisy::Navigation::LinkComponent, Daisy::Navigation::MenuComponent, Daisy::Navigation::MenuItemComponent, Daisy::Navigation::NavbarComponent, Daisy::Navigation::StepComponent, Daisy::Navigation::StepsComponent, Daisy::Navigation::TabsComponent, Hero::IconComponent, BasicComponent

Constant Summary collapse

SELF_CLOSING_TAGS =
i[area base br col embed hr img input keygen link meta param source track wbr].freeze
EMPTY_PART_IGNORED_TAGS =
i[textarea].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args, **kws, &block) ⇒ BaseComponent

Create a new instance of a component.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/loco_motion/base_component.rb', line 33

def initialize(*args, **kws, &block)
  super

  # Create our config object
  @config = LocoMotion::ComponentConfig.new(self, **kws, &block)

  # Run registered initializer hooks from concerns
  self.class.component_initializers.each { |initializer| send(initializer) }

  # Allow certain components to skip styling if they are being inherited
  @skip_styling = config_option(:skip_styling, false)

  # Allow manual passing of the loco parent on init if it's not auto-set
  # via slots
  @loco_parent = kws[:loco_parent] if kws.key?(:loco_parent)
end

Instance Attribute Details

#configObject (readonly)

Return the current configuration of this component.

Returns:

  • LocoMotion::ComponentConfig



22
23
24
# File 'lib/loco_motion/base_component.rb', line 22

def config
  @config
end

#loco_parentObject (readonly)

Returns the value of attribute loco_parent.



244
245
246
# File 'lib/loco_motion/base_component.rb', line 244

def loco_parent
  @loco_parent
end

Class Method Details

.build(*build_args, **build_kws, &build_block) ⇒ Object

Allows you to bulid a customized version of this component without having to define a new class.



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/loco_motion/base_component.rb', line 191

def self.build(*build_args, **build_kws, &build_block)
  klass = Class.new(self)

  # Unless already defined, delegate the name method to the superclass so
  # ViewComponent can find the sidecar partials and render them when no call
  # method is defined.
  unless klass.method_defined?(:name)
    klass.instance_eval do
      def name
        superclass.name
      end
    end
  end

  # Override the initialize method to combine the build and instance args
  klass.class_eval do
    original_initialize = method_defined?(:initialize) ? instance_method(:initialize) : nil

    define_method(:initialize) do |*instance_args, **instance_kws, &instance_block|
      if original_initialize
        original_initialize.bind(self).call
      else
        super(*instance_args, **instance_kws, &instance_block)
      end

      @config.smart_merge!(**build_kws)
    end
  end

  # Finally, execute any block they passed in to allow for customizations
  klass.class_eval(&build_block) if block_given?

  klass
end

.define_modifier(modifier_name) ⇒ Object

Defines a single modifier of this component. Modifiers control certain rendering aspects of the component.

Parameters:

  • modifier_name (Symbol)

    The name of the modifier.



138
139
140
# File 'lib/loco_motion/base_component.rb', line 138

def self.define_modifier(modifier_name)
  define_modifiers(modifier_name)
end

.define_modifiers(*modifier_names) ⇒ Object

Define multiple modifiers for this component. Modifiers control certain rendering aspects of the component.

Parameters:

  • modifier_names (Array[Symbol])

    An array of the modifier names you wish to define.



149
150
151
152
153
154
155
156
157
158
# File 'lib/loco_motion/base_component.rb', line 149

def self.define_modifiers(*modifier_names)
  # Note that since we're using Rails' class_attribute method for these, we
  # must take care not to alter the original object but rather use a setter
  # (the `+=` in this case) to set the new value so Rails knows not to
  # override the parent value.
  #
  # For example, we cannot use `<<` or `concat` here.
  self.valid_modifiers ||= []
  self.valid_modifiers += modifier_names
end

.define_part(part_name, part_defaults = {}) ⇒ Object

Defines a new part of this component which can customize CSS, HTML and more.

Parameters:

  • part_name (Symbol)

    The name of the part.

  • part_defaults (Hash) (defaults to: {})

    Any default config options such as ‘tag_name`.



91
92
93
94
95
96
97
98
99
# File 'lib/loco_motion/base_component.rb', line 91

def self.define_part(part_name, part_defaults = {})
  # Note that since we're using Rails' class_attribute method for these, we
  # must take care not to alter the original object but rather use a setter
  # (the `=` in this case) to set the new value so Rails knows not to override
  # the parent value.
  #
  # For example, we cannot use `merge!` or `[part_name] = ` here.
  self.component_parts = component_parts.merge({ part_name => part_defaults })
end

.define_parts(*part_names) ⇒ Object

Convenience method for defining multiple parts at once with no defaults.

Parameters:

  • part_names (Array<Symbol>)

    The names of the parts you wish to define.



106
107
108
109
110
# File 'lib/loco_motion/base_component.rb', line 106

def self.define_parts(*part_names)
  (part_names || []).each do |part_name|
    define_part(part_name)
  end
end

.define_size(size_name) ⇒ Object

Define a single size of this component. Sizes control how big or small this component will render.

Parameters:

  • size_name (Symbol)

    The name of the size you wish to define.



166
167
168
# File 'lib/loco_motion/base_component.rb', line 166

def self.define_size(size_name)
  define_sizes(size_name)
end

.define_sizes(*size_names) ⇒ Object

Define multiple sizes for this component. Sizes control how big or small this component will render.

Parameters:

  • size_names (Array[Symbol])

    An array of the sizes you wish to define.



176
177
178
179
180
181
182
183
184
185
# File 'lib/loco_motion/base_component.rb', line 176

def self.define_sizes(*size_names)
  # Note that since we're using Rails' class_attribute method for these, we
  # must take care not to alter the original object but rather use a setter
  # (the `+=` in this case) to set the new value so Rails knows not to
  # override the parent value.
  #
  # For example, we cannot use `<<` or `concat` here.
  self.valid_sizes ||= []
  self.valid_sizes += size_names
end

.register_component_initializer(method_name) ⇒ Object

Register an instance method to be called during component initialization.

Parameters:

  • method_name (Symbol)

    The name of the instance method to call.



117
118
119
120
# File 'lib/loco_motion/base_component.rb', line 117

def self.register_component_initializer(method_name)
  # Ensure we don't modify the parent class's array directly
  self.component_initializers += [method_name.to_sym]
end

.register_component_setup(method_name) ⇒ Object

Register an instance method to be called before component rendering.

Parameters:

  • method_name (Symbol)

    The name of the instance method to call.



127
128
129
130
# File 'lib/loco_motion/base_component.rb', line 127

def self.register_component_setup(method_name)
  # Ensure we don't modify the parent class's array directly
  self.component_setups += [method_name.to_sym]
end

.renders_many(*args) ⇒ Object

Override the default many slot to render the BasicComponent if no component is provided.



71
72
73
74
# File 'lib/loco_motion/base_component.rb', line 71

def self.renders_many(*args)
  # If they don't pass extra options, default to BasicComponent
  args&.size == 1 ?  super(*args + [LocoMotion::BasicComponent]) : super
end

.renders_one(*args) ⇒ Object

Override the default slot to render the BasicComponent if no component is provided.



62
63
64
65
# File 'lib/loco_motion/base_component.rb', line 62

def self.renders_one(*args)
  # If they don't pass extra options, default to BasicComponent
  args&.size == 1 ?  super(*args + [LocoMotion::BasicComponent]) : super
end

.set_component_name(component_name) ⇒ Object

Sets the component name used in CSS generation.

Parameters:

  • component_name (Symbol, String)

    The name of the component.



81
82
83
# File 'lib/loco_motion/base_component.rb', line 81

def self.set_component_name(component_name)
  self.component_name = component_name
end

Instance Method Details

#before_renderObject

Run registered setup hooks from concerns before rendering.



53
54
55
56
# File 'lib/loco_motion/base_component.rb', line 53

def before_render
  # Note: ViewComponent::Base does not define before_render, so no super call needed.
  self.class.component_setups.each { |setup| send(setup) }
end

#component_refBaseComponent

Returns a reference to this component. Useful for passing a parent component into child components.

Returns:



232
233
234
# File 'lib/loco_motion/base_component.rb', line 232

def component_ref
  self
end

#config_option(key, default = nil) ⇒ Object

Retrieve the requested component option, or the desired default if no option was provided.

Parameters:

  • key (Symbol)

    The name of the keyword argument option you wish to retrieve.

  • default (Object) (defaults to: nil)

    Any value that you wish to use as a default should the option be undefined. Defaults to ‘nil`.



393
394
395
396
397
# File 'lib/loco_motion/base_component.rb', line 393

def config_option(key, default = nil)
  value = @config.options[key]

  value.nil? ? default : value
end

#cssify(content) ⇒ Object

Convert strings, symbols, and arrays of those into a single CSS-like string.



367
368
369
370
371
# File 'lib/loco_motion/base_component.rb', line 367

def cssify(content)
  css = [content].flatten.compact

  strip_spaces(css.join(" "))
end

#empty_part_content(tag_name) ⇒ Object



277
278
279
280
281
# File 'lib/loco_motion/base_component.rb', line 277

def empty_part_content(tag_name)
  unless EMPTY_PART_IGNORED_TAGS.include?(tag_name.to_sym)
    "<!-- Empty Part Block //-->".html_safe
  end
end

#inspectObject

Provide some nice output for debugging or other purposes.



401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# File 'lib/loco_motion/base_component.rb', line 401

def inspect
  parts = component_parts.map do |part_name, part_defaults|
    {
      part_name: part_name,
      tag_name: rendered_tag_name(part_name),
      css: rendered_css(part_name),
      html: rendered_html(part_name)
    }
  end

  [
    "#<#{self.class.name}",
    "@component_name=#{(component_name || :unnamed).inspect}",
    "@valid_modifiers=#{valid_modifiers.inspect}",
    "@valid_sizes=#{valid_sizes.inspect}",
    "@config=#{@config.inspect}",
    "@component_parts=#{parts.inspect}",
    "@loco_parent=#{loco_parent.inspect}",
  ].join(" ") + ">"
end

#part(part_name, &block) ⇒ Object

Renders the given part.



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/loco_motion/base_component.rb', line 249

def part(part_name, &block)

  # Validate the part_name
  @config.validate_part(part_name)

  # Grab the rendered tag name
  tag_name = rendered_tag_name(part_name)

  if block_given?
    (tag_name, **rendered_html(part_name), &block)
  else
    # The `tag()` helper will allow you to pass any tag without a block, but
    # this isn't valid HTML. In particular, it will render a "self-closing"
    # <div /> tag which doesn't actually close the div.
    #
    # Therefore, we need to pass some kind of block to ensure it closes. We've
    # choosen a comment to keep the output as clean as possible while still
    # informing a developer what is happening.
    if SELF_CLOSING_TAGS.include?(tag_name.to_sym)
      tag(tag_name, **rendered_html(part_name))
    else
      (tag_name, **rendered_html(part_name)) do
        empty_part_content(tag_name)
      end
    end
  end
end

#rendered_css(part_name) ⇒ String

Builds a string suitable for the HTML element’s ‘class` attribute for the requested component part.

Parameters:

  • part_name (Symbol)

    The component part whose CSS you desire.

Returns:

  • (String)

    A string of CSS names.



304
305
306
307
308
309
# File 'lib/loco_motion/base_component.rb', line 304

def rendered_css(part_name)
  default_css = @config.get_part(part_name)[:default_css]
  user_css = @config.get_part(part_name)[:user_css]

  cssify([default_css, user_css])
end

#rendered_data(part_name) ⇒ Hash

Builds the HTML ‘data` attribute.

Parameters:

  • part_name (Symbol)

    The component part whose HTML ‘data` attribute you desire.

Returns:

  • (Hash)

    A hash of objects to be rendered in the ‘data` attribute.



338
339
340
341
342
343
344
345
346
# File 'lib/loco_motion/base_component.rb', line 338

def rendered_data(part_name)
  generated_data = {}

  stimulus_controllers = rendered_stimulus_controllers(part_name)

  generated_data[:controller] = stimulus_controllers if stimulus_controllers.present?

  generated_data
end

#rendered_html(part_name) ⇒ Hash

Builds a Hash of all of the HTML attributes for the requested component part.

Parameters:

  • part_name (Symbol)

    The component part whose HTML you desire.

Returns:

  • (Hash)

    A combination of all generated, component default, and user-specified HTML attributes for the part.



320
321
322
323
324
325
326
327
328
# File 'lib/loco_motion/base_component.rb', line 320

def rendered_html(part_name)
  default_html = @config.get_part(part_name)[:default_html] || {}
  user_html = @config.get_part(part_name)[:user_html] || {}

  generated_html = {
    class: rendered_css(part_name),
    data: rendered_data(part_name)
  }.deep_merge(default_html).deep_merge(user_html)
end

#rendered_stimulus_controllers(part_name) ⇒ Object

Builds a list of Stimulus controllers for the HTML ‘data-controller` attribute.

@ return [String] A space-separated list of Stimulus controllers.

Parameters:

  • part_name (Symbol)

    The component part whose Stimulus controllers you desire.



357
358
359
360
361
362
# File 'lib/loco_motion/base_component.rb', line 357

def rendered_stimulus_controllers(part_name)
  default_controllers = @config.get_part(part_name)[:default_stimulus_controllers]
  user_controllers = @config.get_part(part_name)[:user_stimulus_controllers]

  strip_spaces([default_controllers, user_controllers].join(" "))
end

#rendered_tag_name(part_name) ⇒ Symbol, String

Returns the user-provided or component-default HTML tag-name.

Parameters:

  • part_name (Symbol)

    The part whose tag-name you desire.

Returns:

  • (Symbol, String)

    The HTML tag-name for the requested comopnent part.



290
291
292
293
294
# File 'lib/loco_motion/base_component.rb', line 290

def rendered_tag_name(part_name)
  part = @config.get_part(part_name)

  part[:user_tag_name] || part[:default_tag_name]
end

#set_loco_parent(parent) ⇒ Object

Sets the parent component of this component. Enables child components to ask questions of their parent and access parent config.



241
242
243
# File 'lib/loco_motion/base_component.rb', line 241

def set_loco_parent(parent)
  @loco_parent = parent
end

#strip_spaces(str) ⇒ String

Strip extra whitespace from a given string.

Parameters:

  • str (String)

    The string you wish to strip.

Returns:

  • (String)

    A string with minimal possible whitespace.



380
381
382
# File 'lib/loco_motion/base_component.rb', line 380

def strip_spaces(str)
  str.gsub(/ +/, " ").strip
end