Class: LocoMotion::BaseComponent

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

Direct Known Subclasses

Daisy::Actions::ButtonComponent, Daisy::Actions::DropdownComponent, Daisy::Actions::FabComponent, 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::TextRotateComponent, 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::OtpComponent, 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::AuraComponent, Daisy::Layout::DividerComponent, Daisy::Layout::DrawerComponent, Daisy::Layout::DrawerComponent::Daisy::Layout::DrawerSidebarComponent, Daisy::Layout::FooterComponent, Daisy::Layout::HeroComponent, Daisy::Layout::HoverComponent, Daisy::Layout::HoverGalleryComponent, 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::MegamenuComponent, Daisy::Navigation::MenuComponent, Daisy::Navigation::MenuItemComponent, Daisy::Navigation::NavbarComponent, Daisy::Navigation::StepComponent, Daisy::Navigation::StepsComponent, Daisy::Navigation::TabsComponent, Loco::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

Methods included from Concerns::InspectableComponent

#build_inspect_string

Constructor Details

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

Create a new instance of a component.

All components accept the following universal options for customizing the rendered HTML. Each is also available in a part-specific form by prefixing it with the part name (e.g. title_css, overlay_html, wrapper_aria).

Parameters:

  • kws (Hash)

    a customizable set of options

Options Hash (**kws):

  • :css (String)

    Extra CSS classes for the component's main element.

  • :html (Hash)

    Extra HTML attributes for the component's main element. Supports nested aria: / data: hashes, so html: { aria: { label: "Save" } } renders aria-label="Save".

  • :aria (Hash)

    A shorthand for html: { aria: { ... } }. Each key becomes an aria-* attribute, so aria: { label: "Save" } renders aria-label="Save". Values deep-merge with (and are overridden by nothing except) any aria set by the component itself.

  • :data (Hash)

    A shorthand for html: { data: { ... } }. Each key becomes a data-* attribute, so data: { foo: "bar" } renders data-foo="bar".

  • :tag_name (Symbol)

    Override the HTML tag for the component's main element.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/loco_motion/base_component.rb', line 59

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



24
25
26
# File 'lib/loco_motion/base_component.rb', line 24

def config
  @config
end

#loco_parentObject (readonly)

rubocop:enable Naming/AccessorMethodName



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

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.



219
220
221
222
# File 'lib/loco_motion/base_component.rb', line 219

def self.build(*_build_args, **build_kws, &build_block)
  builder = ComponentBuilder.new(self, build_kws, &build_block)
  builder.build
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.



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

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.



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

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.



119
120
121
122
123
124
125
126
127
# File 'lib/loco_motion/base_component.rb', line 119

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.



134
135
136
137
138
# File 'lib/loco_motion/base_component.rb', line 134

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.



194
195
196
# File 'lib/loco_motion/base_component.rb', line 194

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.



204
205
206
207
208
209
210
211
212
213
# File 'lib/loco_motion/base_component.rb', line 204

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.



145
146
147
148
# File 'lib/loco_motion/base_component.rb', line 145

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.



155
156
157
158
# File 'lib/loco_motion/base_component.rb', line 155

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.



97
98
99
100
# File 'lib/loco_motion/base_component.rb', line 97

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.



88
89
90
91
# File 'lib/loco_motion/base_component.rb', line 88

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.

rubocop:disable Naming/AccessorMethodName

Parameters:

  • component_name (Symbol, String)

    The name of the component.



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

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.



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

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:



230
231
232
# File 'lib/loco_motion/base_component.rb', line 230

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.



391
392
393
394
395
# File 'lib/loco_motion/base_component.rb', line 391

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.



365
366
367
368
369
# File 'lib/loco_motion/base_component.rb', line 365

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

  strip_spaces(css.join(" "))
end

#empty_part_content(tag_name) ⇒ Object



275
276
277
278
279
# File 'lib/loco_motion/base_component.rb', line 275

def empty_part_content(tag_name)
  return if EMPTY_PART_IGNORED_TAGS.include?(tag_name.to_sym)

  "<!-- Empty Part Block //-->".html_safe
end

#inspectObject

Provide some nice output for debugging or other purposes.



399
400
401
402
403
404
405
406
407
408
409
# File 'lib/loco_motion/base_component.rb', line 399

def inspect
  build_inspect_string(
    "component_name",
    "valid_modifiers",
    "valid_sizes",
    "config",
    "component_parts",
    "loco_parent",
    suffix: ">"
  )
end

#part(part_name, &block) ⇒ Object

Renders the given part.



248
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
# File 'lib/loco_motion/base_component.rb', line 248

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.



302
303
304
305
306
307
# File 'lib/loco_motion/base_component.rb', line 302

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.



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

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.



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

def rendered_html(part_name)
  default_html = @config.get_part(part_name)[:default_html] || {}
  user_html = @config.get_part(part_name)[:user_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.



355
356
357
358
359
360
# File 'lib/loco_motion/base_component.rb', line 355

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.



288
289
290
291
292
# File 'lib/loco_motion/base_component.rb', line 288

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.

rubocop:disable Naming/AccessorMethodName



239
240
241
# File 'lib/loco_motion/base_component.rb', line 239

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.



378
379
380
# File 'lib/loco_motion/base_component.rb', line 378

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