HasStimulusAttrs is a Ruby gem that provides a clean, declarative DSL for managing Stimulus.js data attributes in Ruby classes. It's particularly useful for component-based architectures in Rails applications where you need to generate Stimulus-compatible HTML attributes programmatically.
When building Rails applications with Stimulus.js, you often need to generate HTML elements with specific data attributes that Stimulus uses:
data-controllerdata-actiondata-[controller]-targetdata-[controller]-value- etc.
Managing these attributes manually can become cumbersome, especially when dealing with:
- Multiple controllers on a single element
- Dynamic values that change based on component state
- Conditional attributes
- Consistent naming conventions
HasStimulusAttrs solves these problems by providing a Ruby DSL that handles the complexity of generating proper Stimulus attributes.
The gem works by:
- Including the
HasStimulusAttrsmodule in your Ruby class - Defining a
controller_namemethod that returns your Stimulus controller's identifier - Using the provided DSL methods to declare what Stimulus attributes your component needs
- The gem automatically generates the correct
dom_datahash with properly formatted Stimulus attributes
HasStimulusAttrs
├── Includes HasDomAttrs (for DOM attribute management)
├── Includes StimulusHelpers (for Stimulus-specific formatting)
└── Provides ClassMethods when included
- has_dom_attrs - Provides the underlying DOM attribute management functionality
- stimulus_helpers - Provides helper methods for formatting Stimulus-specific attributes
- activesupport - Used for core Ruby extensions (like
blank?)
class MyComponent
include HasStimulusAttrs
def self.controller_name
"my-component"
end
endAdds a Stimulus controller to the element.
# Use default controller name
has_stimulus_controller
# Add additional controller
has_stimulus_controller "click-outside"
# Conditional controller
has_stimulus_controller "modal", if: :open?
# Dynamic controller name
has_stimulus_controller -> { "theme-#{current_theme}" }Defines a Stimulus action.
# Basic action
has_stimulus_action "click", "handleClick"
# Action for different controller
has_stimulus_action "submit", "save", controller: "form-controller"
# Conditional action
has_stimulus_action "keydown", "handleEscape", if: :keyboard_enabled?
# Dynamic action name (v0.2.2+)
has_stimulus_action "click", -> { admin? ? "adminAction" : "userAction" }Defines CSS classes managed by Stimulus.
# Static class
has_stimulus_class "active", "component--active"
# Dynamic class
has_stimulus_class "size", -> { "component--#{size}" }
# Using method
has_stimulus_class "theme", :theme_class_nameDefines Stimulus outlets.
# CSS selector outlet
has_stimulus_outlet "modal", "#main-modal"
# Dynamic outlet
has_stimulus_outlet "target", -> { "##{dom_id}" }Defines Stimulus parameters.
# Static param
has_stimulus_param :url, "/api/endpoint"
# Dynamic param
has_stimulus_param :id, -> { model.id }
# Using method
has_stimulus_param :config, :configuration_jsonMarks element as a Stimulus target.
# Basic target
has_stimulus_target "button"
# Target for different controller
has_stimulus_target "input", controller: "form-controller"Defines Stimulus values.
# Static value
has_stimulus_value "endpoint", "/api/data"
# Dynamic value
has_stimulus_value "userId", -> { current_user.id }
# Using method name as value
has_stimulus_value :timeout # calls timeout methodManually clears the cached dom_data result, forcing recomputation on the next call.
class DynamicComponent
include HasStimulusAttrs
attr_accessor :state
has_stimulus_value "state", -> { state }
def update_state(new_state)
@state = new_state
reset_dom_data_cache! # Force recomputation
end
endAll methods support these options:
:if- Include attribute only if condition is truthy:unless- Include attribute unless condition is truthy:controller- Specify a different controller (can be string or Proc)
class DropdownComponent
include HasStimulusAttrs
attr_reader :open
def self.controller_name
"dropdown"
end
has_stimulus_controller
has_stimulus_action "click", "toggle"
has_stimulus_class "open", "dropdown--open"
has_stimulus_value "open", -> { open }
has_stimulus_target "menu"
endclass ModalComponent
include HasStimulusAttrs
def self.controller_name
"modal"
end
has_stimulus_controller
has_stimulus_controller "trap-focus"
has_stimulus_controller "click-outside", if: :dismissible?
has_stimulus_action "click", "close", controller: "click-outside"
has_stimulus_action "keydown.esc", "close"
endclass ThemeComponent
include HasStimulusAttrs
attr_reader :theme, :user_preferences
def self.controller_name
"theme"
end
has_stimulus_controller
has_stimulus_value "theme", -> { user_preferences[:theme] || "light" }
has_stimulus_class "mode", -> { "theme--#{theme}" }
has_stimulus_param :config, -> { theme_configuration.to_json }
endclass ApplicationComponent
include HasStimulusAttrs
def self.controller_name
name.underscore.dasherize
end
end
class AlertComponent < ApplicationComponent
# Inherits controller_name as "alert-component"
has_stimulus_controller
has_stimulus_action "click", "dismiss"
has_stimulus_class "type", -> { "alert--#{type}" }
endclass ButtonComponent < ViewComponent::Base
include HasStimulusAttrs
def self.controller_name
"button"
end
has_stimulus_controller
has_stimulus_action "click", "handleClick"
has_stimulus_value "loading", -> { loading? }
def call
tag.button(**dom_attrs) do
content
end
end
endclass Card < Phlex::HTML
include HasStimulusAttrs
def self.controller_name
"card"
end
has_stimulus_controller
has_stimulus_action "mouseenter", "highlight"
has_stimulus_action "mouseleave", "unhighlight"
def template
div(**dom_attrs) do
yield
end
end
endclass ToggleComponent
include HasStimulusAttrs
has_stimulus_controller "toggle"
has_stimulus_controller "animation", if: :animated?
has_stimulus_controller "a11y", unless: :accessibility_disabled?
def animated?
@options[:animate] != false
end
def accessibility_disabled?
@options[:disable_a11y] == true
end
endclass PolymorphicComponent
include HasStimulusAttrs
has_stimulus_controller -> { "#{record.class.name.underscore}-controller" }
has_stimulus_value "id", -> { record.id }
has_stimulus_value "type", -> { record.class.name }
endclass FormComponent
include HasStimulusAttrs
# Multiple actions on same event
has_stimulus_action "submit", "validate"
has_stimulus_action "submit", "save"
# Actions with modifiers
has_stimulus_action "keydown.enter", "submit"
has_stimulus_action "input->debounced:300", "search"
# Dynamic action based on state
has_stimulus_action "click", -> { draft? ? "saveDraft" : "publish" }
endclass MyComponentTest < Minitest::Test
def test_stimulus_controller_included
component = MyComponent.new
assert_includes component.dom_data[:controller], "my-component"
end
def test_conditional_controller
component = MyComponent.new(active: true)
assert_includes component.dom_data[:controller], "active-state"
component = MyComponent.new(active: false)
refute_includes component.dom_data[:controller], "active-state"
end
def test_stimulus_values
component = MyComponent.new(user_id: 123)
assert_equal "123", component.dom_data["my-component-user-id-value"]
end
end# Good: Consistent with Stimulus conventions
def self.controller_name
"user-profile" # kebab-case
end
# Avoid: Inconsistent naming
def self.controller_name
"UserProfile" # Wrong case
end# Good: Single responsibility
class SearchComponent
has_stimulus_controller "search"
has_stimulus_action "input", "performSearch"
has_stimulus_value "endpoint", "/search"
end
# Avoid: Too many responsibilities
class KitchenSinkComponent
has_stimulus_controller "search"
has_stimulus_controller "modal"
has_stimulus_controller "dropdown"
# ... many more
end# Good: Dynamic value using Proc
has_stimulus_value "timestamp", -> { Time.current.to_i }
# Avoid: Static value that should be dynamic
has_stimulus_value "timestamp", Time.current.to_i # Set once at class load# Good: Conditional attributes for performance
has_stimulus_controller "animation", if: :animations_enabled?
has_stimulus_controller "analytics", unless: :private_mode?
# Avoid: Always including optional controllers
has_stimulus_controller "animation" # Even when not needed# Wrong: No controller_name defined
class MyComponent
include HasStimulusAttrs
has_stimulus_controller # Will raise NotImplementedError
end
# Correct: Define controller_name
class MyComponent
include HasStimulusAttrs
def self.controller_name
"my-component"
end
has_stimulus_controller
end# Wrong: Proc called at class definition
has_stimulus_value "random", -> { rand(100) }.call
# Correct: Proc called at runtime
has_stimulus_value "random", -> { rand(100) }# Be careful with multiple controllers
has_stimulus_target "button" # For default controller
has_stimulus_target "button", controller: "modal" # Different target!HasStimulusAttrs includes several built-in performance optimizations:
- Automatic Memoization:
dom_datais automatically cached after first computation - Early Conditional Exit: Expensive Procs are skipped when
:if/:unlessconditions fail - Controller Name Caching: Instance-level caching avoids repeated class method calls
- Lazy Evaluation: Procs are only evaluated when
dom_datais called
Manually clear the cached dom_data when component state changes:
class DynamicComponent
include HasStimulusAttrs
attr_accessor :theme
def self.controller_name
"dynamic"
end
has_stimulus_value "theme", -> { theme }
def theme=(new_theme)
@theme = new_theme
reset_dom_data_cache! # Clear cache when state changes
end
end- Use Conditional Attributes: Leverage
:if/:unlessfor expensive operations - Cache External Data: Pre-fetch expensive data rather than computing in Procs
- Reset Cache Appropriately: Call
reset_dom_data_cache!only when component state changes
class OptimizedComponent
include HasStimulusAttrs
# These are automatically optimized:
has_stimulus_controller "rich-text-editor", if: :rich_text_enabled?
has_stimulus_controller "syntax-highlighter", if: :code_blocks_present?
has_stimulus_value "config", -> { expensive_config_computation }
private
def expensive_config_computation
# This will only run once per component instance
# unless reset_dom_data_cache! is called
complex_calculation
end
end- Memoized calls: No Proc re-evaluation on subsequent
dom_datacalls - Conditional skipping: Expensive operations avoided when conditions aren't met
- Cached controller names: Single class method call per instance
- Memory efficient: Cache cleared automatically when component is garbage collected
component = MyComponent.new
puts component.dom_data.inspect
# => {:controller=>"my-component", :action=>"click->my-component#handleClick", ...}# In Rails console or tests
component = MyComponent.new
component.dom_data.each do |key, value|
puts "data-#{key}=\"#{value}\""
endUse browser developer tools to inspect the generated HTML and ensure Stimulus attributes are correct.
- 0.3.0 (Unreleased): Major performance optimizations
- Automatic
dom_datamemoization to prevent expensive Proc re-evaluation - Early conditional exit optimization for
:if/:unlessattributes - Controller name instance-level caching
- Added
reset_dom_data_cache!method for manual cache management
- Automatic
- 0.2.2 (2025-01-31): Added support for Proc in
has_stimulus_action - 0.2.0 (2023-03-22): Added Proc support for controller option
- 0.1.0: Initial release
The gem is open source and welcomes contributions. Key areas for contribution:
- Additional stimulus attribute types
- Performance improvements
- Documentation and examples
- Integration guides for different frameworks
HasStimulusAttrs provides a powerful, Ruby-idiomatic way to manage Stimulus.js attributes in your components. By leveraging its DSL, you can write cleaner, more maintainable component code while ensuring proper Stimulus integration.