Class: LocoMotion::BaseComponent

Inherits:
ViewComponent::Base
  • Object
show all
Includes:
Concerns::InspectableComponent, RailsHeroicon::Helper
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::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::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::CodeComponent::Daisy::Mockup::CodeLineComponent, Daisy::Mockup::DeviceComponent, Daisy::Mockup::FrameComponent, Daisy::Navigation::BreadcrumbsComponent, Daisy::Navigation::BreadcrumbsComponent::Daisy::Navigation::BreadcrumbItemComponent, Daisy::Navigation::DockComponent, Daisy::Navigation::DockComponent::Daisy::Navigation::DockSectionComponent, Daisy::Navigation::LinkComponent, Daisy::Navigation::MenuComponent, Daisy::Navigation::MenuComponent::Daisy::Navigation::MenuItemComponent, Daisy::Navigation::NavbarComponent, Daisy::Navigation::StepsComponent, Daisy::Navigation::StepsComponent::Daisy::Navigation::StepComponent, 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

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.



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

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



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

def config
  @config
end

#loco_parentObject (readonly)

rubocop:enable Naming/AccessorMethodName



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.



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

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.



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

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.



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

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



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

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.



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

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.



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

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.



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

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.



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

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.



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

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.



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

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.



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

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.



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

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.



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

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:



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

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



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

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.



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

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

  strip_spaces(css.join(" "))
end

#empty_part_content(tag_name) ⇒ Object



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

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.



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

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.



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



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

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.



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

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.



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

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.



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

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.



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

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



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

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.



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

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