Module: ViewComponentCssDsl
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/view_component_css_dsl.rb,
lib/view_component_css_dsl/version.rb
Constant Summary collapse
- HTML_ATTR_KEYS =
HTML attributes auto-extracted from kwargs at construction time. Anything in this set is captured into @html_attrs instead of being passed to initialize, so callers can pass ‘class:`, `data:`, `aria:`, etc. without the component declaring them. To opt out, accept a kwarg with the same name in initialize (e.g. `def initialize(class:)`) or use a keyrest name other than html_attrs.
Set[ :alt, :aria, :autofocus, :class, :colspan, :contenteditable, :data, :dir, :disabled, :download, :draggable, :enterkeyhint, :formaction, :headers, :hidden, :href, :id, :inputmode, :lang, :loading, :low, :media, :onclick, :open, :optimum, :popover, :popovertarget, :popovertargetaction, :preload, :readonly, :rel, :role, :rowspan, :spellcheck, :src, :srcset, :style, :tabindex, :target, :title, :type, :value ].freeze
- SPACING_REGEX =
Single combined regex for padding/margin spacing (replaces 14 separate patterns) Captures: type (p/m), axis (x/y/t/r/b/l or nil for all), value
/\b(p|m)(x|y|t|r|b|l)?-(\d+)\b/- SPACING_AXIS_MAP =
Maps axis character to Set of affected sides
{ nil => Set[:t, :r, :b, :l], # p-4, m-4 = all sides "x" => Set[:l, :r], "y" => Set[:t, :b], "t" => Set[:t], "r" => Set[:r], "b" => Set[:b], "l" => Set[:l] }.freeze
- BORDER_REGEX =
Border width patterns (kept separate due to anchoring requirements)
/^border(?:-(x|y|t|r|b|l))?(?:-\d+)?$/- CATEGORIES =
Other category patterns (non-spacing, simple override by category) IMPORTANT: Use anchored patterns (^/$) to avoid matching substrings within compound classes (e.g., ‘h-8` within `min-h-8`, `flex` within `inline-flex`)
{ background: /^bg-/, text_color: /^text-((\w+-\d+)|white|black|transparent|current|inherit|action|success|danger|warning|brand)(\/\d+)?$/, text_size: /^text-(xs|sm|base|lg|xl|\d*xl)$/, border_color: /^border-(?!t|r|b|l|x|y|\d)(\w+-\d+|\w+)(\/\d+)?$/, width: /^w-/, height: /^h-/, min_width: /^min-w-/, min_height: /^min-h-/, max_width: /^max-w-/, max_height: /^max-h-/, # Display classes - note: `hidden` is intentionally excluded because it's # commonly used as a visibility toggle alongside other display classes # (e.g., "inline-flex hidden" where JS removes "hidden" to show element) display: /^(block|inline-block|inline-flex|inline-grid|inline|flex|grid|table-cell|table-row|table|contents|flow-root|list-item)$/, justify: /^justify-/, align: /^items-/, font_weight: /^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/, rounded: /^rounded(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, position: /^(static|relative|absolute|fixed|sticky)$/ }.freeze
- KNOWN_MODIFIERS =
Known Tailwind modifiers (prefixes like hover:, md:, first:, etc.) Classes with different modifiers should NOT conflict with each other
Set[ # Responsive "sm", "md", "lg", "xl", "2xl", "max-sm", "max-md", "max-lg", "max-xl", "max-2xl", # Interactive state "hover", "focus", "focus-within", "focus-visible", "active", "visited", "target", # Structural "first", "last", "only", "odd", "even", "first-of-type", "last-of-type", "only-of-type", "empty", # Form state "disabled", "enabled", "checked", "indeterminate", "default", "required", "valid", "invalid", "in-range", "out-of-range", "placeholder-shown", "autofill", "read-only", # Pseudo-elements "before", "after", "first-letter", "first-line", "marker", "selection", "file", "backdrop", "placeholder", # Media/Preference "dark", "print", "portrait", "landscape", "motion-safe", "motion-reduce", "contrast-more", "contrast-less", "forced-colors", # Direction "rtl", "ltr", # Attribute "open", # Direct children "*" ].freeze
- MODIFIER_PATTERNS =
Patterns that match dynamic modifiers (with optional names/arbitrary values) These use flexible regex to match ANY valid Tailwind modifier syntax
[ /^group(?:\/\w+)?$/, # group, group/<any-name> /^group-\w+(?:\/\w+)?$/, # group-hover, group-<state>/<any-name> /^peer(?:\/\w+)?$/, # peer, peer/<any-name> /^peer-\w+(?:\/\w+)?$/, # peer-checked, peer-<state>/<any-name> /^aria-\w+$/, # aria-checked, aria-<any-attr> /^aria-\[.+\]$/, # aria-[<arbitrary>] /^data-\[.+\]$/, # data-[<arbitrary>] /^supports-\[.+\]$/, # supports-[<arbitrary>] /^has-\[.+\]$/, # has-[<arbitrary>] /^group-has-\[.+\]$/, # group-has-[<arbitrary>] /^peer-has-\[.+\]$/, # peer-has-[<arbitrary>] /^min-\[.+\]$/, # min-[<arbitrary>] /^max-\[.+\]$/ # max-[<arbitrary>] ].freeze
- DATA_MERGE_KEYS =
%i[controller action].freeze
- VERSION =
"0.1.0"
Instance Method Summary collapse
-
#aria_attrs ⇒ Object
Overwrite in subclass to define default aria-attrs.
-
#css ⇒ Object
Instance method: generates final CSS string.
-
#custom_css ⇒ Object
Returns caller’s custom classes from html_attrs.
-
#data_attrs ⇒ Object
Overwrite in component subclass to set default data-attrs.
-
#final_aria_attrs ⇒ Object
Using merge allows for default value #aria_attrs, but also for dev to override that value per-instance as needed.
-
#final_data_attrs ⇒ Object
Loop through data-attrs and merge values from DATA_MERGE_KEYS.
-
#html_attrs ⇒ Object
Returns the hash to splat onto the top-level element of a component template:.
Instance Method Details
#aria_attrs ⇒ Object
Overwrite in subclass to define default aria-attrs
438 439 440 |
# File 'lib/view_component_css_dsl.rb', line 438 def aria_attrs {} end |
#css ⇒ Object
Instance method: generates final CSS string
343 344 345 |
# File 'lib/view_component_css_dsl.rb', line 343 def css build_classes end |
#custom_css ⇒ Object
Returns caller’s custom classes from html_attrs
348 349 350 351 352 |
# File 'lib/view_component_css_dsl.rb', line 348 def custom_css return "" unless @html_attrs @html_attrs.fetch(:class, "") end |
#data_attrs ⇒ Object
Overwrite in component subclass to set default data-attrs. They will be merged into html_attrs.
Example:
def data_attrs
{
controller: "some-stimulus-controller",
action: "click->some-stimulus-controller#someAction",
active: active?
}
end
391 392 393 |
# File 'lib/view_component_css_dsl.rb', line 391 def data_attrs {} end |
#final_aria_attrs ⇒ Object
Using merge allows for default value #aria_attrs, but also for dev to override that value per-instance as needed
444 445 446 |
# File 'lib/view_component_css_dsl.rb', line 444 def final_aria_attrs aria_attrs.merge(@html_attrs.fetch(:aria, {})) end |
#final_data_attrs ⇒ Object
Loop through data-attrs and merge values from DATA_MERGE_KEYS. Overwrite any others.
Ensures the caller doesn’t wipe out e.g. data-controller or data-action values defined by the dev in #data_attrs
Example:
Assuming MyComponent with #data_attrs defined as:
def data_attrs
{
controller: "foo",
label: "Hello world"
}
end
MyComponent.new(data: “bar”, label: “Goodbye”)
Will output the following data-attrs:
<div data-controller=“foo bar” data-label=“Goodbye”>
Notice:
-
data-controller from the caller is set alongside “foo” instead of overwriting
-
In contrast, data-label from the caller overwrites the default
424 425 426 427 428 429 430 431 432 433 434 435 |
# File 'lib/view_component_css_dsl.rb', line 424 def final_data_attrs incoming_data = @html_attrs.fetch(:data, {}) incoming_data.each_with_object(data_attrs) do |(key, value), final_data| final_value = if key.in?(DATA_MERGE_KEYS) [data_attrs[key], value].compact.join(" ") else value end final_data[key] = final_value end end |
#html_attrs ⇒ Object
Returns the hash to splat onto the top-level element of a component template:
<%= tag.div **html_attrs do %> ... <% end %>
Includes the smart-merged ‘:class`, merged `:data` and `:aria` (from any component-defined defaults + caller overrides), and every other caller-passed HTML attribute (`id:`, `role:`, etc.) forwarded to the rendered element.
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 |
# File 'lib/view_component_css_dsl.rb', line 361 def html_attrs return {} unless @html_attrs result = @html_attrs.except(:aria, :class, :data) # Only include aria/data if they have content, otherwise they'd override # inline attrs in templates like: tag.div data: {foo: "bar"}, **html_attrs aria = final_aria_attrs data = final_data_attrs result[:aria] = aria if aria.present? result[:data] = data if data.present? css_value = css result[:class] = css_value if css_value.present? result end |