vident 1.0.2 → 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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/README.md +45 -17
  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/{vident2 → vident}/internals/action_builder.rb +18 -22
  22. data/lib/vident/internals/attribute_writer.rb +17 -0
  23. data/lib/{vident2 → vident}/internals/class_list_builder.rb +5 -22
  24. data/lib/vident/internals/declaration.rb +13 -0
  25. data/lib/{vident2 → vident}/internals/declarations.rb +6 -18
  26. data/lib/{vident2 → vident}/internals/draft.rb +3 -16
  27. data/lib/{vident2 → vident}/internals/dsl.rb +6 -32
  28. data/lib/vident/internals/plan.rb +9 -0
  29. data/lib/vident/internals/registry.rb +37 -0
  30. data/lib/{vident2 → vident}/internals/resolver.rb +101 -91
  31. data/lib/{vident2 → vident}/internals/target_builder.rb +1 -7
  32. data/lib/vident/stable_id.rb +3 -3
  33. data/lib/{vident2 → vident}/stimulus/action.rb +11 -24
  34. data/lib/vident/stimulus/base.rb +26 -0
  35. data/lib/{vident2 → vident}/stimulus/class_map.rb +6 -18
  36. data/lib/{vident2 → vident}/stimulus/collection.rb +6 -8
  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/{vident2 → vident}/stimulus/outlet.rb +12 -32
  42. data/lib/{vident2 → vident}/stimulus/param.rb +5 -11
  43. data/lib/{vident2 → vident}/stimulus/target.rb +5 -14
  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 +122 -19
  51. data/skills/vident/api-reference.md +259 -115
  52. data/skills/vident/examples.md +23 -10
  53. metadata +38 -60
  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 -106
  57. data/lib/vident/component_class_lists.rb +0 -37
  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 -136
  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
  81. data/lib/vident2/caching.rb +0 -93
  82. data/lib/vident2/component.rb +0 -538
  83. data/lib/vident2/engine.rb +0 -18
  84. data/lib/vident2/error.rb +0 -30
  85. data/lib/vident2/internals/attribute_writer.rb +0 -22
  86. data/lib/vident2/internals/declaration.rb +0 -17
  87. data/lib/vident2/internals/plan.rb +0 -12
  88. data/lib/vident2/internals/registry.rb +0 -41
  89. data/lib/vident2/phlex/html.rb +0 -84
  90. data/lib/vident2/phlex.rb +0 -9
  91. data/lib/vident2/stimulus/controller.rb +0 -59
  92. data/lib/vident2/stimulus/naming.rb +0 -26
  93. data/lib/vident2/stimulus/null.rb +0 -16
  94. data/lib/vident2/stimulus/value.rb +0 -77
  95. data/lib/vident2/tailwind.rb +0 -19
  96. data/lib/vident2/version.rb +0 -5
  97. data/lib/vident2/view_component/base.rb +0 -124
  98. data/lib/vident2/view_component.rb +0 -9
  99. data/lib/vident2.rb +0 -50
@@ -1,67 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/core_ext/string/inflections"
4
-
5
- require "json"
6
-
7
- module Vident
8
- class StimulusAttributeBase
9
- # `"admin/users"` → `"admin--users"`; accepts Symbol or String.
10
- def self.stimulize_path(path)
11
- path.to_s.split("/").map(&:dasherize).join("--")
12
- end
13
-
14
- # `:my_thing` → `"myThing"`
15
- def self.js_name(name)
16
- name.to_s.camelize(:lower)
17
- end
18
-
19
- attr_reader :implied_controller
20
-
21
- def initialize(*args, implied_controller: nil)
22
- @implied_controller = implied_controller
23
- parse_arguments(*args)
24
- end
25
-
26
- def inspect = "#<#{self.class.name} #{to_h}>"
27
-
28
- def to_s = raise(NoMethodError, "Subclasses must implement to_s")
29
-
30
- def to_h = {data_attribute_name => data_attribute_value}
31
-
32
- alias_method :to_hash, :to_h
33
-
34
- def data_attribute_name = raise(NoMethodError, "Subclasses must implement data_attribute_name")
35
-
36
- def data_attribute_value = raise(NoMethodError, "Subclasses must implement data_attribute_value")
37
-
38
- def implied_controller_path
39
- raise ArgumentError, "implied_controller is required to get implied controller path" unless implied_controller
40
- implied_controller.path
41
- end
42
-
43
- def implied_controller_name
44
- raise ArgumentError, "implied_controller is required to get implied controller name" unless implied_controller
45
- implied_controller.name
46
- end
47
-
48
- private
49
-
50
- def stimulize_path(path) = self.class.stimulize_path(path)
51
-
52
- def js_name(name) = self.class.js_name(name)
53
-
54
- # Arrays/Hashes serialise as JSON; everything else via `to_s` (which is how
55
- # `Vident::StimulusNull` emits the literal `"null"`).
56
- def serialize_value(value)
57
- case value
58
- when Array, Hash then value.to_json
59
- else value.to_s
60
- end
61
- end
62
-
63
- def parse_arguments(*args)
64
- raise NotImplementedError, "Subclasses must implement parse_arguments"
65
- end
66
- end
67
- end
@@ -1,129 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- module StimulusAttributes
5
- extend ActiveSupport::Concern
6
- # `extend` + `include` so Naming helpers are callable both in the module
7
- # body (outside define_method args) and inside define_method blocks
8
- # (at instance call-time).
9
- extend Stimulus::Naming
10
- include Stimulus::Naming
11
-
12
- class_methods do
13
- # Symbol so the action parser treats it as a Stimulus event type.
14
- def stimulus_scoped_event(event)
15
- :"#{component_name}:#{stimulus_js_name(event)}"
16
- end
17
-
18
- def stimulus_scoped_event_on_window(event)
19
- :"#{component_name}:#{stimulus_js_name(event)}@window"
20
- end
21
-
22
- private
23
-
24
- def stimulus_js_name(name) = name.to_s.camelize(:lower)
25
- end
26
-
27
- def stimulus_controller(*args)
28
- return args.first if args.length == 1 && args.first.is_a?(StimulusController)
29
- StimulusController.new(*args, implied_controller: implied_controller_path)
30
- end
31
-
32
- # Plural parsers `stimulus_<kind>s(*args)` — generated from the primitives
33
- # registry below. Each accepts: pre-built Value (pass-through), pre-built
34
- # Collection (unwrapped; a single one is returned as-is), Array (splatted
35
- # into the singular builder), Hash (expanded per-pair for
36
- # `hash_expands: true`, single-arg descriptor otherwise), else passed to
37
- # the singular builder. Methods defined this way: `stimulus_controllers`,
38
- # `stimulus_actions`, `stimulus_targets`, `stimulus_outlets`,
39
- # `stimulus_values`, `stimulus_params`, `stimulus_classes`.
40
- Stimulus::PRIMITIVES.each do |primitive|
41
- define_method(primitive.key) do |*args|
42
- collection_class = primitive.collection_class
43
- return collection_class.new if args.empty? || args.all?(&:blank?)
44
- return args.first if args.length == 1 && args.first.is_a?(collection_class)
45
-
46
- singular = primitive.singular_key
47
- converted = []
48
- args.each do |arg|
49
- case arg
50
- when primitive.value_class then converted << arg
51
- when collection_class then converted.concat(arg.to_a)
52
- when Hash
53
- if primitive.keyed?
54
- arg.each { |name, val| converted << send(singular, name, val) }
55
- else
56
- converted << send(singular, arg)
57
- end
58
- when Array then converted << send(singular, *arg)
59
- else converted << send(singular, arg)
60
- end
61
- end
62
- collection_class.new(converted)
63
- end
64
- end
65
-
66
- def stimulus_action(*args)
67
- return args.first if args.length == 1 && args.first.is_a?(StimulusAction)
68
- StimulusAction.new(*args, implied_controller:)
69
- end
70
-
71
- def stimulus_target(*args)
72
- return args.first if args.length == 1 && args.first.is_a?(StimulusTarget)
73
- StimulusTarget.new(*args, implied_controller:)
74
- end
75
-
76
- # `component_id: @id` scopes the auto-generated selector to this component
77
- # instance (e.g. `#<host-id> [data-controller~=<outlet>]`).
78
- def stimulus_outlet(*args)
79
- return args.first if args.length == 1 && args.first.is_a?(StimulusOutlet)
80
- StimulusOutlet.new(*args, implied_controller:, component_id: @id)
81
- end
82
-
83
- def stimulus_value(*args)
84
- return args.first if args.length == 1 && args.first.is_a?(StimulusValue)
85
- StimulusValue.new(*args, implied_controller:)
86
- end
87
-
88
- def stimulus_param(*args)
89
- return args.first if args.length == 1 && args.first.is_a?(StimulusParam)
90
- StimulusParam.new(*args, implied_controller:)
91
- end
92
-
93
- def stimulus_class(*args)
94
- return args.first if args.length == 1 && args.first.is_a?(StimulusClass)
95
- StimulusClass.new(*args, implied_controller:)
96
- end
97
-
98
- # Mutators `add_stimulus_<kind>s` — build from input, merge into the
99
- # per-kind collection ivar. Methods defined: `add_stimulus_controllers`,
100
- # `add_stimulus_actions`, `add_stimulus_targets`, `add_stimulus_outlets`,
101
- # `add_stimulus_values`, `add_stimulus_params`, `add_stimulus_classes`.
102
- Stimulus::PRIMITIVES.each do |primitive|
103
- define_method(mutator_method(primitive)) do |input|
104
- added = send(primitive.key, *Array.wrap(input))
105
- existing = instance_variable_get(collection_ivar(primitive))
106
- instance_variable_set(collection_ivar(primitive), existing ? existing.merge(added) : added)
107
- end
108
- end
109
-
110
- def stimulus_scoped_event(event) = self.class.stimulus_scoped_event(event)
111
-
112
- def stimulus_scoped_event_on_window(event) = self.class.stimulus_scoped_event_on_window(event)
113
-
114
- private
115
-
116
- def implied_controller
117
- StimulusController.new(implied_controller: implied_controller_path)
118
- end
119
-
120
- # The first registered controller path becomes the implied controller for
121
- # unqualified DSL entries (e.g. `actions :click` → `implied#click`).
122
- def implied_controller_path
123
- return @implied_controller_path if defined?(@implied_controller_path)
124
- path = Array.wrap(@stimulus_controllers).first
125
- raise(StandardError, "No controllers have been specified") unless path
126
- @implied_controller_path = path
127
- end
128
- end
129
- end
@@ -1,136 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- class StimulusBuilder
5
- # Primitives the DSL block tracks. Controllers are set via the component's
6
- # `stimulus_controllers:` prop, not the DSL, so they're skipped here.
7
- # Storage shape per primitive is an Array for positional kinds (actions,
8
- # targets) and a Hash for keyed kinds (values, params, classes, outlets).
9
- DSL_PRIMITIVES = Stimulus::PRIMITIVES.reject { |primitive| primitive.name == :controllers }.freeze
10
-
11
- def initialize
12
- @entries = DSL_PRIMITIVES.to_h { |primitive| [primitive.name, primitive.keyed? ? {} : []] }
13
- @values_from_props = []
14
- end
15
-
16
- def merge_with(other)
17
- DSL_PRIMITIVES.each do |primitive|
18
- mine = @entries[primitive.name]
19
- theirs = other.entries_for(primitive.name)
20
- primitive.keyed? ? mine.merge!(theirs) : mine.concat(theirs)
21
- end
22
- @values_from_props.concat(other.values_from_props_list)
23
- self
24
- end
25
-
26
- def actions(*names)
27
- @entries[:actions].concat(names)
28
- self
29
- end
30
-
31
- def targets(*names)
32
- @entries[:targets].concat(names)
33
- self
34
- end
35
-
36
- def values(**hash)
37
- @entries[:values].merge!(hash) unless hash.empty?
38
- self
39
- end
40
-
41
- def params(**hash)
42
- @entries[:params].merge!(hash) unless hash.empty?
43
- self
44
- end
45
-
46
- def classes(**hash)
47
- @entries[:classes].merge!(hash) unless hash.empty?
48
- self
49
- end
50
-
51
- def values_from_props(*names)
52
- @values_from_props.concat(names)
53
- self
54
- end
55
-
56
- # `outlets({"admin--users" => ".sel"})` accepts a positional Hash for
57
- # identifiers that can't be Ruby kwarg keys (contain `--`).
58
- def outlets(positional = nil, **hash)
59
- bucket = @entries[:outlets]
60
- bucket.merge!(positional) if positional.is_a?(Hash)
61
- bucket.merge!(hash) unless hash.empty?
62
- self
63
- end
64
-
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)
72
- attrs = {}
73
- DSL_PRIMITIVES.each do |primitive|
74
- entries = @entries[primitive.name]
75
- next if entries.empty?
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
81
- end
82
- attrs
83
- end
84
-
85
- def to_hash(component_instance) = to_attributes(component_instance)
86
- alias_method :to_h, :to_hash
87
-
88
- protected
89
-
90
- def entries_for(name) = @entries[name]
91
-
92
- def values_from_props_list = @values_from_props
93
-
94
- private
95
-
96
- # Outlets don't support procs — static merge only. The other keyed kinds
97
- # and the positional (Array-shaped) kinds resolve procs in the component
98
- # instance and drop nil results.
99
- def resolve_entries(primitive, entries, component_instance, phase:)
100
- if primitive.name == :outlets
101
- return (phase == :procs) ? {} : entries.dup
102
- end
103
-
104
- if primitive.keyed?
105
- resolve_hash_filtering_nil(entries, component_instance, phase:)
106
- else
107
- resolve_array_filtering_nil(entries, component_instance, phase:)
108
- end
109
- end
110
-
111
- def resolve_array_filtering_nil(array, component_instance, phase:)
112
- array.each_with_object([]) do |value, out|
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
117
- out << resolved unless resolved.nil?
118
- end
119
- end
120
-
121
- # Dropping nil matters because Stimulus's Boolean value parser reads an
122
- # empty data attribute as `true` — so `-> { flag? || nil }` would silently
123
- # flip a Boolean value on. Omitting the entry keeps the attribute off.
124
- def resolve_hash_filtering_nil(hash, component_instance, phase:)
125
- hash.each_with_object({}) do |(key, value), out|
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
130
- out[key] = resolved unless resolved.nil?
131
- end
132
- end
133
-
134
- def callable?(value) = value.respond_to?(:call)
135
- end
136
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- class StimulusClass < StimulusAttributeBase
5
- attr_reader :controller, :class_name, :css_classes
6
-
7
- def to_s = @css_classes.join(" ")
8
-
9
- def data_attribute_name = "#{@controller}-#{@class_name}-class"
10
-
11
- def data_attribute_value = to_s
12
-
13
- private
14
-
15
- def parse_arguments(*args)
16
- case args.size
17
- when 2
18
- parse_two_arguments(args[0], args[1])
19
- when 3
20
- parse_three_arguments(args[0], args[1], args[2])
21
- else
22
- raise ArgumentError, "Invalid number of arguments: #{args.size}"
23
- end
24
- end
25
-
26
- def parse_two_arguments(class_name, css_classes)
27
- if class_name.is_a?(Symbol)
28
- # class name on implied controller + css classes
29
- @controller = implied_controller_name
30
- @class_name = class_name.to_s.dasherize
31
- @css_classes = normalize_css_classes(css_classes)
32
- else
33
- raise ArgumentError, "Invalid argument types: #{class_name.class}, #{css_classes.class}"
34
- end
35
- end
36
-
37
- def parse_three_arguments(controller, class_name, css_classes)
38
- if controller.is_a?(String) && class_name.is_a?(Symbol)
39
- # controller + class name + css classes
40
- @controller = stimulize_path(controller)
41
- @class_name = class_name.to_s.dasherize
42
- @css_classes = normalize_css_classes(css_classes)
43
- else
44
- raise ArgumentError, "Invalid argument types: #{controller.class}, #{class_name.class}, #{css_classes.class}"
45
- end
46
- end
47
-
48
- def normalize_css_classes(css_classes)
49
- case css_classes
50
- when String
51
- css_classes.split(/\s+/).reject(&:empty?)
52
- when Array
53
- css_classes.map(&:to_s).reject(&:empty?)
54
- else
55
- raise ArgumentError, "CSS classes must be a String or Array, got #{css_classes.class}"
56
- end
57
- end
58
- end
59
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- class StimulusClassCollection < StimulusCollectionBase
5
- def to_h
6
- return {} if items.empty?
7
-
8
- items.each_with_object({}) { |css_class, merged| merged.merge!(css_class.to_h) }
9
- end
10
- end
11
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- class StimulusCollectionBase
5
- def initialize(items = [])
6
- @items = Array(items).flatten.compact
7
- end
8
-
9
- def <<(item)
10
- @items << item
11
- self
12
- end
13
-
14
- def to_h
15
- raise NoMethodError, "Subclasses must implement to_h"
16
- end
17
-
18
- def to_a = @items.dup
19
-
20
- def to_hash = to_h
21
-
22
- def empty? = @items.empty?
23
-
24
- def any? = !empty?
25
-
26
- def merge(*other_collections)
27
- merged = self.class.new
28
- merged.instance_variable_set(:@items, @items.dup)
29
-
30
- other_collections.each do |collection|
31
- next unless collection.is_a?(self.class)
32
- merged.instance_variable_get(:@items).concat(collection.instance_variable_get(:@items))
33
- end
34
-
35
- merged
36
- end
37
-
38
- def self.merge(*collections)
39
- return new if collections.empty?
40
-
41
- first_collection = collections.first
42
- return first_collection if collections.size == 1
43
-
44
- first_collection.merge(*collections[1..-1])
45
- end
46
-
47
- protected
48
-
49
- attr_reader :items
50
- end
51
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- module StimulusComponent
5
- extend ActiveSupport::Concern
6
-
7
- include StimulusAttributes
8
-
9
- # Thin back-compat alias; see `Vident::StimulusAttributeBase.stimulize_path`
10
- # for the canonical implementation.
11
- def stimulus_identifier_from_path(path)
12
- StimulusAttributeBase.stimulize_path(path)
13
- end
14
- module_function :stimulus_identifier_from_path
15
-
16
- # Base class for all Vident components, which provides common functionality and properties.
17
-
18
- class_methods do
19
- def no_stimulus_controller
20
- @no_stimulus_controller = true
21
- end
22
-
23
- def stimulus_controller? = !@no_stimulus_controller
24
-
25
- # The "path" of the Stimulus controller, which is used to generate the controller name.
26
- def stimulus_identifier_path = name&.underscore || "anonymous_component"
27
-
28
- # Stimulus controller identifier
29
- def stimulus_identifier = StimulusComponent.stimulus_identifier_from_path(stimulus_identifier_path)
30
-
31
- # The "name" of the component from its class name and namespace. This is used to generate an HTML class name
32
- # that can helps identify the component type in the DOM or for styling purposes.
33
- def component_name
34
- @component_name ||= stimulus_identifier
35
- end
36
- end
37
-
38
- # Components have the following properties
39
- included do
40
- extend Literal::Properties
41
-
42
- # StimulusJS support
43
- # # TODO: revisit inputs and how many ways of specifying the same thing...
44
- prop :stimulus_controllers, _Array(_Union(String, Symbol, StimulusController, StimulusControllerCollection)), default: -> do
45
- if self.class.stimulus_controller?
46
- [default_controller_path]
47
- else
48
- []
49
- end
50
- end
51
- prop :stimulus_actions, _Array(_Union(String, Symbol, Array, Hash, StimulusAction, StimulusAction::Descriptor, StimulusActionCollection)), default: -> { [] }
52
- prop :stimulus_targets, _Array(_Union(String, Symbol, Array, Hash, StimulusTarget, StimulusTargetCollection)), default: -> { [] }
53
- prop :stimulus_outlets, _Array(_Union(String, Symbol, StimulusOutlet, StimulusOutletCollection)), default: -> { [] }
54
- prop :stimulus_outlet_host, _Nilable(Vident::Component) # A component that will host this component as an outlet
55
- prop :stimulus_values, _Union(_Hash(Symbol, _Any), Array, StimulusValue, StimulusValueCollection), default: -> { {} } # TODO: instead of _Any, is it _Interface(:to_s)?
56
- prop :stimulus_params, _Union(_Hash(Symbol, _Any), Array, StimulusParam, StimulusParamCollection), default: -> { {} }
57
- prop :stimulus_classes, _Union(_Hash(Symbol, String), Array, StimulusClass, StimulusClassCollection), default: -> { {} }
58
- end
59
-
60
- # If connecting an outlet to this specific component instance, use this ID
61
- def outlet_id
62
- @outlet_id ||= [stimulus_identifier, "##{id}"]
63
- end
64
-
65
- # The Stimulus controller identifier for this component
66
- def stimulus_identifier = self.class.stimulus_identifier
67
-
68
- # An name that can helps identify the component type in the DOM or for styling purposes (its also used as a class name on the root element)
69
- def component_name = self.class.component_name
70
-
71
- # The `component` class name is used to create the controller name.
72
- # The path of the Stimulus controller when none is explicitly set
73
- def default_controller_path = self.class.stimulus_identifier_path
74
- end
75
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- class StimulusController < StimulusAttributeBase
5
- attr_reader :path, :name
6
-
7
- def to_s = name
8
-
9
- def data_attribute_name = "controller"
10
-
11
- def data_attribute_value = name
12
-
13
- private
14
-
15
- # `@implied_controller` on this class is a raw path String (not a
16
- # StimulusController instance as on the base), so the base class's
17
- # `.path` / `.name` accessors don't apply and we override.
18
- def implied_controller_path
19
- raise ArgumentError, "implied_controller is required to get implied controller path" unless @implied_controller
20
- @implied_controller
21
- end
22
-
23
- def implied_controller_name
24
- raise ArgumentError, "implied_controller is required to get implied controller name" unless @implied_controller
25
- stimulize_path(@implied_controller)
26
- end
27
-
28
- def parse_arguments(*args)
29
- case args.size
30
- when 0
31
- @path = implied_controller_path
32
- @name = implied_controller_name
33
- when 1
34
- @path = args[0].to_s
35
- @name = stimulize_path(@path)
36
- else
37
- raise ArgumentError, "Invalid number of arguments: #{args.size}. Expected 0 or 1 argument."
38
- end
39
- end
40
- end
41
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- class StimulusControllerCollection < StimulusCollectionBase
5
- def to_h
6
- return {} if items.empty?
7
-
8
- controller_values = items.map(&:to_s).reject(&:empty?)
9
- return {} if controller_values.empty?
10
-
11
- {controller: controller_values.join(" ")}
12
- end
13
- end
14
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- class StimulusDataAttributeBuilder
5
- def initialize(**collections_by_name)
6
- unknown = collections_by_name.keys - Stimulus.names
7
- raise ArgumentError, "Unknown stimulus primitive(s) #{unknown.inspect}" if unknown.any?
8
-
9
- @collections_by_name = collections_by_name.transform_values { |v| Array(v) }
10
- end
11
-
12
- def build
13
- Stimulus::PRIMITIVES.each_with_object({}) do |primitive, attrs|
14
- attrs.merge!(merge_collection(primitive.collection_class, @collections_by_name[primitive.name] || []))
15
- end.transform_keys(&:to_s).compact
16
- end
17
-
18
- private
19
-
20
- # Items are either pre-built collections (DSL / resolver path) or raw value
21
- # objects (child_element path). Merge-or-wrap accordingly.
22
- def merge_collection(collection_class, items)
23
- return {} if items.empty?
24
-
25
- if items.first.is_a?(collection_class)
26
- collection_class.merge(*items).to_h
27
- else
28
- collection_class.new(items).to_h
29
- end
30
- end
31
- end
32
- end
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- module StimulusHelper
5
- extend ActiveSupport::Concern
6
-
7
- class_methods do
8
- def stimulus(&block)
9
- # Initialize stimulus builder if not already present
10
- if @stimulus_builder.nil?
11
- @stimulus_builder = StimulusBuilder.new
12
- @inheritance_merged = false
13
- end
14
-
15
- # Ensure inheritance is applied
16
- ensure_inheritance_merged
17
-
18
- # Execute the new block to add/merge new attributes
19
- @stimulus_builder.instance_eval(&block)
20
- end
21
-
22
- def stimulus_dsl_attributes(component_instance, phase: :all)
23
- # If no stimulus blocks have been defined on this class, check parent
24
- if @stimulus_builder.nil? && superclass.respond_to?(:stimulus_dsl_attributes)
25
- return superclass.stimulus_dsl_attributes(component_instance, phase:)
26
- end
27
-
28
- # Ensure inheritance is applied at access time
29
- ensure_inheritance_merged
30
-
31
- @stimulus_builder&.to_attributes(component_instance, phase:) || {}
32
- end
33
-
34
- private
35
-
36
- def ensure_inheritance_merged
37
- return if @inheritance_merged || @stimulus_builder.nil?
38
-
39
- if superclass.respond_to?(:stimulus_dsl_builder, true)
40
- parent_builder = superclass.send(:stimulus_dsl_builder)
41
- if parent_builder
42
- @stimulus_builder.merge_with(parent_builder)
43
- end
44
- end
45
- @inheritance_merged = true
46
- end
47
-
48
- protected
49
-
50
- def stimulus_dsl_builder = @stimulus_builder
51
- end
52
-
53
- # Instance method to get DSL attributes for this component instance
54
- def stimulus_dsl_attributes(phase: :all) = self.class.stimulus_dsl_attributes(self, phase:)
55
-
56
- # Instance method to resolve prop-mapped values at runtime
57
- def resolve_values_from_props(prop_names)
58
- return {} if prop_names.empty?
59
-
60
- prop_names.each_with_object({}) do |name, resolved|
61
- # Map from instance variable if it exists
62
- resolved[name] = instance_variable_get("@#{name}") if instance_variable_defined?("@#{name}")
63
- end
64
- end
65
- end
66
- end