vident 1.0.1 → 1.0.2

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +4 -1
  4. data/lib/vident/component_attribute_resolver.rb +27 -8
  5. data/lib/vident/component_class_lists.rb +3 -0
  6. data/lib/vident/stimulus_builder.rb +28 -11
  7. data/lib/vident/stimulus_helper.rb +4 -4
  8. data/lib/vident/version.rb +1 -1
  9. data/lib/vident2/caching.rb +93 -0
  10. data/lib/vident2/component.rb +538 -0
  11. data/lib/vident2/engine.rb +18 -0
  12. data/lib/vident2/error.rb +30 -0
  13. data/lib/vident2/internals/action_builder.rb +101 -0
  14. data/lib/vident2/internals/attribute_writer.rb +22 -0
  15. data/lib/vident2/internals/class_list_builder.rb +79 -0
  16. data/lib/vident2/internals/declaration.rb +17 -0
  17. data/lib/vident2/internals/declarations.rb +76 -0
  18. data/lib/vident2/internals/draft.rb +60 -0
  19. data/lib/vident2/internals/dsl.rb +198 -0
  20. data/lib/vident2/internals/plan.rb +12 -0
  21. data/lib/vident2/internals/registry.rb +41 -0
  22. data/lib/vident2/internals/resolver.rb +306 -0
  23. data/lib/vident2/internals/target_builder.rb +29 -0
  24. data/lib/vident2/phlex/html.rb +84 -0
  25. data/lib/vident2/phlex.rb +9 -0
  26. data/lib/vident2/stimulus/action.rb +140 -0
  27. data/lib/vident2/stimulus/class_map.rb +69 -0
  28. data/lib/vident2/stimulus/collection.rb +42 -0
  29. data/lib/vident2/stimulus/controller.rb +59 -0
  30. data/lib/vident2/stimulus/naming.rb +26 -0
  31. data/lib/vident2/stimulus/null.rb +16 -0
  32. data/lib/vident2/stimulus/outlet.rb +113 -0
  33. data/lib/vident2/stimulus/param.rb +62 -0
  34. data/lib/vident2/stimulus/target.rb +57 -0
  35. data/lib/vident2/stimulus/value.rb +77 -0
  36. data/lib/vident2/tailwind.rb +19 -0
  37. data/lib/vident2/version.rb +5 -0
  38. data/lib/vident2/view_component/base.rb +124 -0
  39. data/lib/vident2/view_component.rb +9 -0
  40. data/lib/vident2.rb +50 -0
  41. data/skills/vident/SKILL.md +11 -2
  42. data/skills/vident/api-reference.md +518 -0
  43. data/skills/vident/examples.md +492 -0
  44. metadata +35 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2966a65af9fba6bbddf4ffe3c93868a99e6c281cc992db859408dbb4c46050be
4
- data.tar.gz: e7224204b0d79fe83df5c3a98f0f779ab035645b7d6fabee01818341c6089480
3
+ metadata.gz: 5a79dcf9aadabd901dedc14da649f0aa2bee309caa0e17c7c10f0c3afea4c863
4
+ data.tar.gz: 34115cae656bdc0ef14f21f79aa0e98afa131b517cadd2aee5bf2f5fe6781074
5
5
  SHA512:
6
- metadata.gz: 7b1a90bb87dc01712457acfa7b28d6dc3379cce5a66fa635dfbf6f357d1ecb2e635a563330e4f8facc88f3c213ec7309a40193d20a55e391f33f05ec418130cb
7
- data.tar.gz: 5a9aecbe18952241b8b53788a41808e8ba43968c4adc4989aa1184deb683cd91b1b680ee5ff1e697746ceb8fa7cceb0f000ee051f7da7d34f6b1dae48b0952a4
6
+ metadata.gz: 9e3c2a2a1ea99972a07fe4e50b5652aed7a6971e389984d997f9f08509f78c3310f6b1e506e4aa7d18622055937a0cacdf58ef19e72599499fdf9af73a32fa7d
7
+ data.tar.gz: d40bb597e686af902916aebc6dc26b1b4bf803dfb3c4abcac1528140106fe601a229d19ac9f51b0c0d3d2a560548f29ade5356084328e6eaa99aa4946dfc75b9
data/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ 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
+ ## [1.0.2] - 2026-04-21
10
+
11
+ ### Changed
12
+
13
+ - 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.
14
+
15
+ ### Added
16
+
17
+ - `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.
18
+
19
+
9
20
  ## [1.0.0] - 2026-04-19
10
21
 
11
22
  ### Breaking
data/README.md CHANGED
@@ -415,7 +415,10 @@ class DynamicComponent < Vident::ViewComponent::Base
415
415
  end
416
416
  ```
417
417
 
418
- Procs have access to instance variables, component methods, and Rails helpers.
418
+ 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:
419
+
420
+ - **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.
421
+ - **ViewComponent**: call `helpers.<method>` or `view_context.<method>` directly.
419
422
 
420
423
  **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
424
 
@@ -6,14 +6,16 @@ module Vident
6
6
 
7
7
  private
8
8
 
9
- # Prepare attributes set at initialization, which will later be merged together before rendering.
9
+ # Prepare attributes set at initialization. The DSL's static entries are
10
+ # merged in here so user-land `after_component_initialize` mutators append
11
+ # after them (preserving DSL-first ordering). DSL procs are NOT resolved
12
+ # here — they run at render time via `resolve_stimulus_attributes_at_render_time`
13
+ # so they can reach `helpers` / `view_context`.
10
14
  def prepare_component_attributes
11
15
  prepare_stimulus_collections
12
16
 
13
- # Add stimulus attributes from DSL first (lower precedence)
14
- add_stimulus_attributes_from_dsl
17
+ add_stimulus_attributes_from_dsl(phase: :static)
15
18
 
16
- # Process root_element_attributes (higher precedence)
17
19
  extra = root_element_attributes
18
20
  @html_options = (extra[:html_options] || {}).merge(@html_options) if extra.key?(:html_options)
19
21
  @root_element_attributes_classes = extra[:classes]
@@ -23,6 +25,19 @@ module Vident
23
25
  Stimulus::PRIMITIVES.each do |primitive|
24
26
  send(mutator_method(primitive), extra[primitive.key]) if extra.key?(primitive.key)
25
27
  end
28
+
29
+ @stimulus_proc_attributes_resolved = false
30
+ end
31
+
32
+ # Render-phase: resolve DSL proc entries against the component instance,
33
+ # now that `helpers` / `view_context` are wired. Triggered by Phlex's
34
+ # `before_template` and ViewComponent's `before_render`. Idempotent —
35
+ # `stimulus_data_attributes` also calls this as a safety net.
36
+ def resolve_stimulus_attributes_at_render_time
37
+ return if @stimulus_proc_attributes_resolved
38
+ @stimulus_proc_attributes_resolved = true
39
+
40
+ add_stimulus_attributes_from_dsl(phase: :procs)
26
41
  end
27
42
 
28
43
  def resolve_root_element_attributes_before_render(root_element_html_options = nil)
@@ -51,10 +66,13 @@ module Vident
51
66
  final_attributes.merge!(other_html_options.except(:data))
52
67
  end
53
68
 
54
- # Run every DSL attribute through its `add_stimulus_*` mutator. `values_from_props`
55
- # is a sidecar on values, resolved at instance render time.
56
- def add_stimulus_attributes_from_dsl
57
- dsl_attrs = self.class.stimulus_dsl_attributes(self)
69
+ # Run DSL attributes through their `add_stimulus_*` mutators. `phase:` is
70
+ # forwarded to the builder: `:static` skips procs (init-time), `:procs`
71
+ # skips non-procs (render-time), `:all` resolves everything.
72
+ # `values_from_props` is a sidecar on values, resolved at instance
73
+ # render time (only during the static phase since it has no procs).
74
+ def add_stimulus_attributes_from_dsl(phase: :all)
75
+ dsl_attrs = self.class.stimulus_dsl_attributes(self, phase:)
58
76
  return if dsl_attrs.empty?
59
77
 
60
78
  Stimulus::PRIMITIVES.each do |primitive|
@@ -80,6 +98,7 @@ module Vident
80
98
  end
81
99
 
82
100
  def stimulus_data_attributes
101
+ resolve_stimulus_attributes_at_render_time
83
102
  collections = Stimulus::PRIMITIVES.to_h { |primitive| [primitive.name, instance_variable_get(collection_ivar(primitive))] }
84
103
  StimulusDataAttributeBuilder.new(**collections).build
85
104
  end
@@ -8,6 +8,9 @@ module Vident
8
8
  # Getter for a stimulus classes list so can be used in view to set initial state on SSR
9
9
  # Returns a String of classes that can be used in a `class` attribute.
10
10
  def class_list_for_stimulus_classes(*names)
11
+ # DSL proc entries are resolved lazily at render time; trigger them now
12
+ # so procs that use only instance state work from ERB/template.
13
+ resolve_stimulus_attributes_at_render_time if respond_to?(:resolve_stimulus_attributes_at_render_time, true)
11
14
  ClassListBuilder.new(tailwind_merger:).build(
12
15
  @stimulus_classes_collection&.to_a,
13
16
  stimulus_class_names: names
@@ -62,14 +62,23 @@ module Vident
62
62
  self
63
63
  end
64
64
 
65
- def to_attributes(component_instance)
65
+ # `phase:` controls which entries are resolved.
66
+ # - `:static` — resolve non-proc entries only; procs are skipped entirely
67
+ # (no placeholder). Used at init time, before helpers/view_context exist.
68
+ # - `:procs` — resolve proc entries only; non-procs are skipped. Used at
69
+ # render time so procs can reach `helpers` / `view_context`.
70
+ # - `:all` — resolve everything (legacy path, retained for safety).
71
+ def to_attributes(component_instance, phase: :all)
66
72
  attrs = {}
67
73
  DSL_PRIMITIVES.each do |primitive|
68
74
  entries = @entries[primitive.name]
69
75
  next if entries.empty?
70
- attrs[primitive.key] = resolve_entries(primitive, entries, component_instance)
76
+ resolved = resolve_entries(primitive, entries, component_instance, phase:)
77
+ attrs[primitive.key] = resolved unless resolved.nil? || resolved.empty?
78
+ end
79
+ if phase != :procs && !@values_from_props.empty?
80
+ attrs[:stimulus_values_from_props] = @values_from_props.dup
71
81
  end
72
- attrs[:stimulus_values_from_props] = @values_from_props.dup unless @values_from_props.empty?
73
82
  attrs
74
83
  end
75
84
 
@@ -87,19 +96,24 @@ module Vident
87
96
  # Outlets don't support procs — static merge only. The other keyed kinds
88
97
  # and the positional (Array-shaped) kinds resolve procs in the component
89
98
  # instance and drop nil results.
90
- def resolve_entries(primitive, entries, component_instance)
91
- return entries.dup if primitive.name == :outlets
99
+ def resolve_entries(primitive, entries, component_instance, phase:)
100
+ if primitive.name == :outlets
101
+ return (phase == :procs) ? {} : entries.dup
102
+ end
92
103
 
93
104
  if primitive.keyed?
94
- resolve_hash_filtering_nil(entries, component_instance)
105
+ resolve_hash_filtering_nil(entries, component_instance, phase:)
95
106
  else
96
- resolve_array_filtering_nil(entries, component_instance)
107
+ resolve_array_filtering_nil(entries, component_instance, phase:)
97
108
  end
98
109
  end
99
110
 
100
- def resolve_array_filtering_nil(array, component_instance)
111
+ def resolve_array_filtering_nil(array, component_instance, phase:)
101
112
  array.each_with_object([]) do |value, out|
102
- resolved = callable?(value) ? component_instance.instance_exec(&value) : value
113
+ is_proc = callable?(value)
114
+ next if phase == :static && is_proc
115
+ next if phase == :procs && !is_proc
116
+ resolved = is_proc ? component_instance.instance_exec(&value) : value
103
117
  out << resolved unless resolved.nil?
104
118
  end
105
119
  end
@@ -107,9 +121,12 @@ module Vident
107
121
  # Dropping nil matters because Stimulus's Boolean value parser reads an
108
122
  # empty data attribute as `true` — so `-> { flag? || nil }` would silently
109
123
  # flip a Boolean value on. Omitting the entry keeps the attribute off.
110
- def resolve_hash_filtering_nil(hash, component_instance)
124
+ def resolve_hash_filtering_nil(hash, component_instance, phase:)
111
125
  hash.each_with_object({}) do |(key, value), out|
112
- resolved = callable?(value) ? component_instance.instance_exec(&value) : value
126
+ is_proc = callable?(value)
127
+ next if phase == :static && is_proc
128
+ next if phase == :procs && !is_proc
129
+ resolved = is_proc ? component_instance.instance_exec(&value) : value
113
130
  out[key] = resolved unless resolved.nil?
114
131
  end
115
132
  end
@@ -19,16 +19,16 @@ module Vident
19
19
  @stimulus_builder.instance_eval(&block)
20
20
  end
21
21
 
22
- def stimulus_dsl_attributes(component_instance)
22
+ def stimulus_dsl_attributes(component_instance, phase: :all)
23
23
  # If no stimulus blocks have been defined on this class, check parent
24
24
  if @stimulus_builder.nil? && superclass.respond_to?(:stimulus_dsl_attributes)
25
- return superclass.stimulus_dsl_attributes(component_instance)
25
+ return superclass.stimulus_dsl_attributes(component_instance, phase:)
26
26
  end
27
27
 
28
28
  # Ensure inheritance is applied at access time
29
29
  ensure_inheritance_merged
30
30
 
31
- @stimulus_builder&.to_attributes(component_instance) || {}
31
+ @stimulus_builder&.to_attributes(component_instance, phase:) || {}
32
32
  end
33
33
 
34
34
  private
@@ -51,7 +51,7 @@ module Vident
51
51
  end
52
52
 
53
53
  # Instance method to get DSL attributes for this component instance
54
- def stimulus_dsl_attributes = self.class.stimulus_dsl_attributes(self)
54
+ def stimulus_dsl_attributes(phase: :all) = self.class.stimulus_dsl_attributes(self, phase:)
55
55
 
56
56
  # Instance method to resolve prop-mapped values at runtime
57
57
  def resolve_values_from_props(prop_names)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vident
4
- VERSION = "1.0.1"
4
+ VERSION = "1.0.2"
5
5
 
6
6
  # Shared version for all vident gems
7
7
  def self.version
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+
5
+ module Vident2
6
+ # Fragment-caching opt-in. Include into a component to get `cacheable?`,
7
+ # `cache_key`, and the `with_cache_key` / `depends_on` class helpers.
8
+ #
9
+ # `cache_component_modified_time` lives on the adapter base class
10
+ # (Phlex: `.rb` mtime; VC: sidecar template + `.rb` mtime).
11
+ module Caching
12
+ extend ActiveSupport::Concern
13
+
14
+ class_methods do
15
+ def inherited(subclass)
16
+ subclass.instance_variable_set(:@named_cache_key_attributes, @named_cache_key_attributes&.clone)
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 StandardError, "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
+ key = "#{self.class.name}/#{cache_keys_for_sources(key_attributes).join("/")}"
89
+ raise StandardError, "Cache key for key #{key} is blank!" if key.blank?
90
+ @cache_key[index] = cache_key_modifier.present? ? "#{key}/#{cache_key_modifier}" : key
91
+ end
92
+ end
93
+ end