vident 1.0.1 → 2.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +49 -18
  4. data/lib/vident/caching.rb +4 -110
  5. data/lib/vident/capabilities/caching.rb +98 -0
  6. data/lib/vident/capabilities/child_element_rendering.rb +92 -0
  7. data/lib/vident/capabilities/class_list_building.rb +23 -0
  8. data/lib/vident/capabilities/declarable.rb +39 -0
  9. data/lib/vident/capabilities/identifiable.rb +54 -0
  10. data/lib/vident/capabilities/inspectable.rb +17 -0
  11. data/lib/vident/capabilities/root_element_rendering.rb +31 -0
  12. data/lib/vident/capabilities/stimulus_data_emitting.rb +98 -0
  13. data/lib/vident/capabilities/stimulus_declaring.rb +79 -0
  14. data/lib/vident/capabilities/stimulus_draft.rb +51 -0
  15. data/lib/vident/capabilities/stimulus_mutation.rb +60 -0
  16. data/lib/vident/capabilities/stimulus_parsing.rb +144 -0
  17. data/lib/vident/capabilities/tailwind.rb +18 -0
  18. data/lib/vident/component.rb +14 -76
  19. data/lib/vident/engine.rb +6 -5
  20. data/lib/vident/error.rb +16 -0
  21. data/lib/vident/internals/action_builder.rb +97 -0
  22. data/lib/vident/internals/attribute_writer.rb +17 -0
  23. data/lib/vident/internals/class_list_builder.rb +62 -0
  24. data/lib/vident/internals/declaration.rb +13 -0
  25. data/lib/vident/internals/declarations.rb +64 -0
  26. data/lib/vident/internals/draft.rb +47 -0
  27. data/lib/vident/internals/dsl.rb +172 -0
  28. data/lib/vident/internals/plan.rb +9 -0
  29. data/lib/vident/internals/registry.rb +37 -0
  30. data/lib/vident/internals/resolver.rb +316 -0
  31. data/lib/vident/internals/target_builder.rb +23 -0
  32. data/lib/vident/stable_id.rb +3 -3
  33. data/lib/vident/stimulus/action.rb +127 -0
  34. data/lib/vident/stimulus/base.rb +26 -0
  35. data/lib/vident/stimulus/class_map.rb +57 -0
  36. data/lib/vident/stimulus/collection.rb +40 -0
  37. data/lib/vident/stimulus/combinable.rb +30 -0
  38. data/lib/vident/stimulus/controller.rb +45 -0
  39. data/lib/vident/stimulus/naming.rb +9 -9
  40. data/lib/vident/stimulus/null.rb +7 -0
  41. data/lib/vident/stimulus/outlet.rb +93 -0
  42. data/lib/vident/stimulus/param.rb +56 -0
  43. data/lib/vident/stimulus/target.rb +48 -0
  44. data/lib/vident/stimulus/value.rb +57 -0
  45. data/lib/vident/stimulus_null.rb +4 -8
  46. data/lib/vident/tailwind.rb +4 -17
  47. data/lib/vident/types.rb +28 -0
  48. data/lib/vident/version.rb +1 -6
  49. data/lib/vident.rb +44 -36
  50. data/skills/vident/SKILL.md +133 -21
  51. data/skills/vident/api-reference.md +662 -0
  52. data/skills/vident/examples.md +505 -0
  53. metadata +40 -28
  54. data/lib/vident/child_element_helper.rb +0 -64
  55. data/lib/vident/class_list_builder.rb +0 -112
  56. data/lib/vident/component_attribute_resolver.rb +0 -87
  57. data/lib/vident/component_class_lists.rb +0 -34
  58. data/lib/vident/stimulus/primitive.rb +0 -38
  59. data/lib/vident/stimulus.rb +0 -31
  60. data/lib/vident/stimulus_action.rb +0 -133
  61. data/lib/vident/stimulus_action_collection.rb +0 -11
  62. data/lib/vident/stimulus_attribute_base.rb +0 -67
  63. data/lib/vident/stimulus_attributes.rb +0 -129
  64. data/lib/vident/stimulus_builder.rb +0 -119
  65. data/lib/vident/stimulus_class.rb +0 -59
  66. data/lib/vident/stimulus_class_collection.rb +0 -11
  67. data/lib/vident/stimulus_collection_base.rb +0 -51
  68. data/lib/vident/stimulus_component.rb +0 -75
  69. data/lib/vident/stimulus_controller.rb +0 -41
  70. data/lib/vident/stimulus_controller_collection.rb +0 -14
  71. data/lib/vident/stimulus_data_attribute_builder.rb +0 -32
  72. data/lib/vident/stimulus_helper.rb +0 -66
  73. data/lib/vident/stimulus_outlet.rb +0 -90
  74. data/lib/vident/stimulus_outlet_collection.rb +0 -11
  75. data/lib/vident/stimulus_param.rb +0 -42
  76. data/lib/vident/stimulus_param_collection.rb +0 -11
  77. data/lib/vident/stimulus_target.rb +0 -47
  78. data/lib/vident/stimulus_target_collection.rb +0 -18
  79. data/lib/vident/stimulus_value.rb +0 -39
  80. data/lib/vident/stimulus_value_collection.rb +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2966a65af9fba6bbddf4ffe3c93868a99e6c281cc992db859408dbb4c46050be
4
- data.tar.gz: e7224204b0d79fe83df5c3a98f0f779ab035645b7d6fabee01818341c6089480
3
+ metadata.gz: 1c85914dca2e7df8a1ff9711be7cf0f1528409ff7d44dbb72501474c2c1359c2
4
+ data.tar.gz: 43e7d176b19a36b2c7d8640cd230429ae59a46e0b77ad9e6c6d24c9c40e38973
5
5
  SHA512:
6
- metadata.gz: 7b1a90bb87dc01712457acfa7b28d6dc3379cce5a66fa635dfbf6f357d1ecb2e635a563330e4f8facc88f3c213ec7309a40193d20a55e391f33f05ec418130cb
7
- data.tar.gz: 5a9aecbe18952241b8b53788a41808e8ba43968c4adc4989aa1184deb683cd91b1b680ee5ff1e697746ceb8fa7cceb0f000ee051f7da7d34f6b1dae48b0952a4
6
+ metadata.gz: 4e07543b9851d9b4d7a9ef98eadec0a53ad8c8e409934fd0f92e942386235fa7f964d36fa04bb84bde91d036ce4cf7231d848e13274f3190f91573b00339a999
7
+ data.tar.gz: b89835399b32ff4e98efd24f1fb75328ed0633a73c935eb6a7c844f5f1dc7f909ade0805741445db81a0d704ffcc066aa416c17ee6a65100b9d8fb9738d86b41
data/CHANGELOG.md CHANGED
@@ -6,6 +6,60 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
8
 
9
+ ## [2.0.0] - 2026-04-24
10
+
11
+ Vident 2.0 is a ground-up rearchitecture of the DSL, attribute resolution, and composition model. The public shape of `stimulus do ... end`, `root_element`, `child_element`, outlets, props, and the `stimulus_*:` prop/kwarg API is preserved for common cases, but several internals and a handful of edge-case behaviours changed. See `doc/reviews/v1-gotchas.md` for the full list of fixed gotchas; the highlights are below.
12
+
13
+ ### Breaking
14
+
15
+ - **Namespace consolidation.** The `Vident2::*` namespace used during side-by-side development has been removed. Everything now lives under `Vident::*` directly: `Vident::Component`, `Vident::Capabilities::*`, `Vident::Phlex::HTML`, `Vident::ViewComponent::Base`, `Vident::Stimulus::*` (value classes), `Vident::Internals::*` (DSL/Resolver/Plan). Upgrade path is a `s/Vident2::/Vident::/g` on project code.
16
+ - **Legacy collection classes removed.** `Vident::StimulusAction`, `StimulusTarget`, `StimulusController`, `StimulusValue`, `StimulusParam`, `StimulusOutlet`, `StimulusClass`, and each of their `*Collection` companions (plus `StimulusBuilder`, `StimulusDataAttributeBuilder`, `StimulusAttributeBase`, `StimulusAttributes`, `StimulusAction::Descriptor`) are gone. The 2.0 equivalents live under `Vident::Stimulus::*` (`Action`, `Target`, `Controller`, `Value`, `Param`, `Outlet`, `ClassMap`, `Collection`). `#to_h` now uses **Symbol keys** uniformly; V1 returned String keys for some primitives.
17
+ - **`child_element` strictness.** Passing both `stimulus_<singular>:` and `stimulus_<plural>:` kwargs now raises `ArgumentError` (`"mutually exclusive — pass one or the other."`). V1 silently dropped the singular when the plural was an empty array (latent since 1.0.0, see "Fixed" below). Workaround at call sites that relied on the V1 shape: `stimulus_targets: [:input, *control_targets]`.
18
+ - **`stimulus_controllers:` prop appends instead of replacing.** V1 replaced the implied controller when you passed `stimulus_controllers:` at render time; V2 appends, so the root element carries `data-controller="implied extra"`. Migration: if you wanted to *replace*, add `no_stimulus_controller` at the class level.
19
+ - **Outlet DSL procs now resolve.** V1 silently passed a proc through as the outlet selector string, emitting literal `#<Proc:0x...>`. V2 evaluates the proc in the instance binding and emits the returned string. This turns previously broken code into working code, but if you relied on the proc-as-identity string for some reason, you'll now see a different data attribute.
20
+ - **`nil` vs `false` drop rule.** V1 dropped both `nil` and `false` proc returns silently via a `blank?` filter. V2 drops only `nil`; `false` reaches the parser and raises `Vident::ParseError`. Fix: return `nil` (not `false`) to mean "don't emit this entry."
21
+ - **`no_stimulus_controller` + DSL body now raises loudly.** V1 raised a bare `StandardError` at instance init. V2 raises `Vident::DeclarationError` at `stimulus do ... end` time with the offending class name and caller location in the message.
22
+ - **StableId strategy.** Unchanged from 1.0, but reiterated here because it's the biggest gotcha during upgrade. `Vident::StableId` requires an explicit `strategy` and per-request seed — `bin/rails generate vident:install` sets both up correctly.
23
+
24
+ ### Added
25
+
26
+ - **Fluent action DSL.** Inside `stimulus do ... end`, the singular `action(...)` call returns a chainable builder: `action(:escape).on(:keydown).keyboard("esc").window`, `action(:save).modifier(:prevent, :stop)`, `action(:delete).when { admin? }`. Chain methods: `.on`, `.call_method`, `.modifier`, `.keyboard`, `.window`, `.on_controller`, `.when`.
27
+ - **Kwargs shorthand for actions.** `action :save, on: :click, modifier: [:prevent, :stop], keyboard: "ctrl+s", window: true, on_controller: :admin, call_method: :handle_save, when: -> { ... }` — equivalent to the fluent chain, pick whichever reads better. Unknown kwargs raise `ArgumentError`.
28
+ - **Controller aliases.** `controller "admin/users", as: :admin` inside `stimulus do` declares a short alias; `action(...).on_controller(:admin)` or `action(..., on_controller: :admin)` then routes through it. Unknown alias refs raise `Vident::DeclarationError`. Runtime inputs (`stimulus_actions: [{method: :save, controller: :admin}]`) resolve the same map.
29
+ - **Capability mixin composition.** `Vident::Component` is a composition root that includes twelve focused capability mixins (`Tailwind`, `Declarable`, `Identifiable`, `StimulusDeclaring`, `StimulusParsing`, `StimulusMutation`, `StimulusDraft`, `StimulusDataEmitting`, `ClassListBuilding`, `RootElementRendering`, `ChildElementRendering`, `Inspectable`), plus an opt-in `Caching` mixin (thirteen in total, including Caching). Mixin order mirrors capability dependencies.
30
+ - **Singular DSL primitives.** `value(:name, *, **)`, `param(:name, *, **)`, `outlet(:name, *, **)`, `class_map(:name, *, **)`, `controller(path, as: nil)`, `controllers(*paths)`, `target(:name).when { ... }` — full set of singular entry points alongside the plural forms.
31
+ - **Draft → Plan seal pattern.** Internal: `after_component_initialize` mutators (e.g. `add_stimulus_actions`) still work against a mutable Draft; the Draft seals into an immutable Plan once rendering begins. Prevents the V1 "mutator silently ignored post-render" class of bug.
32
+ - **Phase-split proc resolution.** DSL procs are evaluated in two phases: static (after_initialize, no `view_context`) and procs (`before_template` / `before_render`, `view_context`/`helpers` wired). Procs can now call Rails helpers (`number_with_precision`, url helpers, `t`, `l`, etc.) safely — already backported to 1.0.2.
33
+ - `has_stimulus_controller` class method re-enables the implied controller in a subclass after a parent declared `no_stimulus_controller` (#27).
34
+ - `root_element_class_list(extra = nil)` and `root_element_data_attributes` for components rendering through third-party tag helpers instead of `root_element(...)` (#25; V1 names `render_classes` / `stimulus_data_attributes` — see UPGRADING.md §8).
35
+ - Class-level Stimulus builders — `MyComponent.stimulus_target(:x)` etc. — return value objects without a component instance (#18).
36
+ - `Vident::Types::*` — seven named Literal unions for the built-in `stimulus_*:` prop types, reusable from user components (#16).
37
+ - `cache_component(*keys, **options, &block)` on both adapter base classes — fragment-caches the block using the Vident-computed `cache_key` (#13).
38
+ - Shared `Vident::Stimulus::Base` + `Combinable` for the value classes: `with(**overrides)` combinator, canonical `deconstruct_keys` (restores pattern matching — previously shadowed by the `to_h` data-attribute override), and `Action.parse([event, existing_action])` merge form (#28).
39
+
40
+ ### Fixed
41
+
42
+ - `child_element` no longer silently drops a `stimulus_<singular>:` kwarg when `stimulus_<plural>:` is also passed with an empty array. The V1 helper returned early on an empty-but-truthy plural, losing the singular and emitting no data attribute — latent since 1.0.0, surfaced by call sites like `child_element(:input, stimulus_target: :input, stimulus_targets: control_targets)` where `control_targets` defaulted to `[]`. V2 raises `ArgumentError` on both-set (see Breaking); workaround on 1.x is `stimulus_targets: [:input, *control_targets]` (also works on 2.x).
43
+ - Outlet DSL procs now resolve instead of being treated as identity strings (see Breaking).
44
+ - `add_stimulus_actions([:click, :handle])` now treats the Array as *one* action descriptor (event + method pair) rather than splatting it into two separate `:click` and `:handle` actions. Matches the DSL's `actions [:click, :handle]` semantics — the V1 asymmetry is gone.
45
+
46
+ ### Removed
47
+
48
+ - `Vident::StimulusAction::Descriptor` typed data class. The V2 equivalent is `Vident::Stimulus::Action` itself — the Hash shape accepted by the DSL (`{event:, method:, controller:, options:, keyboard:, window:}`) parses directly into the value class now. Callers that instantiated `Descriptor` explicitly should swap to `Vident::Stimulus::Action.parse(hash, implied: ...)` or just pass the Hash.
49
+ - Legacy top-level requires like `require "vident/stimulus_action"`. The main `require "vident"` loads the whole surface; deep requires for individual value classes still work (`require "vident/stimulus/action"`) but are no longer the documented path.
50
+
51
+
52
+ ## [1.0.2] - 2026-04-21
53
+
54
+ ### Changed
55
+
56
+ - Stimulus DSL procs (`values foo: -> { ... }`, `actions -> { ... }`, etc.) now resolve at **render time** — Phlex's `before_template` for `Vident::Phlex::HTML`, ViewComponent's `before_render` for `Vident::ViewComponent::Base` — instead of in `after_initialize`. Procs can now reach `helpers` / `view_context`, so they can call Rails helpers (`number_with_precision`, `t`, `l`, url helpers, etc.). Non-proc DSL entries still land in the collections at init time, so `after_component_initialize` mutators and external readers see them in the same order as before.
57
+
58
+ ### Added
59
+
60
+ - `phlex_helpers :name1, :name2, ...` class macro on `Vident::Phlex::HTML` — opts the component into Phlex's per-helper Rails adapters (`Phlex::Rails::Helpers::<CamelCase>`) so DSL procs can call helpers bare (`number_with_precision(@amount, precision: 2)`) instead of via the deprecated `helpers.<method>`. Unknown helper names raise `ArgumentError` at class definition.
61
+
62
+
9
63
  ## [1.0.0] - 2026-04-19
10
64
 
11
65
  ### Breaking
data/README.md CHANGED
@@ -79,9 +79,13 @@ class ButtonComponent < Vident::ViewComponent::Base
79
79
 
80
80
  # Configure Stimulus integration
81
81
  stimulus do
82
- # Setup actions, including with proc to evaluate on instance
83
- actions [:click, :handle_click],
84
- -> { [stimulus_scoped_event(:my_custom_event), :handle_this] if should_handle_this? }
82
+ # Fluent action DSL: reads left-to-right as "the handle_click method fires on the click event".
83
+ action(:handle_click).on(:click)
84
+ # Kwargs shorthand — same result, pick whichever reads better:
85
+ action :handle_submit, on: :submit, modifier: [:prevent, :stop]
86
+ # Proc for conditional / cross-component wiring, evaluated in the instance at render time.
87
+ action(-> { [stimulus_scoped_event(:my_custom_event), :handle_this] if should_handle_this? })
88
+
85
89
  # Map the clicked_count prop as a Stimulus value
86
90
  values_from_props :clicked_count
87
91
  # Dynamic values using procs (evaluated in component context)
@@ -178,7 +182,7 @@ Use the component in your views:
178
182
  <%= render ButtonComponent.new(text: "Cancel", url: "/home", style: :secondary) %>
179
183
 
180
184
  <!-- Override things -->
181
- <%= render ButtonComponent.new(text: "Cancel", url: "/home" classes: "bg-red-900", html_options: {role: "button"}) %>
185
+ <%= render ButtonComponent.new(text: "Cancel", url: "/home", classes: "bg-red-900", html_options: {role: "button"}) %>
182
186
  ```
183
187
 
184
188
  The rendered HTML includes all Stimulus data attributes:
@@ -227,7 +231,7 @@ class CardComponent < Vident::ViewComponent::Base
227
231
  # Property with validation
228
232
  prop :size, _Union(:small, :medium, :large), default: :medium
229
233
 
230
- # Boolean property (creates predicate method)
234
+ # Boolean property (pass `predicate: :public` to also generate a `?` method)
231
235
  prop :featured, _Boolean, default: false
232
236
  end
233
237
  ```
@@ -271,7 +275,7 @@ The `root_element` helper method renders your component's root element with all
271
275
  ```ruby
272
276
  # In your component class
273
277
  def root_element_classes
274
- ["card", featured? ? "card-featured" : nil]
278
+ ["card", @featured ? "card-featured" : nil]
275
279
  end
276
280
 
277
281
  private
@@ -362,17 +366,41 @@ class ToggleComponent < Vident::ViewComponent::Base
362
366
  end
363
367
  ```
364
368
 
365
- **Action modifiers**the Array form `[:click, :method]` handles the common case. For Stimulus's modifier syntax (`:once`/`:prevent`/`:stop`/`:passive`/`:"!passive"`/`:capture`/`:self`, keyboard filters, `@window`), pass a Hash or a `Vident::StimulusAction::Descriptor`:
369
+ **Action modifiers — fluent DSL.** Singular `action(...)` returns a builder you chain with event, modifier, keyboard, and window setters. Kwargs shorthand is equivalent:
366
370
 
367
371
  ```ruby
368
- actions({event: :click, method: :submit, options: [:once, :prevent]})
369
- actions({event: :keydown, method: :on_key, keyboard: "ctrl+a"})
370
- actions({event: :resize, method: :on_resize, window: true})
371
- # or, if you want a typed, passable object:
372
- actions Vident::StimulusAction::Descriptor.new(event: :click, method: :save, options: [:prevent])
372
+ stimulus do
373
+ action(:submit).on(:click).modifier(:once, :prevent) # click:once:prevent->implied#submit
374
+ action(:on_key).on(:keydown).keyboard("ctrl+a") # keydown.ctrl+a->implied#onKey
375
+ action(:on_resize).on(:resize).window # resize@window->implied#onResize
376
+
377
+ # kwargs shorthand — same result:
378
+ action :submit, on: :click, modifier: [:once, :prevent]
379
+ action :on_key, on: :keydown, keyboard: "ctrl+a"
380
+ action :on_resize, on: :resize, window: true
381
+ action :save, on: :click, call_method: :handle_save
382
+
383
+ # conditional inclusion via `.when` / `when:`:
384
+ action(:delete).when { admin? }
385
+ end
373
386
  ```
374
387
 
375
- Unknown option symbols raise `ArgumentError` at attribute construction, not at render.
388
+ Chain methods: `.on`, `.call_method`, `.modifier`, `.keyboard`, `.window`, `.on_controller`, `.when`. Recognised kwargs: `on:`, `call_method:`, `modifier:` (Symbol or Array), `keyboard:`, `window:`, `on_controller:`, `when:`. Unknown kwargs or modifier symbols raise `ArgumentError`.
389
+
390
+ **Controller aliases.** Declare a short alias with `controller "path", as: :sym`, then reference it from action entries via the fluent `.on_controller(:sym)` or the `on_controller: :sym` kwarg:
391
+
392
+ ```ruby
393
+ stimulus do
394
+ controller "admin/users", as: :admin
395
+
396
+ action(:save).on(:click).on_controller(:admin) # click->admin--users#save
397
+ action :save, on: :click, on_controller: :admin # same, kwargs form
398
+ end
399
+ ```
400
+
401
+ Unknown aliases raise `Vident::DeclarationError` at render time.
402
+
403
+ **Legacy Hash form.** Still accepted for compat — `actions({event: :click, method: :submit, options: [:once, :prevent]})` parses the same way. The Hash descriptor is folded directly into `Vident::Stimulus::Action` — pass a Hash and it is parsed in place. Accepted keys: `method:`, `event:`, `controller:`, `options:`, `keyboard:`, `window:`.
376
404
 
377
405
  ### Dynamic Values and Classes with Procs
378
406
 
@@ -415,7 +443,10 @@ class DynamicComponent < Vident::ViewComponent::Base
415
443
  end
416
444
  ```
417
445
 
418
- Procs have access to instance variables, component methods, and Rails helpers.
446
+ Procs have access to instance variables and component methods. They run at render time (Phlex `before_template` / ViewComponent `before_render`), so they can reach the view context:
447
+
448
+ - **Phlex**: `helpers` is deprecated in phlex-rails. Opt in per Rails helper by including the matching adapter — e.g. `include Phlex::Rails::Helpers::NumberWithPrecision` — and call the helper bare (`number_with_precision(@amount, precision: 2)`) inside the proc. Vident ships a `phlex_helpers :number_with_precision, :t, :l` class macro on `Vident::Phlex::HTML` that does the right include for each name. See [phlex.fun/rails/helpers](https://www.phlex.fun/rails/helpers) for the full list of adapters.
449
+ - **ViewComponent**: call `helpers.<method>` or `view_context.<method>` directly.
419
450
 
420
451
  **Important**: Each proc returns a single value for its corresponding stimulus attribute. If a proc returns an array, that entire array is treated as a single value, not multiple separate values. To provide multiple values for an attribute, use multiple procs or mix procs with static values:
421
452
 
@@ -486,14 +517,14 @@ class MyComponent < Vident::ViewComponent::Base
486
517
  # This would generate: "my-component:dataLoaded"
487
518
  puts stimulus_scoped_event(:data_loaded)
488
519
 
489
- # For window events, this generates: "my-component:dataLoaded@window"
520
+ # For window events, this generates: :"my-component:dataLoaded@window"
490
521
  puts stimulus_scoped_event_on_window(:data_loaded)
491
522
  end
492
523
  end
493
524
 
494
525
  # Available as both class and instance methods:
495
- MyComponent.stimulus_scoped_event(:data_loaded) # => "my-component:dataLoaded"
496
- MyComponent.new.stimulus_scoped_event(:data_loaded) # => "my-component:dataLoaded"
526
+ MyComponent.stimulus_scoped_event(:data_loaded) # => :"my-component:dataLoaded"
527
+ MyComponent.new.stimulus_scoped_event(:data_loaded) # => :"my-component:dataLoaded"
497
528
  ```
498
529
 
499
530
  This is useful for:
@@ -529,7 +560,7 @@ class CustomComponent < Vident::ViewComponent::Base
529
560
  end
530
561
  ```
531
562
 
532
- All stimulus props accept Symbol paths as well as Strings (e.g. `stimulus_controllers: [:custom, :"admin/users"]`). `stimulus_values:` and `stimulus_classes:` additionally accept Array entries (for cross-controller: `[["admin/users", :name, "value"]]`) and pre-built `StimulusValue`/`StimulusValueCollection` / `StimulusClass`/`StimulusClassCollection` instances, so you can compose attribute sets outside the component and pass them in.
563
+ All stimulus props accept Symbol paths as well as Strings (e.g. `stimulus_controllers: [:custom, :"admin/users"]`). `stimulus_values:` and `stimulus_classes:` additionally accept Array entries (for cross-controller: `[["admin/users", :name, "value"]]`) and pre-built `Vident::Stimulus::Value` / `Vident::Stimulus::ClassMap` instances, so you can compose attribute sets outside the component and pass them in.
533
564
 
534
565
  or you can use tag helpers to generate HTML with Stimulus attributes:
535
566
 
@@ -1,114 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Vident
4
- # Rails fragment caching works by either expecting the cached key object to respond to `cache_key` or for that object
5
- # to be an array or hash.
6
- module Caching
7
- extend ActiveSupport::Concern
8
-
9
- class_methods do
10
- def inherited(subclass)
11
- subclass.instance_variable_set(:@named_cache_key_attributes, @named_cache_key_attributes.clone)
12
- super
13
- end
14
-
15
- def with_cache_key(*attrs, name: :_collection)
16
- # Add view file to cache key
17
- attrs << :component_modified_time
18
- attrs << :to_h if respond_to?(:to_h)
19
- named_cache_key_includes(name, *attrs.uniq)
20
- end
21
-
22
- attr_reader :named_cache_key_attributes
23
-
24
- # Components can be used with fragment caching, but you need to be careful! Read on...
25
- #
26
- # <% cache component do %>
27
- # <%= render component %>
28
- # <% end %>
29
- #
30
- # The most important point is that Rails cannot track dependencies on the component itself, so you need to
31
- # be careful to be explicit on the attributes, and manually specify any sub Viewcomponent dependencies that the
32
- # component has. The assumption is that the subcomponent takes any attributes from the parent, so the cache key
33
- # depends on the parent component attributes. Otherwise changes to the parent or sub component views/Ruby class
34
- # will result in different cache keys too. Of course if you invalidate all cache keys with a modifier on deploy
35
- # then no need to worry about changing the cache key on component changes, only on attribute/data changes.
36
- #
37
- # A big caveat is that the cache key cannot depend on anything related to the view_context of the component (such
38
- # as `helpers` as the key is created before the rending pipline is invoked (which is when the view_context is set).
39
- def depends_on(*klasses)
40
- @component_dependencies ||= []
41
- @component_dependencies += klasses
42
- end
43
-
44
- attr_reader :component_dependencies
45
-
46
- def component_modified_time
47
- return @component_modified_time if Rails.env.production? && @component_modified_time
48
-
49
- raise StandardError, "Must implement cache_component_modified_time" unless respond_to?(:cache_component_modified_time)
50
-
51
- # FIXME: This could stack overflow if there are circular dependencies
52
- deps = component_dependencies&.map(&:component_modified_time)&.join("-") || ""
53
- @component_modified_time = deps + cache_component_modified_time
54
- end
3
+ require_relative "capabilities/caching"
55
4
 
56
- private
57
-
58
- def named_cache_key_includes(name, *attrs)
59
- define_cache_key_method unless @named_cache_key_attributes
60
- @named_cache_key_attributes ||= {}
61
- @named_cache_key_attributes[name] = attrs
62
- end
63
-
64
- def define_cache_key_method
65
- # If the presenter defines cache key setup then define the method. Otherwise Rails assumes this
66
- # will return a valid key if the class will respond to this
67
- define_method :cache_key do |n = :_collection|
68
- if defined?(@cache_key)
69
- return @cache_key[n] if @cache_key.key?(n)
70
- else
71
- @cache_key ||= {}
72
- end
73
- generate_cache_key(n)
74
- @cache_key[n]
75
- end
76
- end
77
- end
78
-
79
- # Component modified time which is combined with other cache key attributes to generate cache key for an instance
80
- def component_modified_time = self.class.component_modified_time
81
-
82
- def cacheable? = respond_to?(:cache_key)
83
-
84
- def cache_key_modifier = ENV["RAILS_CACHE_ID"]
85
-
86
- def cache_keys_for_sources(key_attributes)
87
- sources = key_attributes.flat_map { |n| n.is_a?(Proc) ? instance_eval(&n) : send(n) }
88
- sources.compact.map do |item|
89
- next if item == self
90
- generate_item_cache_key_from(item)
91
- end
92
- end
93
-
94
- def generate_item_cache_key_from(item)
95
- if item.respond_to? :cache_key_with_version
96
- item.cache_key_with_version
97
- elsif item.respond_to? :cache_key
98
- item.cache_key
99
- elsif item.is_a?(String)
100
- Digest::SHA1.hexdigest(item)
101
- else
102
- Digest::SHA1.hexdigest(Marshal.dump(item))
103
- end
104
- end
105
-
106
- def generate_cache_key(index)
107
- key_attributes = self.class.named_cache_key_attributes[index]
108
- return nil unless key_attributes
109
- key = "#{self.class.name}/#{cache_keys_for_sources(key_attributes).join("/")}"
110
- raise StandardError, "Cache key for key #{key} is blank!" if key.blank?
111
- @cache_key[index] = cache_key_modifier.present? ? "#{key}/#{cache_key_modifier}" : key
112
- end
113
- end
5
+ module Vident
6
+ # Top-level alias for the mixin at `Vident::Capabilities::Caching`.
7
+ Caching = Capabilities::Caching
114
8
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+
5
+ module Vident
6
+ module Capabilities
7
+ # Fragment-caching opt-in. Include into a component to get `cacheable?`,
8
+ # `cache_key`, and the `with_cache_key` / `depends_on` class helpers.
9
+ # `cache_component_modified_time` must be implemented by the adapter base class.
10
+ module Caching
11
+ extend ActiveSupport::Concern
12
+
13
+ class_methods do
14
+ def inherited(subclass)
15
+ subclass.instance_variable_set(:@named_cache_key_attributes, @named_cache_key_attributes&.dup)
16
+ subclass.instance_variable_set(:@component_dependencies, @component_dependencies&.dup)
17
+ super
18
+ end
19
+
20
+ def with_cache_key(*attrs, name: :_collection)
21
+ attrs << :component_modified_time
22
+ attrs << :to_h if respond_to?(:to_h)
23
+ named_cache_key_includes(name, *attrs.uniq)
24
+ end
25
+
26
+ attr_reader :named_cache_key_attributes
27
+
28
+ def depends_on(*klasses)
29
+ @component_dependencies ||= []
30
+ @component_dependencies += klasses
31
+ end
32
+
33
+ attr_reader :component_dependencies
34
+
35
+ def component_modified_time
36
+ return @component_modified_time if defined?(::Rails) && ::Rails.env.production? && @component_modified_time
37
+
38
+ raise ::Vident::ConfigurationError, "Must implement cache_component_modified_time" unless respond_to?(:cache_component_modified_time)
39
+
40
+ deps = component_dependencies&.map(&:component_modified_time)&.join("-") || ""
41
+ @component_modified_time = deps + cache_component_modified_time
42
+ end
43
+
44
+ private
45
+
46
+ def named_cache_key_includes(name, *attrs)
47
+ define_cache_key_method unless @named_cache_key_attributes
48
+ @named_cache_key_attributes ||= {}
49
+ @named_cache_key_attributes[name] = attrs
50
+ end
51
+
52
+ def define_cache_key_method
53
+ define_method :cache_key do |n = :_collection|
54
+ @cache_key ||= {}
55
+ return @cache_key[n] if @cache_key.key?(n)
56
+ generate_cache_key(n)
57
+ @cache_key[n]
58
+ end
59
+ end
60
+ end
61
+
62
+ def component_modified_time = self.class.component_modified_time
63
+
64
+ def cacheable? = respond_to?(:cache_key)
65
+
66
+ def cache_key_modifier = ENV["RAILS_CACHE_ID"]
67
+
68
+ def cache_keys_for_sources(key_attributes)
69
+ sources = key_attributes.flat_map { |n| n.is_a?(Proc) ? instance_eval(&n) : send(n) }
70
+ sources.compact.filter_map { |item| generate_item_cache_key_from(item) unless item == self }
71
+ end
72
+
73
+ def generate_item_cache_key_from(item)
74
+ if item.respond_to?(:cache_key_with_version)
75
+ item.cache_key_with_version
76
+ elsif item.respond_to?(:cache_key)
77
+ item.cache_key
78
+ elsif item.is_a?(String)
79
+ Digest::SHA1.hexdigest(item)
80
+ else
81
+ Digest::SHA1.hexdigest(Marshal.dump(item))
82
+ end
83
+ end
84
+
85
+ def generate_cache_key(index)
86
+ key_attributes = self.class.named_cache_key_attributes[index]
87
+ return nil unless key_attributes
88
+ sources = cache_keys_for_sources(key_attributes)
89
+ if sources.empty?
90
+ raise ::Vident::ConfigurationError,
91
+ "no cache key sources resolved for #{self.class.name} — ensure `with_cache_key` attributes return non-nil values"
92
+ end
93
+ key = "#{self.class.name}/#{sources.join("/")}"
94
+ @cache_key[index] = cache_key_modifier.present? ? "#{key}/#{cache_key_modifier}" : key
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../internals/registry"
4
+ require_relative "../stimulus/collection"
5
+
6
+ module Vident
7
+ module Capabilities
8
+ module ChildElementRendering
9
+ def child_element(
10
+ tag_name,
11
+ stimulus_controllers: nil,
12
+ stimulus_targets: nil,
13
+ stimulus_actions: nil,
14
+ stimulus_outlets: nil,
15
+ stimulus_values: nil,
16
+ stimulus_params: nil,
17
+ stimulus_classes: nil,
18
+ stimulus_controller: nil,
19
+ stimulus_target: nil,
20
+ stimulus_action: nil,
21
+ stimulus_outlet: nil,
22
+ stimulus_value: nil,
23
+ stimulus_param: nil,
24
+ stimulus_class: nil,
25
+ **options,
26
+ &block
27
+ )
28
+ inputs = {
29
+ controllers: [stimulus_controllers, stimulus_controller],
30
+ actions: [stimulus_actions, stimulus_action],
31
+ targets: [stimulus_targets, stimulus_target],
32
+ outlets: [stimulus_outlets, stimulus_outlet],
33
+ values: [stimulus_values, stimulus_value],
34
+ params: [stimulus_params, stimulus_param],
35
+ class_maps: [stimulus_classes, stimulus_class]
36
+ }
37
+
38
+ data_attrs = {}
39
+ ::Vident::Internals::Registry.each do |kind|
40
+ plural, singular = inputs.fetch(kind.name)
41
+ child_element_check_plural!(plural, singular, kind)
42
+ coll = child_element_build_collection(kind, plural, singular)
43
+ data_attrs.merge!(coll.to_h) unless coll.empty?
44
+ end
45
+
46
+ generate_child_element(tag_name, data_attrs, options, &block)
47
+ end
48
+
49
+ def generate_child_element(tag_name, stimulus_data_attributes, options, &block)
50
+ raise NoMethodError, "adapter must implement generate_child_element"
51
+ end
52
+
53
+ private
54
+
55
+ def child_element_check_plural!(plural, singular, kind)
56
+ if plural && singular
57
+ raise ArgumentError,
58
+ "'stimulus_#{kind.plural_name}:' and 'stimulus_#{kind.singular_name}:' " \
59
+ "are mutually exclusive — pass one or the other."
60
+ end
61
+ return if plural.nil?
62
+ return if plural.is_a?(Enumerable) && !plural.is_a?(Hash)
63
+ return if plural.is_a?(Hash) && kind.keyed?
64
+ raise ArgumentError,
65
+ "'stimulus_#{kind.plural_name}:' must be an enumerable. " \
66
+ "Did you mean 'stimulus_#{kind.singular_name}:'?"
67
+ end
68
+
69
+ # Exactly one of `plural` / `singular` is non-nil; guard above
70
+ # rejects both-set.
71
+ def child_element_build_collection(kind, plural, singular)
72
+ plural_method = :"stimulus_#{kind.plural_name}"
73
+ singular_method = :"stimulus_#{kind.singular_name}"
74
+
75
+ if plural
76
+ if kind.keyed? && plural.is_a?(Hash)
77
+ send(plural_method, plural)
78
+ elsif plural.is_a?(Array)
79
+ send(plural_method, *plural)
80
+ else
81
+ send(plural_method, *Array.wrap(plural))
82
+ end
83
+ elsif singular
84
+ coll_items = [send(singular_method, *Array.wrap(singular))]
85
+ ::Vident::Stimulus::Collection.new(kind: kind, items: coll_items)
86
+ else
87
+ ::Vident::Stimulus::Collection.new(kind: kind, items: [])
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../internals/class_list_builder"
4
+
5
+ module Vident
6
+ module Capabilities
7
+ module ClassListBuilding
8
+ def class_list_for_stimulus_classes(*names)
9
+ resolve_stimulus_attributes_at_render_time
10
+ plan = seal_draft
11
+ maps = plan.class_maps
12
+ return "" if maps.empty? || names.empty?
13
+
14
+ result = ::Vident::Internals::ClassListBuilder.call(
15
+ stimulus_classes: maps,
16
+ stimulus_class_names: names,
17
+ tailwind_merger: tailwind_merger
18
+ )
19
+ result || ""
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../types"
4
+
5
+ module Vident
6
+ module Capabilities
7
+ module Declarable
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ extend Literal::Properties
12
+
13
+ prop :element_tag, Symbol, default: :div
14
+ prop :id, _Nilable(String)
15
+ prop :classes, _Union(String, _Array(String)), default: -> { [] }
16
+ prop :html_options, Hash, default: -> { {} }
17
+
18
+ # `stimulus_controllers:` APPENDS to the implied controller (which
19
+ # seeds first unless `no_stimulus_controller`).
20
+ prop :stimulus_controllers, ::Vident::Types::StimulusControllers, default: -> { [] }
21
+ prop :stimulus_actions, ::Vident::Types::StimulusActions, default: -> { [] }
22
+ prop :stimulus_targets, ::Vident::Types::StimulusTargets, default: -> { [] }
23
+ prop :stimulus_outlets, ::Vident::Types::StimulusOutlets, default: -> { [] }
24
+ prop :stimulus_outlet_host, _Nilable(::Vident::Component)
25
+ prop :stimulus_values, ::Vident::Types::StimulusValues, default: -> { {} }
26
+ prop :stimulus_params, ::Vident::Types::StimulusParams, default: -> { {} }
27
+ prop :stimulus_classes, ::Vident::Types::StimulusClasses, default: -> { {} }
28
+ end
29
+
30
+ class_methods do
31
+ def prop_names
32
+ literal_properties.properties_index.keys.map(&:to_sym)
33
+ end
34
+ end
35
+
36
+ def prop_names = self.class.prop_names
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../stimulus/controller"
4
+ require_relative "../stable_id"
5
+
6
+ module Vident
7
+ module Capabilities
8
+ module Identifiable
9
+ extend ActiveSupport::Concern
10
+
11
+ class_methods do
12
+ def stimulus_identifier_path
13
+ name&.underscore || "anonymous_component"
14
+ end
15
+
16
+ def stimulus_identifier
17
+ stimulus_identifier_path.split("/").map(&:dasherize).join("--")
18
+ end
19
+
20
+ def component_name
21
+ @component_name ||= stimulus_identifier
22
+ end
23
+ end
24
+
25
+ def component_name = self.class.component_name
26
+
27
+ def stimulus_identifier = self.class.stimulus_identifier
28
+
29
+ private def default_controller_path = self.class.stimulus_identifier_path
30
+
31
+ # `.presence` is intentional — blank string falls through to auto-generation.
32
+ def id
33
+ @id.presence || random_id
34
+ end
35
+
36
+ def random_id
37
+ @__vident_auto_id ||= "#{component_name}-#{::Vident::StableId.next_id_in_sequence}"
38
+ end
39
+
40
+ def outlet_id
41
+ @outlet_id ||= [stimulus_identifier, "##{id}"]
42
+ end
43
+
44
+ private
45
+
46
+ def implied_controller
47
+ @__vident_implied_controller ||= ::Vident::Stimulus::Controller.new(
48
+ path: self.class.stimulus_identifier_path,
49
+ name: self.class.stimulus_identifier
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ module Capabilities
5
+ module Inspectable
6
+ # Matches the Data.define#with convention introduced in Ruby 3.2.
7
+ def with(overrides = {})
8
+ self.class.new(**to_h.merge(overrides))
9
+ end
10
+
11
+ def inspect(klass_name = "Component")
12
+ attr_text = to_h.map { |k, v| "#{k}=#{v.inspect}" }.join(", ")
13
+ "#<#{self.class.name}<Vident::#{klass_name}> #{attr_text}>"
14
+ end
15
+ end
16
+ end
17
+ end