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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a3a965811c25f20f220e33a4d7e579081309e45501235c168238b8ed83db4b3
4
- data.tar.gz: '0804e6cd1db9b7399bb70ec14e3df96b78a67bd84a92d6d799d39709924923cb'
3
+ metadata.gz: 0d92df61326364a67fd89516850b87e5dfe9e85cbbc1982f6f5815978f033c18
4
+ data.tar.gz: d7cfcb016ff83d8aa53c43527cb6753b587958e075f071f0261cf16044365ac1
5
5
  SHA512:
6
- metadata.gz: 850a00e3ee81c9783f59711efdd2045b4405225dc424759c6f8a5071526187aa588f1e1136cd572ddaef16dc9b1134142eeaef4c17a69e5726eaa3e35a0d8a69
7
- data.tar.gz: 9e81d373052c1861e0304beb023ce2b0ed0e345bcd844a7afcba10fe7dec2c9cdb7173fba46893339d83102b247f0bd2da56ea4c61484a820561f60675951ef8
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
  [![HasStimulusAttrs](https://github.com/tomasc/has_stimulus_attrs/actions/workflows/ruby.yml/badge.svg)](https://github.com/tomasc/has_stimulus_attrs/actions/workflows/ruby.yml)
4
4
 
5
- Helper methods for dealing with [stimulus](https://stimulus.hotwired.dev/) data attributes.
5
+ A Ruby DSL for managing [Stimulus.js](https://stimulus.hotwired.dev/) data attributes in component-based architectures.
6
6
 
7
- Relies on [`has_dom_attrs`](https://github.com/tomasc/has_dom_attrs) and [`stimulus_helpers`](https://github.com/tomasc/stimulus_helpers).
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
- Install the gem and add to the application's Gemfile by executing:
12
-
13
- $ bundle add has_stimulus_attrs
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
- You can then use the included class methods to easily set stimulus attributes on
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
- After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
91
-
92
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
85
+ ```bash
86
+ bin/setup # Install dependencies
87
+ bin/console # Interactive prompt
88
+ ```
93
89
 
94
90
  ## Contributing
95
91
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HasStimulusAttrs
4
- VERSION = "0.2.2"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -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
- cond = options[:if] || options[:unless]
143
- cond_value = case cond
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
- end
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 cond && options.key?(:unless)
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.2.2
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-01-31 00:00:00.000000000 Z
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