has_stimulus_attrs 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/CLAUDE.md +612 -0
- data/README.md +10 -14
- data/lib/has_stimulus_attrs/version.rb +1 -1
- data/lib/has_stimulus_attrs.rb +45 -6
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d92df61326364a67fd89516850b87e5dfe9e85cbbc1982f6f5815978f033c18
|
4
|
+
data.tar.gz: d7cfcb016ff83d8aa53c43527cb6753b587958e075f071f0261cf16044365ac1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb72b769b5aa0ed1eadb3bb7dfaac561c987ff2d11acc9273fa80ec71284fcd790ec42bf7a78c82b607561062981b97b1fad3c611e6e57dd3662b439b584cb8d
|
7
|
+
data.tar.gz: 58afec2f65b664ce1a424931e7b987e0bf210343f6d782407f04514df651995107a22e8f09d9a5e9b21950caf27390f3eb88a23a8702274009a6041567ef960d
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [0.3.0](https://github.com/tomasc/has_stimulus_attrs/compare/v0.2.2...v0.3.0) (2025-01-02)
|
4
|
+
|
5
|
+
### Performance
|
6
|
+
|
7
|
+
* **Major performance optimizations**: Implemented intelligent memoization for `dom_data` method to prevent expensive Proc re-evaluation on repeated calls
|
8
|
+
* **Early conditional exit**: Optimized conditional attribute evaluation to skip expensive operations when `:if`/`:unless` conditions fail
|
9
|
+
* **Controller name caching**: Added instance-level caching for `controller_name` to avoid repeated class method calls
|
10
|
+
* **Smart cache management**: Added `reset_dom_data_cache!` method for manual cache invalidation when needed
|
11
|
+
|
12
|
+
### Features
|
13
|
+
|
14
|
+
* **Performance testing**: Added comprehensive performance test suite to validate optimizations and prevent regressions
|
15
|
+
|
3
16
|
## [0.2.2](https://github.com/tomasc/has_stimulus_attrs/compare/v0.2.1...v0.2.2) (2025-01-31)
|
4
17
|
|
5
18
|
### Features
|
data/CLAUDE.md
ADDED
@@ -0,0 +1,612 @@
|
|
1
|
+
# HasStimulusAttrs
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
HasStimulusAttrs is a Ruby gem that provides a clean, declarative DSL for managing [Stimulus.js](https://stimulus.hotwired.dev/) 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.
|
6
|
+
|
7
|
+
## Core Concepts
|
8
|
+
|
9
|
+
### What Problem Does This Solve?
|
10
|
+
|
11
|
+
When building Rails applications with Stimulus.js, you often need to generate HTML elements with specific data attributes that Stimulus uses:
|
12
|
+
- `data-controller`
|
13
|
+
- `data-action`
|
14
|
+
- `data-[controller]-target`
|
15
|
+
- `data-[controller]-value`
|
16
|
+
- etc.
|
17
|
+
|
18
|
+
Managing these attributes manually can become cumbersome, especially when dealing with:
|
19
|
+
- Multiple controllers on a single element
|
20
|
+
- Dynamic values that change based on component state
|
21
|
+
- Conditional attributes
|
22
|
+
- Consistent naming conventions
|
23
|
+
|
24
|
+
HasStimulusAttrs solves these problems by providing a Ruby DSL that handles the complexity of generating proper Stimulus attributes.
|
25
|
+
|
26
|
+
### How It Works
|
27
|
+
|
28
|
+
The gem works by:
|
29
|
+
1. Including the `HasStimulusAttrs` module in your Ruby class
|
30
|
+
2. Defining a `controller_name` method that returns your Stimulus controller's identifier
|
31
|
+
3. Using the provided DSL methods to declare what Stimulus attributes your component needs
|
32
|
+
4. The gem automatically generates the correct `dom_data` hash with properly formatted Stimulus attributes
|
33
|
+
|
34
|
+
## Architecture
|
35
|
+
|
36
|
+
### Module Structure
|
37
|
+
|
38
|
+
```
|
39
|
+
HasStimulusAttrs
|
40
|
+
├── Includes HasDomAttrs (for DOM attribute management)
|
41
|
+
├── Includes StimulusHelpers (for Stimulus-specific formatting)
|
42
|
+
└── Provides ClassMethods when included
|
43
|
+
```
|
44
|
+
|
45
|
+
### Key Dependencies
|
46
|
+
|
47
|
+
1. **has_dom_attrs** - Provides the underlying DOM attribute management functionality
|
48
|
+
2. **stimulus_helpers** - Provides helper methods for formatting Stimulus-specific attributes
|
49
|
+
3. **activesupport** - Used for core Ruby extensions (like `blank?`)
|
50
|
+
|
51
|
+
## API Reference
|
52
|
+
|
53
|
+
### Including the Module
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class MyComponent
|
57
|
+
include HasStimulusAttrs
|
58
|
+
|
59
|
+
def self.controller_name
|
60
|
+
"my-component"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
### Available Methods
|
66
|
+
|
67
|
+
#### `has_stimulus_controller(name = controller_name, **options)`
|
68
|
+
|
69
|
+
Adds a Stimulus controller to the element.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# Use default controller name
|
73
|
+
has_stimulus_controller
|
74
|
+
|
75
|
+
# Add additional controller
|
76
|
+
has_stimulus_controller "click-outside"
|
77
|
+
|
78
|
+
# Conditional controller
|
79
|
+
has_stimulus_controller "modal", if: :open?
|
80
|
+
|
81
|
+
# Dynamic controller name
|
82
|
+
has_stimulus_controller -> { "theme-#{current_theme}" }
|
83
|
+
```
|
84
|
+
|
85
|
+
#### `has_stimulus_action(event, action, controller: nil, **options)`
|
86
|
+
|
87
|
+
Defines a Stimulus action.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
# Basic action
|
91
|
+
has_stimulus_action "click", "handleClick"
|
92
|
+
|
93
|
+
# Action for different controller
|
94
|
+
has_stimulus_action "submit", "save", controller: "form-controller"
|
95
|
+
|
96
|
+
# Conditional action
|
97
|
+
has_stimulus_action "keydown", "handleEscape", if: :keyboard_enabled?
|
98
|
+
|
99
|
+
# Dynamic action name (v0.2.2+)
|
100
|
+
has_stimulus_action "click", -> { admin? ? "adminAction" : "userAction" }
|
101
|
+
```
|
102
|
+
|
103
|
+
#### `has_stimulus_class(name, value, controller: nil, **options)`
|
104
|
+
|
105
|
+
Defines CSS classes managed by Stimulus.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
# Static class
|
109
|
+
has_stimulus_class "active", "component--active"
|
110
|
+
|
111
|
+
# Dynamic class
|
112
|
+
has_stimulus_class "size", -> { "component--#{size}" }
|
113
|
+
|
114
|
+
# Using method
|
115
|
+
has_stimulus_class "theme", :theme_class_name
|
116
|
+
```
|
117
|
+
|
118
|
+
#### `has_stimulus_outlet(name, value, controller: nil, **options)`
|
119
|
+
|
120
|
+
Defines Stimulus outlets.
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
# CSS selector outlet
|
124
|
+
has_stimulus_outlet "modal", "#main-modal"
|
125
|
+
|
126
|
+
# Dynamic outlet
|
127
|
+
has_stimulus_outlet "target", -> { "##{dom_id}" }
|
128
|
+
```
|
129
|
+
|
130
|
+
#### `has_stimulus_param(name, value, controller: nil, **options)`
|
131
|
+
|
132
|
+
Defines Stimulus parameters.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# Static param
|
136
|
+
has_stimulus_param :url, "/api/endpoint"
|
137
|
+
|
138
|
+
# Dynamic param
|
139
|
+
has_stimulus_param :id, -> { model.id }
|
140
|
+
|
141
|
+
# Using method
|
142
|
+
has_stimulus_param :config, :configuration_json
|
143
|
+
```
|
144
|
+
|
145
|
+
#### `has_stimulus_target(name, controller: nil, **options)`
|
146
|
+
|
147
|
+
Marks element as a Stimulus target.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
# Basic target
|
151
|
+
has_stimulus_target "button"
|
152
|
+
|
153
|
+
# Target for different controller
|
154
|
+
has_stimulus_target "input", controller: "form-controller"
|
155
|
+
```
|
156
|
+
|
157
|
+
#### `has_stimulus_value(name, value = nil, controller: nil, **options)`
|
158
|
+
|
159
|
+
Defines Stimulus values.
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
# Static value
|
163
|
+
has_stimulus_value "endpoint", "/api/data"
|
164
|
+
|
165
|
+
# Dynamic value
|
166
|
+
has_stimulus_value "userId", -> { current_user.id }
|
167
|
+
|
168
|
+
# Using method name as value
|
169
|
+
has_stimulus_value :timeout # calls timeout method
|
170
|
+
```
|
171
|
+
|
172
|
+
#### `reset_dom_data_cache!`
|
173
|
+
|
174
|
+
Manually clears the cached `dom_data` result, forcing recomputation on the next call.
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
class DynamicComponent
|
178
|
+
include HasStimulusAttrs
|
179
|
+
|
180
|
+
attr_accessor :state
|
181
|
+
|
182
|
+
has_stimulus_value "state", -> { state }
|
183
|
+
|
184
|
+
def update_state(new_state)
|
185
|
+
@state = new_state
|
186
|
+
reset_dom_data_cache! # Force recomputation
|
187
|
+
end
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
### Options
|
192
|
+
|
193
|
+
All methods support these options:
|
194
|
+
- `:if` - Include attribute only if condition is truthy
|
195
|
+
- `:unless` - Include attribute unless condition is truthy
|
196
|
+
- `:controller` - Specify a different controller (can be string or Proc)
|
197
|
+
|
198
|
+
## Usage Patterns
|
199
|
+
|
200
|
+
### Basic Component
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
class DropdownComponent
|
204
|
+
include HasStimulusAttrs
|
205
|
+
|
206
|
+
attr_reader :open
|
207
|
+
|
208
|
+
def self.controller_name
|
209
|
+
"dropdown"
|
210
|
+
end
|
211
|
+
|
212
|
+
has_stimulus_controller
|
213
|
+
has_stimulus_action "click", "toggle"
|
214
|
+
has_stimulus_class "open", "dropdown--open"
|
215
|
+
has_stimulus_value "open", -> { open }
|
216
|
+
has_stimulus_target "menu"
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
### Component with Multiple Controllers
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
class ModalComponent
|
224
|
+
include HasStimulusAttrs
|
225
|
+
|
226
|
+
def self.controller_name
|
227
|
+
"modal"
|
228
|
+
end
|
229
|
+
|
230
|
+
has_stimulus_controller
|
231
|
+
has_stimulus_controller "trap-focus"
|
232
|
+
has_stimulus_controller "click-outside", if: :dismissible?
|
233
|
+
|
234
|
+
has_stimulus_action "click", "close", controller: "click-outside"
|
235
|
+
has_stimulus_action "keydown.esc", "close"
|
236
|
+
end
|
237
|
+
```
|
238
|
+
|
239
|
+
### Dynamic Component
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
class ThemeComponent
|
243
|
+
include HasStimulusAttrs
|
244
|
+
|
245
|
+
attr_reader :theme, :user_preferences
|
246
|
+
|
247
|
+
def self.controller_name
|
248
|
+
"theme"
|
249
|
+
end
|
250
|
+
|
251
|
+
has_stimulus_controller
|
252
|
+
has_stimulus_value "theme", -> { user_preferences[:theme] || "light" }
|
253
|
+
has_stimulus_class "mode", -> { "theme--#{theme}" }
|
254
|
+
has_stimulus_param :config, -> { theme_configuration.to_json }
|
255
|
+
end
|
256
|
+
```
|
257
|
+
|
258
|
+
### Inheritance Pattern
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
class ApplicationComponent
|
262
|
+
include HasStimulusAttrs
|
263
|
+
|
264
|
+
def self.controller_name
|
265
|
+
name.underscore.dasherize
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
class AlertComponent < ApplicationComponent
|
270
|
+
# Inherits controller_name as "alert-component"
|
271
|
+
has_stimulus_controller
|
272
|
+
has_stimulus_action "click", "dismiss"
|
273
|
+
has_stimulus_class "type", -> { "alert--#{type}" }
|
274
|
+
end
|
275
|
+
```
|
276
|
+
|
277
|
+
## Integration with Rails
|
278
|
+
|
279
|
+
### ViewComponent Example
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
class ButtonComponent < ViewComponent::Base
|
283
|
+
include HasStimulusAttrs
|
284
|
+
|
285
|
+
def self.controller_name
|
286
|
+
"button"
|
287
|
+
end
|
288
|
+
|
289
|
+
has_stimulus_controller
|
290
|
+
has_stimulus_action "click", "handleClick"
|
291
|
+
has_stimulus_value "loading", -> { loading? }
|
292
|
+
|
293
|
+
def call
|
294
|
+
tag.button(**dom_attrs) do
|
295
|
+
content
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
```
|
300
|
+
|
301
|
+
### Phlex Example
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
class Card < Phlex::HTML
|
305
|
+
include HasStimulusAttrs
|
306
|
+
|
307
|
+
def self.controller_name
|
308
|
+
"card"
|
309
|
+
end
|
310
|
+
|
311
|
+
has_stimulus_controller
|
312
|
+
has_stimulus_action "mouseenter", "highlight"
|
313
|
+
has_stimulus_action "mouseleave", "unhighlight"
|
314
|
+
|
315
|
+
def template
|
316
|
+
div(**dom_attrs) do
|
317
|
+
yield
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
```
|
322
|
+
|
323
|
+
## Advanced Usage
|
324
|
+
|
325
|
+
### Conditional Controllers
|
326
|
+
|
327
|
+
```ruby
|
328
|
+
class ToggleComponent
|
329
|
+
include HasStimulusAttrs
|
330
|
+
|
331
|
+
has_stimulus_controller "toggle"
|
332
|
+
has_stimulus_controller "animation", if: :animated?
|
333
|
+
has_stimulus_controller "a11y", unless: :accessibility_disabled?
|
334
|
+
|
335
|
+
def animated?
|
336
|
+
@options[:animate] != false
|
337
|
+
end
|
338
|
+
|
339
|
+
def accessibility_disabled?
|
340
|
+
@options[:disable_a11y] == true
|
341
|
+
end
|
342
|
+
end
|
343
|
+
```
|
344
|
+
|
345
|
+
### Dynamic Controller Names
|
346
|
+
|
347
|
+
```ruby
|
348
|
+
class PolymorphicComponent
|
349
|
+
include HasStimulusAttrs
|
350
|
+
|
351
|
+
has_stimulus_controller -> { "#{record.class.name.underscore}-controller" }
|
352
|
+
has_stimulus_value "id", -> { record.id }
|
353
|
+
has_stimulus_value "type", -> { record.class.name }
|
354
|
+
end
|
355
|
+
```
|
356
|
+
|
357
|
+
### Complex Actions
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
class FormComponent
|
361
|
+
include HasStimulusAttrs
|
362
|
+
|
363
|
+
# Multiple actions on same event
|
364
|
+
has_stimulus_action "submit", "validate"
|
365
|
+
has_stimulus_action "submit", "save"
|
366
|
+
|
367
|
+
# Actions with modifiers
|
368
|
+
has_stimulus_action "keydown.enter", "submit"
|
369
|
+
has_stimulus_action "input->debounced:300", "search"
|
370
|
+
|
371
|
+
# Dynamic action based on state
|
372
|
+
has_stimulus_action "click", -> { draft? ? "saveDraft" : "publish" }
|
373
|
+
end
|
374
|
+
```
|
375
|
+
|
376
|
+
## Testing
|
377
|
+
|
378
|
+
### Testing Components with Stimulus Attrs
|
379
|
+
|
380
|
+
```ruby
|
381
|
+
class MyComponentTest < Minitest::Test
|
382
|
+
def test_stimulus_controller_included
|
383
|
+
component = MyComponent.new
|
384
|
+
assert_includes component.dom_data[:controller], "my-component"
|
385
|
+
end
|
386
|
+
|
387
|
+
def test_conditional_controller
|
388
|
+
component = MyComponent.new(active: true)
|
389
|
+
assert_includes component.dom_data[:controller], "active-state"
|
390
|
+
|
391
|
+
component = MyComponent.new(active: false)
|
392
|
+
refute_includes component.dom_data[:controller], "active-state"
|
393
|
+
end
|
394
|
+
|
395
|
+
def test_stimulus_values
|
396
|
+
component = MyComponent.new(user_id: 123)
|
397
|
+
assert_equal "123", component.dom_data["my-component-user-id-value"]
|
398
|
+
end
|
399
|
+
end
|
400
|
+
```
|
401
|
+
|
402
|
+
## Best Practices
|
403
|
+
|
404
|
+
### 1. Use Consistent Naming
|
405
|
+
|
406
|
+
```ruby
|
407
|
+
# Good: Consistent with Stimulus conventions
|
408
|
+
def self.controller_name
|
409
|
+
"user-profile" # kebab-case
|
410
|
+
end
|
411
|
+
|
412
|
+
# Avoid: Inconsistent naming
|
413
|
+
def self.controller_name
|
414
|
+
"UserProfile" # Wrong case
|
415
|
+
end
|
416
|
+
```
|
417
|
+
|
418
|
+
### 2. Keep Controllers Focused
|
419
|
+
|
420
|
+
```ruby
|
421
|
+
# Good: Single responsibility
|
422
|
+
class SearchComponent
|
423
|
+
has_stimulus_controller "search"
|
424
|
+
has_stimulus_action "input", "performSearch"
|
425
|
+
has_stimulus_value "endpoint", "/search"
|
426
|
+
end
|
427
|
+
|
428
|
+
# Avoid: Too many responsibilities
|
429
|
+
class KitchenSinkComponent
|
430
|
+
has_stimulus_controller "search"
|
431
|
+
has_stimulus_controller "modal"
|
432
|
+
has_stimulus_controller "dropdown"
|
433
|
+
# ... many more
|
434
|
+
end
|
435
|
+
```
|
436
|
+
|
437
|
+
### 3. Use Procs for Dynamic Values
|
438
|
+
|
439
|
+
```ruby
|
440
|
+
# Good: Dynamic value using Proc
|
441
|
+
has_stimulus_value "timestamp", -> { Time.current.to_i }
|
442
|
+
|
443
|
+
# Avoid: Static value that should be dynamic
|
444
|
+
has_stimulus_value "timestamp", Time.current.to_i # Set once at class load
|
445
|
+
```
|
446
|
+
|
447
|
+
### 4. Leverage Conditionals
|
448
|
+
|
449
|
+
```ruby
|
450
|
+
# Good: Conditional attributes for performance
|
451
|
+
has_stimulus_controller "animation", if: :animations_enabled?
|
452
|
+
has_stimulus_controller "analytics", unless: :private_mode?
|
453
|
+
|
454
|
+
# Avoid: Always including optional controllers
|
455
|
+
has_stimulus_controller "animation" # Even when not needed
|
456
|
+
```
|
457
|
+
|
458
|
+
## Common Pitfalls
|
459
|
+
|
460
|
+
### 1. Forgetting controller_name
|
461
|
+
|
462
|
+
```ruby
|
463
|
+
# Wrong: No controller_name defined
|
464
|
+
class MyComponent
|
465
|
+
include HasStimulusAttrs
|
466
|
+
has_stimulus_controller # Will raise NotImplementedError
|
467
|
+
end
|
468
|
+
|
469
|
+
# Correct: Define controller_name
|
470
|
+
class MyComponent
|
471
|
+
include HasStimulusAttrs
|
472
|
+
|
473
|
+
def self.controller_name
|
474
|
+
"my-component"
|
475
|
+
end
|
476
|
+
|
477
|
+
has_stimulus_controller
|
478
|
+
end
|
479
|
+
```
|
480
|
+
|
481
|
+
### 2. Incorrect Proc Usage
|
482
|
+
|
483
|
+
```ruby
|
484
|
+
# Wrong: Proc called at class definition
|
485
|
+
has_stimulus_value "random", -> { rand(100) }.call
|
486
|
+
|
487
|
+
# Correct: Proc called at runtime
|
488
|
+
has_stimulus_value "random", -> { rand(100) }
|
489
|
+
```
|
490
|
+
|
491
|
+
### 3. Naming Conflicts
|
492
|
+
|
493
|
+
```ruby
|
494
|
+
# Be careful with multiple controllers
|
495
|
+
has_stimulus_target "button" # For default controller
|
496
|
+
has_stimulus_target "button", controller: "modal" # Different target!
|
497
|
+
```
|
498
|
+
|
499
|
+
## Performance Considerations
|
500
|
+
|
501
|
+
HasStimulusAttrs includes several built-in performance optimizations:
|
502
|
+
|
503
|
+
### Built-in Optimizations
|
504
|
+
|
505
|
+
1. **Automatic Memoization**: `dom_data` is automatically cached after first computation
|
506
|
+
2. **Early Conditional Exit**: Expensive Procs are skipped when `:if`/`:unless` conditions fail
|
507
|
+
3. **Controller Name Caching**: Instance-level caching avoids repeated class method calls
|
508
|
+
4. **Lazy Evaluation**: Procs are only evaluated when `dom_data` is called
|
509
|
+
|
510
|
+
### Performance Methods
|
511
|
+
|
512
|
+
#### `reset_dom_data_cache!`
|
513
|
+
|
514
|
+
Manually clear the cached `dom_data` when component state changes:
|
515
|
+
|
516
|
+
```ruby
|
517
|
+
class DynamicComponent
|
518
|
+
include HasStimulusAttrs
|
519
|
+
|
520
|
+
attr_accessor :theme
|
521
|
+
|
522
|
+
def self.controller_name
|
523
|
+
"dynamic"
|
524
|
+
end
|
525
|
+
|
526
|
+
has_stimulus_value "theme", -> { theme }
|
527
|
+
|
528
|
+
def theme=(new_theme)
|
529
|
+
@theme = new_theme
|
530
|
+
reset_dom_data_cache! # Clear cache when state changes
|
531
|
+
end
|
532
|
+
end
|
533
|
+
```
|
534
|
+
|
535
|
+
### Optimization Best Practices
|
536
|
+
|
537
|
+
1. **Use Conditional Attributes**: Leverage `:if`/`:unless` for expensive operations
|
538
|
+
2. **Cache External Data**: Pre-fetch expensive data rather than computing in Procs
|
539
|
+
3. **Reset Cache Appropriately**: Call `reset_dom_data_cache!` only when component state changes
|
540
|
+
|
541
|
+
```ruby
|
542
|
+
class OptimizedComponent
|
543
|
+
include HasStimulusAttrs
|
544
|
+
|
545
|
+
# These are automatically optimized:
|
546
|
+
has_stimulus_controller "rich-text-editor", if: :rich_text_enabled?
|
547
|
+
has_stimulus_controller "syntax-highlighter", if: :code_blocks_present?
|
548
|
+
has_stimulus_value "config", -> { expensive_config_computation }
|
549
|
+
|
550
|
+
private
|
551
|
+
|
552
|
+
def expensive_config_computation
|
553
|
+
# This will only run once per component instance
|
554
|
+
# unless reset_dom_data_cache! is called
|
555
|
+
complex_calculation
|
556
|
+
end
|
557
|
+
end
|
558
|
+
```
|
559
|
+
|
560
|
+
### Performance Impact
|
561
|
+
|
562
|
+
- **Memoized calls**: No Proc re-evaluation on subsequent `dom_data` calls
|
563
|
+
- **Conditional skipping**: Expensive operations avoided when conditions aren't met
|
564
|
+
- **Cached controller names**: Single class method call per instance
|
565
|
+
- **Memory efficient**: Cache cleared automatically when component is garbage collected
|
566
|
+
|
567
|
+
## Debugging Tips
|
568
|
+
|
569
|
+
### 1. Inspect Generated Attributes
|
570
|
+
|
571
|
+
```ruby
|
572
|
+
component = MyComponent.new
|
573
|
+
puts component.dom_data.inspect
|
574
|
+
# => {:controller=>"my-component", :action=>"click->my-component#handleClick", ...}
|
575
|
+
```
|
576
|
+
|
577
|
+
### 2. Check Formatted Output
|
578
|
+
|
579
|
+
```ruby
|
580
|
+
# In Rails console or tests
|
581
|
+
component = MyComponent.new
|
582
|
+
component.dom_data.each do |key, value|
|
583
|
+
puts "data-#{key}=\"#{value}\""
|
584
|
+
end
|
585
|
+
```
|
586
|
+
|
587
|
+
### 3. Verify in Browser
|
588
|
+
|
589
|
+
Use browser developer tools to inspect the generated HTML and ensure Stimulus attributes are correct.
|
590
|
+
|
591
|
+
## Version History
|
592
|
+
|
593
|
+
- **0.3.0** (Unreleased): Major performance optimizations
|
594
|
+
- Automatic `dom_data` memoization to prevent expensive Proc re-evaluation
|
595
|
+
- Early conditional exit optimization for `:if`/`:unless` attributes
|
596
|
+
- Controller name instance-level caching
|
597
|
+
- Added `reset_dom_data_cache!` method for manual cache management
|
598
|
+
- **0.2.2** (2025-01-31): Added support for Proc in `has_stimulus_action`
|
599
|
+
- **0.2.0** (2023-03-22): Added Proc support for controller option
|
600
|
+
- **0.1.0**: Initial release
|
601
|
+
|
602
|
+
## Contributing
|
603
|
+
|
604
|
+
The gem is open source and welcomes contributions. Key areas for contribution:
|
605
|
+
1. Additional stimulus attribute types
|
606
|
+
2. Performance improvements
|
607
|
+
3. Documentation and examples
|
608
|
+
4. Integration guides for different frameworks
|
609
|
+
|
610
|
+
## Conclusion
|
611
|
+
|
612
|
+
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.
|
data/README.md
CHANGED
@@ -2,19 +2,15 @@
|
|
2
2
|
|
3
3
|
[](https://github.com/tomasc/has_stimulus_attrs/actions/workflows/ruby.yml)
|
4
4
|
|
5
|
-
|
5
|
+
A Ruby DSL for managing [Stimulus.js](https://stimulus.hotwired.dev/) data attributes in component-based architectures.
|
6
6
|
|
7
|
-
|
7
|
+
Built on [`has_dom_attrs`](https://github.com/tomasc/has_dom_attrs) and [`stimulus_helpers`](https://github.com/tomasc/stimulus_helpers).
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
|
-
|
17
|
-
$ gem install has_stimulus_attrs
|
11
|
+
```bash
|
12
|
+
bundle add has_stimulus_attrs
|
13
|
+
```
|
18
14
|
|
19
15
|
## Usage
|
20
16
|
|
@@ -48,8 +44,7 @@ DetailsComponent.controller_name
|
|
48
44
|
# => "details-component"
|
49
45
|
```
|
50
46
|
|
51
|
-
|
52
|
-
your class:
|
47
|
+
Use the DSL methods to define Stimulus attributes:
|
53
48
|
|
54
49
|
```ruby
|
55
50
|
class ModalComponent < ApplicationComponent
|
@@ -87,9 +82,10 @@ end
|
|
87
82
|
|
88
83
|
## Development
|
89
84
|
|
90
|
-
|
91
|
-
|
92
|
-
|
85
|
+
```bash
|
86
|
+
bin/setup # Install dependencies
|
87
|
+
bin/console # Interactive prompt
|
88
|
+
```
|
93
89
|
|
94
90
|
## Contributing
|
95
91
|
|
data/lib/has_stimulus_attrs.rb
CHANGED
@@ -10,6 +10,10 @@ module HasStimulusAttrs
|
|
10
10
|
include HasDomAttrs
|
11
11
|
include StimulusHelpers
|
12
12
|
|
13
|
+
def controller_name
|
14
|
+
@_stimulus_controller_name ||= self.class.controller_name
|
15
|
+
end
|
16
|
+
|
13
17
|
class << self
|
14
18
|
def included(base)
|
15
19
|
base.extend ClassMethods
|
@@ -136,23 +140,32 @@ module HasStimulusAttrs
|
|
136
140
|
|
137
141
|
private
|
138
142
|
def prepend___has_stimulus___method(key, value, **options)
|
143
|
+
# First, add the stimulus attribute module
|
139
144
|
prepend(
|
140
145
|
Module.new do
|
141
146
|
define_method :dom_data do
|
142
|
-
|
143
|
-
|
147
|
+
# Early exit for conditional attributes - avoid expensive key/value evaluation
|
148
|
+
if options.key?(:if)
|
149
|
+
cond = options[:if]
|
150
|
+
cond_value = case cond
|
144
151
|
when Proc then instance_exec(&cond)
|
145
152
|
when Symbol, String then send(cond)
|
146
|
-
|
147
|
-
|
148
|
-
if cond && options.key?(:if)
|
153
|
+
else cond
|
154
|
+
end
|
149
155
|
return super() unless cond_value
|
150
156
|
end
|
151
157
|
|
152
|
-
if
|
158
|
+
if options.key?(:unless)
|
159
|
+
cond = options[:unless]
|
160
|
+
cond_value = case cond
|
161
|
+
when Proc then instance_exec(&cond)
|
162
|
+
when Symbol, String then send(cond)
|
163
|
+
else cond
|
164
|
+
end
|
153
165
|
return super() if cond_value
|
154
166
|
end
|
155
167
|
|
168
|
+
# Only evaluate key and value if conditions pass
|
156
169
|
k = case key
|
157
170
|
when Proc then instance_exec(&key)
|
158
171
|
else key
|
@@ -169,6 +182,32 @@ module HasStimulusAttrs
|
|
169
182
|
end
|
170
183
|
end
|
171
184
|
)
|
185
|
+
|
186
|
+
# Then, ensure memoization is always at the top
|
187
|
+
ensure_memoization_at_top
|
188
|
+
end
|
189
|
+
|
190
|
+
def ensure_memoization_at_top
|
191
|
+
# Remove any existing memoization module
|
192
|
+
if const_defined?(:StimulusMemoization, false)
|
193
|
+
remove_const(:StimulusMemoization)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Create a new memoization module at the top
|
197
|
+
memoization_module = Module.new do
|
198
|
+
def dom_data
|
199
|
+
return @_stimulus_dom_data if defined?(@_stimulus_dom_data)
|
200
|
+
@_stimulus_dom_data = super
|
201
|
+
end
|
202
|
+
|
203
|
+
def reset_dom_data_cache!
|
204
|
+
remove_instance_variable(:@_stimulus_dom_data) if defined?(@_stimulus_dom_data)
|
205
|
+
super if defined?(super)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
const_set(:StimulusMemoization, memoization_module)
|
210
|
+
prepend(memoization_module)
|
172
211
|
end
|
173
212
|
end
|
174
213
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: has_stimulus_attrs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tomas Celizna
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2025-
|
12
|
+
date: 2025-06-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: stimulus_helpers
|
@@ -134,6 +134,7 @@ files:
|
|
134
134
|
- ".rubocop.yml"
|
135
135
|
- ".ruby-version"
|
136
136
|
- CHANGELOG.md
|
137
|
+
- CLAUDE.md
|
137
138
|
- Gemfile
|
138
139
|
- LICENSE
|
139
140
|
- README.md
|