Module: ViewComponentCssDsl

Extended by:
ActiveSupport::Concern
Defined in:
lib/view_component_css_dsl.rb,
lib/view_component_css_dsl/version.rb

Constant Summary collapse

MERGER =

Single shared merger. tailwind_merge builds a conflict-group index on initialization, so we pay that cost once per process rather than per call.

TailwindMerge::Merger.new
HTML_ATTR_KEYS =
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
DATA_MERGE_KEYS =
%i[controller action].freeze
VERSION =
"0.1.3"

Instance Method Summary collapse

Instance Method Details

#aria_attrsObject

Overwrite in subclass to define default aria-attrs



369
370
371
# File 'lib/view_component_css_dsl.rb', line 369

def aria_attrs
  {}
end

#cssObject

Instance method: generates final CSS string



276
277
278
# File 'lib/view_component_css_dsl.rb', line 276

def css
  build_classes
end

#custom_cssObject

Returns caller’s custom classes from html_attrs



281
282
283
284
285
# File 'lib/view_component_css_dsl.rb', line 281

def custom_css
  return "" unless @html_attrs

  @html_attrs.fetch(:class, "")
end

#data_attrsObject

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



327
328
329
# File 'lib/view_component_css_dsl.rb', line 327

def data_attrs
  {}
end

#final_aria_attrsObject

Merge in order: DSL declarations -> method override -> caller’s :aria. Hash#merge throughout — caller wins on collision (no additive semantics).



375
376
377
# File 'lib/view_component_css_dsl.rb', line 375

def final_aria_attrs
  resolved_attr_rules(:aria).merge(aria_attrs).merge(@html_attrs.fetch(:aria, {}))
end

#final_data_attrsObject

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



360
361
362
363
364
365
366
# File 'lib/view_component_css_dsl.rb', line 360

def final_data_attrs
  # Merge in order: DSL declarations -> method override -> caller's :data.
  # Each layer uses DATA_MERGE_KEYS semantics (controller/action concatenate,
  # everything else replaces).
  combined = merge_data_layer(resolved_attr_rules(:data), data_attrs)
  merge_data_layer(combined, @html_attrs.fetch(:data, {}))
end

#html_attrsObject

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.



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/view_component_css_dsl.rb', line 294

def html_attrs
  return {} unless @html_attrs

  # Start with DSL-declared top-level attrs; caller's html_attrs layer on top
  # (caller wins on collision, mirroring the css behavior).
  dsl_attrs = resolved_attr_rules(:attribute)
  result = dsl_attrs.merge(@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