module Kiso::ComponentHelper
def kiso_merge_ui_layers(component, instance_ui)
-
(Hash{Symbol => String})- merged ui hash with all slot
Parameters:
-
instance_ui(Hash{Symbol => String}, nil) -- per-instance ui -
component(Symbol) -- the component name (e.g. +:card+)
def kiso_merge_ui_layers(component, instance_ui) global_ui = Kiso.config.theme.dig(component, :ui) return instance_ui || {} if global_ui.nil? || global_ui.empty? return global_ui if instance_ui.nil? || instance_ui.empty? # Instance wins. For slots in both, concatenate — tailwind_merge # resolves conflicts when .render(class:) is called. global_ui.merge(instance_ui) do |_slot, global_classes, instance_classes| "#{global_classes} #{instance_classes}" end end
def kiso_prepare_options(component_options, slot:, **data_attrs)
- Example: Caller passes data attributes through -
Example: With a Stimulus controller -
Example: Basic usage in a component partial -
Raises:
-
(ArgumentError)- if +component_options+ contains a +class:+ key.
Returns:
-
(Hash)- merged data attributes hash suitable for the +data:+
Parameters:
-
data_attrs(Hash) -- additional data attributes merged into the -
slot(String) -- the +data-slot+ value in kebab-case -
component_options(Hash) -- the +**component_options+ splat from
def kiso_prepare_options(component_options, slot:, **data_attrs) if component_options.key?(:class) raise ArgumentError, "Use css_classes: instead of class: for Kiso components" end user_data = component_options.delete(:data) || {} component_data = {slot: slot, **data_attrs} # Stimulus data-action and data-controller are space-separated and # additive — concatenate rather than overwrite so both the component's # and user's bindings apply. CONCATENABLE_DATA_KEYS.each do |key| if user_data.key?(key) && component_data.key?(key) component_data[key] = "#{user_data.delete(key)} #{component_data[key]}" end end user_data.merge(component_data) end
def kiso_render_component(component, part, path_prefix:, collection:, ui:, scope:, merge_global_ui: true, **kwargs, &block)
-
(ActiveSupport::SafeBuffer)- rendered HTML
Parameters:
-
block(Proc) -- optional block for component content -
kwargs(Hash) -- locals passed to the partial -
merge_global_ui(Boolean) -- whether to merge global config ui -
scope(Hash, nil) -- domain locals shared from parent to sub-parts -
ui(Hash{Symbol => String}, nil) -- per-slot class overrides -
collection(Array, nil) -- renders the partial once per item -
path_prefix(String) -- partial path prefix -
part(Symbol, nil) -- optional sub-part name -
component(Symbol) -- the component name
def kiso_render_component(component, part, path_prefix:, collection:, ui:, scope:, merge_global_ui: true, **kwargs, &block) path = if part "#{path_prefix}/#{component}/#{part}" else "#{path_prefix}/#{component}" end # Prevent yield from bubbling up the ERB rendering chain when no block # is passed. Without this, partials that use `capture { yield }.presence` # to support optional block overrides (e.g., toggle/collapse/separator) # would have their `yield` bubble through nested content_tag blocks all # the way to the layout's `<%= yield %>`, capturing the entire page # template content. An explicit empty proc gives `yield` something to # call, returning empty string → `.presence` returns nil → default # content renders correctly. block ||= proc {} if part # Sub-part: merge scope from parent context (scope values first, explicit kwargs win) parent_scope = kiso_current_scope(component) kwargs = parent_scope.merge(kwargs) if parent_scope.present? # Sub-part: merge slot override from parent's ui context parent_ui = kiso_current_ui(component) if (slot_classes = parent_ui[part].presence) existing = kwargs[:css_classes] || "" kwargs[:css_classes] = existing.blank? ? slot_classes : "#{existing} #{slot_classes}" end # Forward ui: to sub-part partial when explicitly provided kwargs[:ui] = ui if ui if collection render partial: path, collection: collection, locals: kwargs, &block else render path, **kwargs, &block end else # Parent component: merge global ui config with instance ui (or use instance ui directly) merged_ui = if merge_global_ui kiso_merge_ui_layers(component, ui) else ui || {} end has_ui = merged_ui.present? has_scope = scope.present? # Push context for composed sub-parts to read (skip when empty) kiso_push_ui_context(component, merged_ui) if has_ui kiso_push_scope(component, scope) if has_scope # Merge scope into parent kwargs so the parent partial receives # scope values as strict locals (scope as defaults, explicit kwargs win) kwargs = scope.merge(kwargs) if has_scope begin locals = has_ui ? kwargs.merge(ui: merged_ui) : kwargs if collection render partial: path, collection: collection, locals: locals, &block else render path, **locals, &block end ensure kiso_pop_ui_context(component) if has_ui kiso_pop_scope(component) if has_scope end end end
def kui(component, part = nil, collection: nil, ui: nil, scope: nil, **kwargs, &block)
- Example: Render a collection -
Example: Share domain locals with sub-parts via scope -
Example: Pass HTML attributes through to the root element -
Example: Override root element classes -
Example: Render an alert with inner element overrides -
Example: Render a card with per-slot overrides -
Example: Render a card with composed sub-parts -
Example: Render a simple badge -
Returns:
-
(ActiveSupport::SafeBuffer)- rendered HTML string
Other tags:
- Yield: - optional block for component content. When omitted, an empty
Parameters:
-
kwargs(Hash) -- locals forwarded to the partial (e.g. +color:+, -
scope(Hash, nil) -- domain locals shared from parent to sub-parts -
ui(Hash{Symbol => String}, nil) -- per-slot class overrides keyed -
collection(Array, nil) -- when present, renders the partial once -
part(Symbol, nil) -- optional sub-part name (e.g. +:header+, -
component(Symbol) -- the component name (e.g. +:badge+, +:card+,
def kui(component, part = nil, collection: nil, ui: nil, scope: nil, **kwargs, &block) kiso_render_component( component, part, path_prefix: "kiso/components", collection: collection, ui: ui, scope: scope, merge_global_ui: true, **kwargs, &block ) end
def kui_tag(tag, theme:, slot:, css_classes: "", variants: {}, **component_options, &block)
- Example: Button with type attribute -
Example: In a component partial -
Returns:
-
(ActiveSupport::SafeBuffer)- rendered HTML
Other tags:
- Yield: - optional block for element content
Parameters:
-
component_options(Hash) -- HTML attributes forwarded to -
variants(Hash) -- variant key-value pairs forwarded to -
css_classes(String) -- caller's class overrides, merged via -
slot(String) -- the +data-slot+ value in kebab-case -
theme(ClassVariants::Instance) -- the theme module to render -
tag(Symbol) -- HTML element name (e.g. +:div+, +:span+, +:button+)
def kui_tag(tag, theme:, slot:, css_classes: "", variants: {}, **component_options, &block) html_options = { class: theme.render(**variants, class: css_classes), data: kiso_prepare_options(component_options, slot: slot), **component_options } if block content_tag(tag, html_options, &block) else content_tag(tag, nil, html_options) end end