pom-component 1.0.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.
data/README.md ADDED
@@ -0,0 +1,1037 @@
1
+ # Pom Component
2
+
3
+ A UI component toolkit for Rails with Tailwind CSS integration. Pom provides a powerful base class for building reusable ViewComponents with advanced features including option management, style composition, and Stimulus.js integration.
4
+
5
+ ## Features
6
+
7
+ - 🎨 **Styleable DSL** - Compose Tailwind CSS classes with automatic conflict resolution
8
+ - ⚙️ **Option DSL** - Define component options with enums, defaults, and validation
9
+ - 🎯 **Type Safety** - Enum validation and required option enforcement
10
+ - 🔄 **Inheritance** - Full support for component inheritance with style and option merging
11
+ - ⚡ **Stimulus Integration** - Built-in helpers for Stimulus.js data attributes
12
+ - 🧩 **Flexible** - Capture extra options and merge HTML attributes intelligently
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'pom-component'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ gem install pom-component
32
+ ```
33
+
34
+ ## Requirements
35
+
36
+ - Ruby >= 3.2.0
37
+ - Rails >= 7.1.0
38
+ - ViewComponent >= 4.0
39
+
40
+ ## Quick Start
41
+
42
+ Create your first component by inheriting from `Pom::Component`:
43
+
44
+ ```ruby
45
+ # app/components/pom/button_component.rb
46
+ module Pom
47
+ class ButtonComponent < Pom::Component
48
+ option :variant, enums: [:primary, :secondary, :danger], default: :primary
49
+ option :size, enums: [:sm, :md, :lg], default: :md
50
+ option :disabled, default: false
51
+
52
+ define_styles(
53
+ base: "inline-flex items-center justify-center font-medium rounded transition",
54
+ variant: {
55
+ primary: "bg-blue-600 text-white hover:bg-blue-700",
56
+ secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
57
+ danger: "bg-red-600 text-white hover:bg-red-700"
58
+ },
59
+ size: {
60
+ sm: "px-3 py-1.5 text-sm",
61
+ md: "px-4 py-2 text-base",
62
+ lg: "px-6 py-3 text-lg"
63
+ },
64
+ disabled: {
65
+ true: "opacity-50 cursor-not-allowed pointer-events-none",
66
+ false: "cursor-pointer"
67
+ }
68
+ )
69
+
70
+ def call
71
+ content_tag :button, content, **html_options
72
+ end
73
+
74
+ private
75
+
76
+ def html_options
77
+ merge_options(
78
+ { class: styles_for(variant: variant, size: size, disabled: disabled) },
79
+ extra_options
80
+ )
81
+ end
82
+ end
83
+ end
84
+ ```
85
+
86
+ Use it in your views:
87
+
88
+ ```erb
89
+ <%# app/views/pages/index.html.erb %>
90
+ <%= render Pom::ButtonComponent.new(variant: :primary, size: :lg) do %>
91
+ Click me!
92
+ <% end %>
93
+ ```
94
+
95
+ Or using the helper method (component must be in the `Pom::` namespace):
96
+
97
+ ```erb
98
+ <%# This looks for Pom::ButtonComponent %>
99
+ <%= pom_button(variant: :danger, disabled: true) do %>
100
+ Delete
101
+ <% end %>
102
+ ```
103
+
104
+ **Note:** The `pom_*` helper methods only work with components defined in the `Pom::` namespace. See [Configuration](#configuration) to learn how to add custom prefixes for other namespaces.
105
+
106
+ ## Component Crafting Guide
107
+
108
+ For comprehensive examples and best practices on building components from basic to complex compositions, see the [Component Crafting Guide](COMPONENT_CRAFTING.md).
109
+
110
+ ## Option DSL
111
+
112
+ The Option DSL provides a declarative way to define component options with validation, defaults, and type safety.
113
+
114
+ ### Basic Usage
115
+
116
+ Define options using the `option` class method:
117
+
118
+ ```ruby
119
+ class CardComponent < Pom::Component
120
+ option :title
121
+ option :variant, enums: [:default, :bordered, :elevated]
122
+ option :padding, default: :md
123
+ end
124
+ ```
125
+
126
+ ### Option Parameters
127
+
128
+ #### `enums:`
129
+
130
+ Restrict option values to a specific set:
131
+
132
+ ```ruby
133
+ option :size, enums: [:sm, :md, :lg]
134
+ ```
135
+
136
+ This will:
137
+
138
+ - Validate values on initialization and when using setters
139
+ - Accept both symbols and strings (automatically converted to symbols)
140
+ - Raise `ArgumentError` for invalid values
141
+
142
+ ```ruby
143
+ # Valid
144
+ component = MyComponent.new(size: :md)
145
+ component = MyComponent.new(size: "lg")
146
+
147
+ # Invalid - raises ArgumentError
148
+ component = MyComponent.new(size: :xl)
149
+ ```
150
+
151
+ #### `default:`
152
+
153
+ Provide a default value when the option is not specified:
154
+
155
+ ```ruby
156
+ option :color, default: :blue
157
+ option :count, default: 0
158
+ option :timestamp, default: -> { Time.current }
159
+ ```
160
+
161
+ Defaults can be:
162
+
163
+ - **Static values**: Strings, symbols, numbers, booleans
164
+ - **Procs/Lambdas**: Called at runtime for dynamic defaults
165
+
166
+ ```ruby
167
+ class TimestampComponent < Pom::Component
168
+ option :created_at, default: -> { Time.current }
169
+ option :format, default: "%Y-%m-%d"
170
+ end
171
+ ```
172
+
173
+ #### `required:`
174
+
175
+ Mark an option as required:
176
+
177
+ ```ruby
178
+ option :user_id, required: true
179
+ option :status, required: true, default: :active
180
+ ```
181
+
182
+ Notes:
183
+
184
+ - Required options without defaults must be provided during initialization
185
+ - Required options with defaults don't raise errors (the default satisfies the requirement)
186
+ - Missing required options raise `ArgumentError`
187
+
188
+ ```ruby
189
+ class UserCardComponent < Pom::Component
190
+ option :name, required: true
191
+ option :email, required: true
192
+ option :role, required: true, default: :member
193
+ end
194
+
195
+ # Valid
196
+ UserCardComponent.new(name: "John", email: "john@example.com")
197
+
198
+ # Invalid - raises ArgumentError: Missing required option: name
199
+ UserCardComponent.new(email: "john@example.com")
200
+ ```
201
+
202
+ ### Generated Methods
203
+
204
+ For each option, three methods are automatically generated:
205
+
206
+ #### Getter Method
207
+
208
+ ```ruby
209
+ component.variant # => :primary
210
+ ```
211
+
212
+ #### Setter Method (with validation)
213
+
214
+ ```ruby
215
+ component.variant = :secondary
216
+ component.size = :invalid # => ArgumentError if enums are defined
217
+ ```
218
+
219
+ #### Predicate Method
220
+
221
+ ```ruby
222
+ component.variant? # => true if variant is present
223
+ component.title? # => false if title is nil or empty
224
+ ```
225
+
226
+ ### Extra Options
227
+
228
+ Any options not explicitly defined are captured in `extra_options`:
229
+
230
+ ```ruby
231
+ class MyComponent < Pom::Component
232
+ option :title
233
+ end
234
+
235
+ component = MyComponent.new(title: "Hello", data: { controller: "modal" }, id: "my-modal")
236
+
237
+ component.title # => "Hello"
238
+ component.extra_options # => { data: { controller: "modal" }, id: "my-modal" }
239
+ ```
240
+
241
+ This is useful for passing through HTML attributes:
242
+
243
+ ```ruby
244
+ def call
245
+ content_tag :div, content, **extra_options
246
+ end
247
+ ```
248
+
249
+ ### Class Methods
250
+
251
+ Query option metadata at the class level:
252
+
253
+ ```ruby
254
+ MyComponent.enum_values_for(:variant) # => [:primary, :secondary, :danger]
255
+ MyComponent.default_value_for(:size) # => :md
256
+ MyComponent.required_options # => [:user_id, :title]
257
+ MyComponent.optional_options # => [:variant, :size, :color]
258
+ ```
259
+
260
+ ### Instance Methods
261
+
262
+ Query and manipulate option values:
263
+
264
+ ```ruby
265
+ # Get all option values as a hash
266
+ component.option_values
267
+ # => { variant: :primary, size: :md, disabled: false }
268
+
269
+ # Check if an option was explicitly set
270
+ component.option_set?(:variant) # => true
271
+ component.option_set?(:size) # => false (using default)
272
+
273
+ # Reset an option to its default value
274
+ component.reset_option(:variant)
275
+ component.variant # => :primary (default)
276
+ ```
277
+
278
+ ### Inheritance
279
+
280
+ Options are inherited and can be extended:
281
+
282
+ ```ruby
283
+ class BaseButton < Pom::Component
284
+ option :size, enums: [:sm, :md, :lg], default: :md
285
+ option :disabled, default: false
286
+ end
287
+
288
+ class IconButton < BaseButton
289
+ option :icon, required: true
290
+ option :icon_position, enums: [:left, :right], default: :left
291
+ end
292
+
293
+ # IconButton has all options: size, disabled, icon, icon_position
294
+ button = IconButton.new(icon: "star", size: :lg)
295
+ ```
296
+
297
+ ## Styleable
298
+
299
+ The Styleable module provides a powerful DSL for composing Tailwind CSS classes with automatic conflict resolution using the `tailwind_merge` gem.
300
+
301
+ ### Basic Usage
302
+
303
+ Define styles using the `define_styles` class method:
304
+
305
+ ```ruby
306
+ class AlertComponent < Pom::Component
307
+ option :variant, enums: [:info, :success, :warning, :error], default: :info
308
+
309
+ define_styles(
310
+ base: "p-4 rounded-lg border",
311
+ variant: {
312
+ info: "bg-blue-50 border-blue-200 text-blue-800",
313
+ success: "bg-green-50 border-green-200 text-green-800",
314
+ warning: "bg-yellow-50 border-yellow-200 text-yellow-800",
315
+ error: "bg-red-50 border-red-200 text-red-800"
316
+ }
317
+ )
318
+
319
+ def call
320
+ content_tag :div, content, class: styles_for(variant: variant)
321
+ end
322
+ end
323
+ ```
324
+
325
+ ### Style Structure
326
+
327
+ Styles are organized into **keys** that map to option values:
328
+
329
+ ```ruby
330
+ define_styles(
331
+ base: "always-applied-classes",
332
+ option_name: {
333
+ option_value_1: "classes-for-value-1",
334
+ option_value_2: "classes-for-value-2"
335
+ }
336
+ )
337
+ ```
338
+
339
+ #### Base Styles
340
+
341
+ Base styles are always applied:
342
+
343
+ ```ruby
344
+ define_styles(
345
+ base: "font-sans antialiased"
346
+ )
347
+ ```
348
+
349
+ Base styles can also be a hash for organization:
350
+
351
+ ```ruby
352
+ define_styles(
353
+ base: {
354
+ default: "component rounded-lg",
355
+ hover: "hover:shadow-md",
356
+ focus: "focus:ring-2 focus:ring-blue-500"
357
+ }
358
+ )
359
+ ```
360
+
361
+ All values in a hash are concatenated and applied.
362
+
363
+ #### Variant Styles
364
+
365
+ Map option values to specific classes:
366
+
367
+ ```ruby
368
+ define_styles(
369
+ variant: {
370
+ solid: "bg-blue-600 text-white",
371
+ outline: "border-2 border-blue-600 text-blue-600",
372
+ ghost: "text-blue-600 hover:bg-blue-50"
373
+ }
374
+ )
375
+ ```
376
+
377
+ ### Using styles_for
378
+
379
+ Generate the class string using `styles_for`:
380
+
381
+ ```ruby
382
+ def call
383
+ content_tag :div, content, class: styles_for(variant: variant, size: size)
384
+ end
385
+ ```
386
+
387
+ The method:
388
+
389
+ 1. Applies base styles
390
+ 2. Resolves each provided option against style definitions
391
+ 3. Concatenates all matching classes
392
+ 4. Uses `tailwind_merge` to resolve conflicts
393
+
394
+ **Only the options you pass to `styles_for` will be applied:**
395
+
396
+ ```ruby
397
+ # Only applies base and variant styles
398
+ styles_for(variant: :primary)
399
+
400
+ # Applies base, variant, and size styles
401
+ styles_for(variant: :primary, size: :lg)
402
+ ```
403
+
404
+ ### Boolean Style Keys
405
+
406
+ Handle boolean options elegantly:
407
+
408
+ ```ruby
409
+ class ButtonComponent < Pom::Component
410
+ option :disabled, default: false
411
+ option :loading, default: false
412
+
413
+ define_styles(
414
+ base: "btn",
415
+ disabled: {
416
+ true: "opacity-50 cursor-not-allowed pointer-events-none",
417
+ false: "cursor-pointer hover:opacity-90"
418
+ },
419
+ loading: {
420
+ true: "animate-pulse",
421
+ false: ""
422
+ }
423
+ )
424
+
425
+ def call
426
+ content_tag :button, content, class: styles_for(disabled: disabled, loading: loading)
427
+ end
428
+ end
429
+ ```
430
+
431
+ ### Dynamic Styles with Lambdas
432
+
433
+ Use lambdas for dynamic style computation based on component state:
434
+
435
+ ```ruby
436
+ class BadgeComponent < Pom::Component
437
+ option :variant, enums: [:solid, :outline], default: :solid
438
+ option :color, enums: [:blue, :green, :red, :yellow], default: :blue
439
+
440
+ define_styles(
441
+ base: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
442
+ variant: {
443
+ solid: ->(color: :blue, **_opts) {
444
+ case color
445
+ when :blue then "bg-blue-100 text-blue-800"
446
+ when :green then "bg-green-100 text-green-800"
447
+ when :red then "bg-red-100 text-red-800"
448
+ when :yellow then "bg-yellow-100 text-yellow-800"
449
+ end
450
+ },
451
+ outline: ->(color: :blue, **_opts) {
452
+ case color
453
+ when :blue then "border border-blue-300 text-blue-700"
454
+ when :green then "border border-green-300 text-green-700"
455
+ when :red then "border border-red-300 text-red-700"
456
+ when :yellow then "border border-yellow-300 text-yellow-700"
457
+ end
458
+ }
459
+ }
460
+ )
461
+
462
+ def call
463
+ content_tag :span, content, class: styles_for(variant: variant, color: color)
464
+ end
465
+ end
466
+
467
+ # Usage
468
+ <%= render BadgeComponent.new(variant: :solid, color: :green) { "Active" } %>
469
+ ```
470
+
471
+ **Important:** Always use full Tailwind CSS class names, not string interpolation. Tailwind's JIT compiler needs to see complete class names to generate the CSS.
472
+
473
+ **How Lambda Parameters Work:**
474
+
475
+ Lambda styles receive all options passed to `styles_for` as keyword arguments. This includes both the matching key (e.g., `variant: :solid`) and any additional arbitrary parameters you provide:
476
+
477
+ ```ruby
478
+ # The lambda for variant: :solid receives ALL parameters
479
+ styles_for(variant: :solid, color: :green, size: :lg)
480
+ # Lambda receives: { variant: :solid, color: :green, size: :lg }
481
+
482
+ # Even custom parameters are passed through
483
+ styles_for(variant: :outline, foo: "bar", baz: 123)
484
+ # Lambda receives: { variant: :outline, foo: "bar", baz: 123 }
485
+ ```
486
+
487
+ This allows lambdas to compute styles based on multiple option combinations.
488
+
489
+ ### Lambda Base Styles
490
+
491
+ Base styles can also be lambdas:
492
+
493
+ ```ruby
494
+ define_styles(
495
+ base: ->(disabled: false, **_opts) {
496
+ classes = ["component rounded-lg transition"]
497
+ classes << "opacity-50" if disabled
498
+ classes.join(" ")
499
+ },
500
+ variant: {
501
+ solid: "bg-blue-600 text-white",
502
+ outline: "border-2 border-blue-600"
503
+ }
504
+ )
505
+ ```
506
+
507
+ ### Style Groups
508
+
509
+ Organize styles for different parts of your component:
510
+
511
+ ```ruby
512
+ class ModalComponent < Pom::Component
513
+ option :size, enums: [:sm, :md, :lg], default: :md
514
+
515
+ define_styles(:overlay, base: "fixed inset-0 bg-black bg-opacity-50")
516
+
517
+ define_styles(:dialog,
518
+ base: "bg-white rounded-lg shadow-xl",
519
+ size: {
520
+ sm: "max-w-sm",
521
+ md: "max-w-md",
522
+ lg: "max-w-lg"
523
+ }
524
+ )
525
+
526
+ define_styles(:header, base: "px-6 py-4 border-b")
527
+ define_styles(:body, base: "px-6 py-4")
528
+ define_styles(:footer, base: "px-6 py-4 border-t bg-gray-50")
529
+
530
+ def call
531
+ content_tag :div, class: styles_for(:overlay) do
532
+ content_tag :div, class: styles_for(:dialog, size: size) do
533
+ concat content_tag(:div, header_content, class: styles_for(:header))
534
+ concat content_tag(:div, body_content, class: styles_for(:body))
535
+ concat content_tag(:div, footer_content, class: styles_for(:footer))
536
+ end
537
+ end
538
+ end
539
+ end
540
+ ```
541
+
542
+ Access group styles:
543
+
544
+ ```ruby
545
+ styles_for(:group_name, option1: value1, option2: value2)
546
+ ```
547
+
548
+ ### Tailwind Merge
549
+
550
+ Pom uses `tailwind_merge` to intelligently resolve conflicting Tailwind classes:
551
+
552
+ ```ruby
553
+ # Later classes override earlier ones for the same property
554
+ define_styles(
555
+ base: "p-4 bg-blue-500",
556
+ variant: {
557
+ danger: "p-6 bg-red-500" # p-6 overrides p-4, bg-red-500 overrides bg-blue-500
558
+ }
559
+ )
560
+
561
+ styles_for(variant: :danger)
562
+ # => "bg-red-500 p-6"
563
+ ```
564
+
565
+ This ensures that:
566
+
567
+ - Only the most specific class is applied
568
+ - No duplicate or conflicting utilities
569
+ - Predictable style precedence
570
+
571
+ ### Inheritance
572
+
573
+ Styles are inherited and merged:
574
+
575
+ ```ruby
576
+ class BaseButton < Pom::Component
577
+ option :size, enums: [:sm, :md, :lg], default: :md
578
+
579
+ define_styles(
580
+ base: "inline-flex items-center justify-center font-medium rounded",
581
+ size: {
582
+ sm: "px-3 py-1.5 text-sm",
583
+ md: "px-4 py-2 text-base",
584
+ lg: "px-6 py-3 text-lg"
585
+ }
586
+ )
587
+ end
588
+
589
+ class PrimaryButton < BaseButton
590
+ option :variant, enums: [:solid, :outline], default: :solid
591
+
592
+ define_styles(
593
+ base: "transition-colors duration-200", # Merged with parent base
594
+ variant: {
595
+ solid: "bg-blue-600 text-white hover:bg-blue-700",
596
+ outline: "border-2 border-blue-600 text-blue-600 hover:bg-blue-50"
597
+ },
598
+ size: {
599
+ lg: "px-8 py-4 text-xl" # Overrides parent's lg size
600
+ }
601
+ )
602
+ end
603
+ ```
604
+
605
+ Child styles:
606
+
607
+ - Merge with parent styles for the same keys
608
+ - Override parent values when there's a conflict
609
+ - Add new style keys and variants
610
+
611
+ ## Helpers
612
+
613
+ ### OptionHelper
614
+
615
+ Intelligently merge option hashes:
616
+
617
+ ```ruby
618
+ def html_options
619
+ merge_options(
620
+ { class: base_classes, data: { controller: "dropdown" } },
621
+ { class: variant_classes, data: { action: "click->dropdown#toggle" } },
622
+ extra_options
623
+ )
624
+ end
625
+
626
+ # Result:
627
+ # {
628
+ # class: "merged-classes", # Uses tailwind_merge
629
+ # data: {
630
+ # controller: "dropdown",
631
+ # action: "click->dropdown#toggle"
632
+ # }
633
+ # }
634
+ ```
635
+
636
+ Special handling for:
637
+
638
+ - **`:class`**: Merged using `tailwind_merge`
639
+ - **`:data`**: Deep merged with concatenation for `controller` and `action`
640
+ - Other keys: Last value wins
641
+
642
+ ### ViewHelper
643
+
644
+ Render Pom components using helper methods:
645
+
646
+ ```ruby
647
+ # Instead of:
648
+ <%= render Pom::ButtonComponent.new(variant: :primary) { "Click" } %>
649
+
650
+ # Use:
651
+ <%= pom_button(variant: :primary) { "Click" } %>
652
+ ```
653
+
654
+ The helper automatically converts `pom_component_name` to `Pom::ComponentNameComponent`.
655
+
656
+ **Important:** By default, this only works for components defined in the `Pom::` namespace:
657
+
658
+ ```ruby
659
+ # pom_button looks for Pom::ButtonComponent
660
+ module Pom
661
+ class ButtonComponent < Pom::Component
662
+ # ...
663
+ end
664
+ end
665
+ ```
666
+
667
+ To use helper methods with other namespaces (e.g., `ui_card`, `admin_dashboard`), see the [Configuration](#configuration) section to learn how to add custom prefixes.
668
+
669
+ If your components are NOT in a configured namespace, use the regular `render` helper:
670
+
671
+ ```ruby
672
+ # For components outside configured namespaces
673
+ <%= render ButtonComponent.new(variant: :primary) { "Click" } %>
674
+ ```
675
+
676
+ ### StimulusHelper
677
+
678
+ Generate Stimulus data attributes:
679
+
680
+ ```ruby
681
+ class DropdownComponent < Pom::Component
682
+ def stimulus
683
+ "dropdown"
684
+ end
685
+
686
+ def button_options
687
+ merge_options(
688
+ stimulus_target(:button),
689
+ stimulus_action({ click: :toggle }),
690
+ { class: "btn" }
691
+ )
692
+ end
693
+
694
+ def menu_options
695
+ merge_options(
696
+ stimulus_target(:menu),
697
+ stimulus_class(:open, "block"),
698
+ { class: "dropdown-menu" }
699
+ )
700
+ end
701
+ end
702
+ ```
703
+
704
+ Available helpers:
705
+
706
+ #### `stimulus_target(name, stimulus: nil)`
707
+
708
+ ```ruby
709
+ stimulus_target(:menu)
710
+ # => { "data-dropdown-target" => "menu" }
711
+
712
+ stimulus_target([:menu, :item])
713
+ # => { "data-dropdown-target" => "menu item" }
714
+
715
+ stimulus_target(:button, stimulus: "modal")
716
+ # => { "data-modal-target" => "button" }
717
+ ```
718
+
719
+ #### `stimulus_action(action_map, stimulus: nil)`
720
+
721
+ ```ruby
722
+ stimulus_action(:toggle)
723
+ # => { "data-action" => "dropdown#toggle" }
724
+
725
+ stimulus_action({ click: :toggle, mouseenter: :show })
726
+ # => { "data-action" => "click->dropdown#toggle mouseenter->dropdown#show" }
727
+
728
+ stimulus_action(:open, stimulus: "modal")
729
+ # => { "data-action" => "modal#open" }
730
+ ```
731
+
732
+ #### `stimulus_value(name, value, stimulus: nil)`
733
+
734
+ ```ruby
735
+ stimulus_value(:open, false)
736
+ # => { "data-dropdown-open-value" => false }
737
+
738
+ stimulus_value(:items, ["a", "b", "c"])
739
+ # => { "data-dropdown-items-value" => "[\"a\",\"b\",\"c\"]" }
740
+
741
+ stimulus_value(:count, 5, stimulus: "counter")
742
+ # => { "data-counter-count-value" => 5 }
743
+ ```
744
+
745
+ #### `stimulus_class(name, value, stimulus: nil)`
746
+
747
+ ```ruby
748
+ stimulus_class(:open, "block")
749
+ # => { "data-dropdown-open-class" => "block" }
750
+
751
+ stimulus_class(:hidden, "hidden", stimulus: "modal")
752
+ # => { "data-modal-hidden-class" => "hidden" }
753
+ ```
754
+
755
+ #### `stimulus_controller`
756
+
757
+ Returns the dasherized controller name (requires a `stimulus` method):
758
+
759
+ ```ruby
760
+ def stimulus
761
+ "dropdown"
762
+ end
763
+
764
+ stimulus_controller # => "dropdown"
765
+ ```
766
+
767
+ ## Component Utilities
768
+
769
+ ### Component Name and ID
770
+
771
+ Auto-generated component identifiers:
772
+
773
+ ```ruby
774
+ class UserCardComponent < Pom::Component
775
+ def call
776
+ content_tag :div, content, id: auto_id, data: { component: component_name }
777
+ end
778
+ end
779
+
780
+ component = UserCardComponent.new
781
+ component.component_name # => "user-card"
782
+ component.auto_id # => "user-card-a3f2"
783
+ component.uid # => "a3f2" (unique 4-char hex)
784
+ ```
785
+
786
+ ## Complete Example
787
+
788
+ Here's a comprehensive example combining all features:
789
+
790
+ ```ruby
791
+ # app/components/pom/card_component.rb
792
+ module Pom
793
+ class CardComponent < Pom::Component
794
+ option :variant, enums: [:default, :bordered, :elevated], default: :default
795
+ option :padding, enums: [:none, :sm, :md, :lg], default: :md
796
+ option :clickable, default: false
797
+ option :href
798
+
799
+ define_styles(:container,
800
+ base: "bg-white rounded-lg overflow-hidden",
801
+ variant: {
802
+ default: "border border-gray-200",
803
+ bordered: "border-2 border-gray-900",
804
+ elevated: "shadow-lg"
805
+ },
806
+ clickable: {
807
+ true: "cursor-pointer transition hover:shadow-xl",
808
+ false: ""
809
+ }
810
+ )
811
+
812
+ define_styles(:body,
813
+ padding: {
814
+ none: "",
815
+ sm: "p-3",
816
+ md: "p-6",
817
+ lg: "p-8"
818
+ }
819
+ )
820
+
821
+ def call
822
+ if href.present?
823
+ link_to href, **container_options do
824
+ content_tag :div, content, class: styles_for(:body, padding: padding)
825
+ end
826
+ else
827
+ content_tag :div, **container_options do
828
+ content_tag :div, content, class: styles_for(:body, padding: padding)
829
+ end
830
+ end
831
+ end
832
+
833
+ private
834
+
835
+ def container_options
836
+ merge_options(
837
+ {
838
+ class: styles_for(:container, variant: variant, clickable: clickable || href?),
839
+ id: auto_id
840
+ },
841
+ extra_options
842
+ )
843
+ end
844
+ end
845
+ end
846
+ ```
847
+
848
+ Usage:
849
+
850
+ ```erb
851
+ <%= render Pom::CardComponent.new(variant: :elevated, padding: :lg, data: { controller: "card" }) do %>
852
+ <h3 class="text-xl font-bold mb-2">Card Title</h3>
853
+ <p class="text-gray-600">Card content goes here.</p>
854
+ <% end %>
855
+
856
+ <%# Or with the helper %>
857
+ <%= pom_card(variant: :bordered, clickable: true, href: "/details") do %>
858
+ <p>Clickable card that links to details page</p>
859
+ <% end %>
860
+ ```
861
+
862
+ ## Testing
863
+
864
+ Pom components work seamlessly with ViewComponent's testing utilities:
865
+
866
+ ```ruby
867
+ # test/components/pom/button_component_test.rb
868
+ require "test_helper"
869
+
870
+ module Pom
871
+ class ButtonComponentTest < ViewComponent::TestCase
872
+ test "renders with default options" do
873
+ render_inline(ButtonComponent.new) { "Click me" }
874
+
875
+ assert_selector "button.inline-flex.bg-blue-600"
876
+ assert_text "Click me"
877
+ end
878
+
879
+ test "renders disabled button" do
880
+ render_inline(ButtonComponent.new(disabled: true)) { "Disabled" }
881
+
882
+ assert_selector "button.opacity-50.cursor-not-allowed"
883
+ end
884
+
885
+ test "validates enum values" do
886
+ assert_raises(ArgumentError) do
887
+ ButtonComponent.new(variant: :invalid)
888
+ end
889
+ end
890
+
891
+ test "captures extra options" do
892
+ render_inline(ButtonComponent.new(data: { controller: "button" })) { "Click" }
893
+
894
+ assert_selector "button[data-controller='button']"
895
+ end
896
+ end
897
+ end
898
+ ```
899
+
900
+ ## Best Practices
901
+
902
+ ### 1. Keep Styles Cohesive
903
+
904
+ Group related styles together and use meaningful variant names:
905
+
906
+ ```ruby
907
+ define_styles(
908
+ base: "btn",
909
+ variant: {
910
+ primary: "bg-blue-600 text-white",
911
+ secondary: "bg-gray-600 text-white",
912
+ danger: "bg-red-600 text-white"
913
+ }
914
+ )
915
+ ```
916
+
917
+ ### 2. Use Required Options for Critical Data
918
+
919
+ ```ruby
920
+ option :user, required: true
921
+ option :action, required: true
922
+ ```
923
+
924
+ ### 3. Provide Sensible Defaults
925
+
926
+ ```ruby
927
+ option :size, enums: [:sm, :md, :lg], default: :md
928
+ option :variant, enums: [:default, :primary], default: :default
929
+ ```
930
+
931
+ ### 4. Leverage Extra Options
932
+
933
+ Don't define options for every HTML attribute:
934
+
935
+ ```ruby
936
+ def call
937
+ content_tag :div, content, **merge_options(
938
+ { class: styles_for(variant: variant) },
939
+ extra_options # Captures id, data, aria attributes, etc.
940
+ )
941
+ end
942
+ ```
943
+
944
+ ### 5. Use Style Groups for Complex Components
945
+
946
+ ```ruby
947
+ define_styles(:header, base: "...")
948
+ define_styles(:body, base: "...")
949
+ define_styles(:footer, base: "...")
950
+ ```
951
+
952
+ ### 6. Validate with Enums
953
+
954
+ Use enums to catch typos and invalid values early:
955
+
956
+ ```ruby
957
+ option :status, enums: [:draft, :published, :archived]
958
+ ```
959
+
960
+ ### 7. Organize Components in the Configured Namespace
961
+
962
+ To use the `pom_*` helper methods, define your components in the configured namespace:
963
+
964
+ ```ruby
965
+ # app/components/pom/button_component.rb
966
+ module Pom
967
+ class ButtonComponent < Pom::Component
968
+ # ...
969
+ end
970
+ end
971
+ ```
972
+
973
+ ## Configuration
974
+
975
+ You can configure Pom to use custom component prefixes in addition to the default `pom` prefix. This allows you to organize components in multiple namespaces and use helper methods for all of them.
976
+
977
+ Create an initializer:
978
+
979
+ ```ruby
980
+ # config/initializers/pom.rb
981
+ Pom.configure do |config|
982
+ # Append custom prefixes to the default ["pom"]
983
+ config.component_prefixes << "ui"
984
+ config.component_prefixes << "admin"
985
+ end
986
+ ```
987
+
988
+ Now you can use helper methods for components in any configured namespace:
989
+
990
+ ```ruby
991
+ # app/components/ui/card_component.rb
992
+ module Ui
993
+ class CardComponent < Pom::Component
994
+ # ...
995
+ end
996
+ end
997
+
998
+ # app/components/admin/dashboard_component.rb
999
+ module Admin
1000
+ class DashboardComponent < Pom::Component
1001
+ # ...
1002
+ end
1003
+ end
1004
+ ```
1005
+
1006
+ Use them in views:
1007
+
1008
+ ```erb
1009
+ <%# Looks for Ui::CardComponent %>
1010
+ <%= ui_card(variant: :bordered) do %>
1011
+ Card content
1012
+ <% end %>
1013
+
1014
+ <%# Looks for Admin::DashboardComponent %>
1015
+ <%= admin_dashboard(user: current_user) %>
1016
+
1017
+ <%# Still works - looks for Pom::ButtonComponent %>
1018
+ <%= pom_button(variant: :primary) do %>
1019
+ Click me
1020
+ <% end %>
1021
+ ```
1022
+
1023
+ ### Default Configuration
1024
+
1025
+ By default, `component_prefixes` is set to `["pom"]`, which means only `pom_*` helper methods work out of the box.
1026
+
1027
+ ## License
1028
+
1029
+ The gem is available as open source under the terms of the [MIT License](MIT-LICENSE).
1030
+
1031
+ ## Contributing
1032
+
1033
+ Contributions are welcome! Please feel free to submit a Pull Request.
1034
+
1035
+ ## Credits
1036
+
1037
+ Created by [Hoang Nghiem](https://github.com/hoangnghiem) · Maintained by [Pom](https://github.com/pom-io)