vident 1.0.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +45 -17
- data/lib/vident/caching.rb +4 -110
- data/lib/vident/capabilities/caching.rb +98 -0
- data/lib/vident/capabilities/child_element_rendering.rb +92 -0
- data/lib/vident/capabilities/class_list_building.rb +23 -0
- data/lib/vident/capabilities/declarable.rb +39 -0
- data/lib/vident/capabilities/identifiable.rb +54 -0
- data/lib/vident/capabilities/inspectable.rb +17 -0
- data/lib/vident/capabilities/root_element_rendering.rb +31 -0
- data/lib/vident/capabilities/stimulus_data_emitting.rb +98 -0
- data/lib/vident/capabilities/stimulus_declaring.rb +79 -0
- data/lib/vident/capabilities/stimulus_draft.rb +51 -0
- data/lib/vident/capabilities/stimulus_mutation.rb +60 -0
- data/lib/vident/capabilities/stimulus_parsing.rb +144 -0
- data/lib/vident/capabilities/tailwind.rb +18 -0
- data/lib/vident/component.rb +14 -76
- data/lib/vident/engine.rb +6 -5
- data/lib/vident/error.rb +16 -0
- data/lib/{vident2 → vident}/internals/action_builder.rb +18 -22
- data/lib/vident/internals/attribute_writer.rb +17 -0
- data/lib/{vident2 → vident}/internals/class_list_builder.rb +5 -22
- data/lib/vident/internals/declaration.rb +13 -0
- data/lib/{vident2 → vident}/internals/declarations.rb +6 -18
- data/lib/{vident2 → vident}/internals/draft.rb +3 -16
- data/lib/{vident2 → vident}/internals/dsl.rb +6 -32
- data/lib/vident/internals/plan.rb +9 -0
- data/lib/vident/internals/registry.rb +37 -0
- data/lib/{vident2 → vident}/internals/resolver.rb +101 -91
- data/lib/{vident2 → vident}/internals/target_builder.rb +1 -7
- data/lib/vident/stable_id.rb +3 -3
- data/lib/{vident2 → vident}/stimulus/action.rb +11 -24
- data/lib/vident/stimulus/base.rb +26 -0
- data/lib/{vident2 → vident}/stimulus/class_map.rb +6 -18
- data/lib/{vident2 → vident}/stimulus/collection.rb +6 -8
- data/lib/vident/stimulus/combinable.rb +30 -0
- data/lib/vident/stimulus/controller.rb +45 -0
- data/lib/vident/stimulus/naming.rb +9 -9
- data/lib/vident/stimulus/null.rb +7 -0
- data/lib/{vident2 → vident}/stimulus/outlet.rb +12 -32
- data/lib/{vident2 → vident}/stimulus/param.rb +5 -11
- data/lib/{vident2 → vident}/stimulus/target.rb +5 -14
- data/lib/vident/stimulus/value.rb +57 -0
- data/lib/vident/stimulus_null.rb +4 -8
- data/lib/vident/tailwind.rb +4 -17
- data/lib/vident/types.rb +28 -0
- data/lib/vident/version.rb +1 -6
- data/lib/vident.rb +46 -36
- data/skills/vident/SKILL.md +122 -19
- data/skills/vident/api-reference.md +259 -115
- data/skills/vident/examples.md +23 -10
- metadata +38 -60
- data/lib/vident/child_element_helper.rb +0 -64
- data/lib/vident/class_list_builder.rb +0 -112
- data/lib/vident/component_attribute_resolver.rb +0 -106
- data/lib/vident/component_class_lists.rb +0 -37
- data/lib/vident/stimulus/primitive.rb +0 -38
- data/lib/vident/stimulus.rb +0 -31
- data/lib/vident/stimulus_action.rb +0 -133
- data/lib/vident/stimulus_action_collection.rb +0 -11
- data/lib/vident/stimulus_attribute_base.rb +0 -67
- data/lib/vident/stimulus_attributes.rb +0 -129
- data/lib/vident/stimulus_builder.rb +0 -136
- data/lib/vident/stimulus_class.rb +0 -59
- data/lib/vident/stimulus_class_collection.rb +0 -11
- data/lib/vident/stimulus_collection_base.rb +0 -51
- data/lib/vident/stimulus_component.rb +0 -75
- data/lib/vident/stimulus_controller.rb +0 -41
- data/lib/vident/stimulus_controller_collection.rb +0 -14
- data/lib/vident/stimulus_data_attribute_builder.rb +0 -32
- data/lib/vident/stimulus_helper.rb +0 -66
- data/lib/vident/stimulus_outlet.rb +0 -90
- data/lib/vident/stimulus_outlet_collection.rb +0 -11
- data/lib/vident/stimulus_param.rb +0 -42
- data/lib/vident/stimulus_param_collection.rb +0 -11
- data/lib/vident/stimulus_target.rb +0 -47
- data/lib/vident/stimulus_target_collection.rb +0 -18
- data/lib/vident/stimulus_value.rb +0 -39
- data/lib/vident/stimulus_value_collection.rb +0 -11
- data/lib/vident2/caching.rb +0 -93
- data/lib/vident2/component.rb +0 -538
- data/lib/vident2/engine.rb +0 -18
- data/lib/vident2/error.rb +0 -30
- data/lib/vident2/internals/attribute_writer.rb +0 -22
- data/lib/vident2/internals/declaration.rb +0 -17
- data/lib/vident2/internals/plan.rb +0 -12
- data/lib/vident2/internals/registry.rb +0 -41
- data/lib/vident2/phlex/html.rb +0 -84
- data/lib/vident2/phlex.rb +0 -9
- data/lib/vident2/stimulus/controller.rb +0 -59
- data/lib/vident2/stimulus/naming.rb +0 -26
- data/lib/vident2/stimulus/null.rb +0 -16
- data/lib/vident2/stimulus/value.rb +0 -77
- data/lib/vident2/tailwind.rb +0 -19
- data/lib/vident2/version.rb +0 -5
- data/lib/vident2/view_component/base.rb +0 -124
- data/lib/vident2/view_component.rb +0 -9
- data/lib/vident2.rb +0 -50
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vident
|
|
4
|
+
module Capabilities
|
|
5
|
+
module RootElementRendering
|
|
6
|
+
def root_element_attributes = {}
|
|
7
|
+
|
|
8
|
+
def root_element_classes
|
|
9
|
+
nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def root_element(**overrides, &block)
|
|
13
|
+
raise NoMethodError, "subclass must implement root_element"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Dispatches to the adapter-specific `root_element` on subclasses
|
|
17
|
+
# (Phlex / ViewComponent). Keep as `def` not `alias_method` so Ruby's
|
|
18
|
+
# dynamic dispatch finds the subclass override.
|
|
19
|
+
def root(...)
|
|
20
|
+
root_element(...)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def root_element_tag_type
|
|
26
|
+
tag = resolved_root_element_attributes[:element_tag] || @element_tag
|
|
27
|
+
tag.presence&.to_sym || :div
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../internals/attribute_writer"
|
|
4
|
+
require_relative "../internals/class_list_builder"
|
|
5
|
+
|
|
6
|
+
module Vident
|
|
7
|
+
module Capabilities
|
|
8
|
+
module StimulusDataEmitting
|
|
9
|
+
# For components rendering the root via a third-party helper instead of
|
|
10
|
+
# `root_element(...)`. `extra_classes` lands at the lowest-priority tier
|
|
11
|
+
# so it cannot be clobbered by a cascade winner.
|
|
12
|
+
def root_element_class_list(extra_classes = nil)
|
|
13
|
+
extra = resolved_root_element_attributes
|
|
14
|
+
extra_html_options = extra[:html_options] || {}
|
|
15
|
+
combined_classes = [@classes, extra_classes].flatten.compact
|
|
16
|
+
result = ::Vident::Internals::ClassListBuilder.call(
|
|
17
|
+
component_name: component_name,
|
|
18
|
+
root_element_classes: root_element_classes,
|
|
19
|
+
root_element_attributes_classes: extra[:classes],
|
|
20
|
+
html_options_class: @html_options[:class] || extra_html_options[:class],
|
|
21
|
+
classes_prop: combined_classes.empty? ? nil : combined_classes,
|
|
22
|
+
tailwind_merger: tailwind_merger
|
|
23
|
+
)
|
|
24
|
+
result || ""
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Seals the Draft on first call (idempotent). Symbol keys.
|
|
28
|
+
def root_element_data_attributes
|
|
29
|
+
plan = seal_draft
|
|
30
|
+
data_attrs = ::Vident::Internals::AttributeWriter.call(plan)
|
|
31
|
+
|
|
32
|
+
extra = resolved_root_element_attributes
|
|
33
|
+
extra_data = (extra[:html_options] || {})[:data] || {}
|
|
34
|
+
|
|
35
|
+
merged = data_attrs.dup
|
|
36
|
+
merged.merge!(symbolize_keys(extra_data))
|
|
37
|
+
merged.merge!(symbolize_keys(@html_options[:data] || {}))
|
|
38
|
+
merged
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def resolved_root_element_attributes
|
|
44
|
+
return @__vident_rea if defined?(@__vident_rea)
|
|
45
|
+
value = root_element_attributes
|
|
46
|
+
@__vident_rea = value.is_a?(Hash) ? value : {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_root_element_attributes(overrides)
|
|
50
|
+
plan = seal_draft
|
|
51
|
+
data_attrs = ::Vident::Internals::AttributeWriter.call(plan)
|
|
52
|
+
|
|
53
|
+
extra = resolved_root_element_attributes
|
|
54
|
+
extra_html_options = extra[:html_options] || {}
|
|
55
|
+
extra_class = extra[:classes]
|
|
56
|
+
extra_id = extra[:id]
|
|
57
|
+
extra_data = extra_html_options[:data] || {}
|
|
58
|
+
|
|
59
|
+
# data precedence (low→high): Plan fragments → attrs html_options[:data]
|
|
60
|
+
# → instance html_options[:data] → overrides[:data].
|
|
61
|
+
merged_data = data_attrs.dup
|
|
62
|
+
merged_data.merge!(symbolize_keys(extra_data))
|
|
63
|
+
merged_data.merge!(symbolize_keys(@html_options[:data] || {}))
|
|
64
|
+
merged_data.merge!(symbolize_keys(overrides[:data] || {}))
|
|
65
|
+
|
|
66
|
+
class_list = ::Vident::Internals::ClassListBuilder.call(
|
|
67
|
+
component_name: component_name,
|
|
68
|
+
root_element_classes: root_element_classes,
|
|
69
|
+
root_element_attributes_classes: extra_class,
|
|
70
|
+
root_element_html_class: overrides[:class],
|
|
71
|
+
html_options_class: @html_options[:class] || extra_html_options[:class],
|
|
72
|
+
classes_prop: @classes,
|
|
73
|
+
tailwind_merger: tailwind_merger
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
merged = {}
|
|
77
|
+
merged.merge!(extra_html_options.except(:data, :class))
|
|
78
|
+
merged.merge!(@html_options.except(:data, :class))
|
|
79
|
+
merged.merge!(overrides.except(:data, :class))
|
|
80
|
+
merged[:class] = class_list if class_list
|
|
81
|
+
merged[:data] = merged_data unless merged_data.empty?
|
|
82
|
+
merged[:id] ||= extra_id || id
|
|
83
|
+
|
|
84
|
+
merged
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Intentionally NOT `hash.symbolize_keys` (ActiveSupport): that calls
|
|
88
|
+
# `to_sym` on every key, including integers, and would raise on keys
|
|
89
|
+
# that don't respond to `to_sym`. Here we convert only String keys and
|
|
90
|
+
# leave anything else (Symbols already) untouched — safe for the
|
|
91
|
+
# user-supplied `data:` hashes that reach this method.
|
|
92
|
+
def symbolize_keys(hash)
|
|
93
|
+
return {} unless hash.is_a?(Hash)
|
|
94
|
+
hash.transform_keys { |k| k.is_a?(String) ? k.to_sym : k }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../internals/declarations"
|
|
4
|
+
require_relative "../internals/dsl"
|
|
5
|
+
|
|
6
|
+
module Vident
|
|
7
|
+
module Capabilities
|
|
8
|
+
module StimulusDeclaring
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
@__vident_declarations = ::Vident::Internals::Declarations.empty
|
|
13
|
+
@__vident_no_stimulus_controller = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class_methods do
|
|
17
|
+
def declarations
|
|
18
|
+
@__vident_declarations ||= ::Vident::Internals::Declarations.empty
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def no_stimulus_controller
|
|
22
|
+
if declarations.any?
|
|
23
|
+
raise ::Vident::DeclarationError,
|
|
24
|
+
"#{name || "anonymous component"} called `no_stimulus_controller` after " \
|
|
25
|
+
"`stimulus do` already recorded DSL entries. Declare `no_stimulus_controller` " \
|
|
26
|
+
"before any `stimulus do` block."
|
|
27
|
+
end
|
|
28
|
+
@__vident_no_stimulus_controller = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def has_stimulus_controller
|
|
32
|
+
@__vident_no_stimulus_controller = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def stimulus_controller?
|
|
36
|
+
!@__vident_no_stimulus_controller
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Second+ calls append (positional) or last-write-wins (keyed).
|
|
40
|
+
def stimulus(&block)
|
|
41
|
+
call_site = caller_locations(1, 1)&.first
|
|
42
|
+
dsl = ::Vident::Internals::DSL.new(caller_location: call_site)
|
|
43
|
+
dsl.instance_eval(&block) if block
|
|
44
|
+
fresh = dsl.to_declarations
|
|
45
|
+
|
|
46
|
+
if !stimulus_controller? && fresh.any?
|
|
47
|
+
location = call_site ? " at #{call_site.path}:#{call_site.lineno}" : ""
|
|
48
|
+
raise ::Vident::DeclarationError,
|
|
49
|
+
"#{name || "anonymous component"} declared `no_stimulus_controller` but `stimulus do` emitted DSL entries#{location}. " \
|
|
50
|
+
"A class with no implied controller cannot route DSL entries; drop the `stimulus do` block or remove `no_stimulus_controller`."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@__vident_declarations = declarations.merge(fresh).freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def stimulus_scoped_event(event)
|
|
57
|
+
:"#{component_name}:#{event.to_s.camelize(:lower)}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def stimulus_scoped_event_on_window(event)
|
|
61
|
+
:"#{component_name}:#{event.to_s.camelize(:lower)}@window"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def inherited(subclass)
|
|
65
|
+
super
|
|
66
|
+
subclass.instance_variable_set(:@__vident_declarations, declarations)
|
|
67
|
+
subclass.instance_variable_set(
|
|
68
|
+
:@__vident_no_stimulus_controller,
|
|
69
|
+
instance_variable_get(:@__vident_no_stimulus_controller) || false
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def stimulus_scoped_event(event) = self.class.stimulus_scoped_event(event)
|
|
75
|
+
|
|
76
|
+
def stimulus_scoped_event_on_window(event) = self.class.stimulus_scoped_event_on_window(event)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../internals/resolver"
|
|
4
|
+
|
|
5
|
+
module Vident
|
|
6
|
+
module Capabilities
|
|
7
|
+
module StimulusDraft
|
|
8
|
+
def after_component_initialize
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Flag set before the guards so a sealed Draft can't trap us in a
|
|
12
|
+
# loop where every subsequent call re-takes the sealed branch.
|
|
13
|
+
def resolve_stimulus_attributes_at_render_time
|
|
14
|
+
return if @__vident_procs_resolved
|
|
15
|
+
@__vident_procs_resolved = true
|
|
16
|
+
return if @__vident_draft.nil? || @__vident_draft.sealed?
|
|
17
|
+
::Vident::Internals::Resolver.resolve_procs_into(
|
|
18
|
+
@__vident_draft, self.class.declarations, self
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# DSL procs defer to render (`view_context` isn't wired yet at init time).
|
|
25
|
+
def after_initialize
|
|
26
|
+
@__vident_draft = ::Vident::Internals::Resolver.call(
|
|
27
|
+
self.class.declarations, self, phase: :static
|
|
28
|
+
)
|
|
29
|
+
@stimulus_outlet_host&.add_stimulus_outlets(self)
|
|
30
|
+
after_component_initialize
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def raise_if_sealed!
|
|
34
|
+
if @__vident_draft.nil?
|
|
35
|
+
raise ::Vident::StateError,
|
|
36
|
+
"stimulus Draft is nil — Literal::Data `after_initialize` never fired. " \
|
|
37
|
+
"If you subclassed and overrode `initialize`, ensure `super` is called. " \
|
|
38
|
+
"If you instantiated with `allocate`, call `after_initialize` manually."
|
|
39
|
+
end
|
|
40
|
+
return unless @__vident_draft.sealed?
|
|
41
|
+
raise ::Vident::StateError,
|
|
42
|
+
"cannot modify stimulus attributes after rendering has begun"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def seal_draft
|
|
46
|
+
resolve_stimulus_attributes_at_render_time
|
|
47
|
+
@__vident_plan ||= @__vident_draft.seal!
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
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 StimulusMutation
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
unless ancestors.include?(::Vident::Capabilities::Identifiable)
|
|
13
|
+
raise ::Vident::DeclarationError,
|
|
14
|
+
"#{name || "anonymous component"} must include Vident::Capabilities::Identifiable before Vident::Capabilities::StimulusMutation"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
::Vident::Internals::Registry.each do |kind|
|
|
19
|
+
define_method(:"add_stimulus_#{kind.plural_name}") do |input|
|
|
20
|
+
raise_if_sealed!
|
|
21
|
+
values = unwrap_mutator_input(kind, input)
|
|
22
|
+
values.each { |v| @__vident_draft.public_send(:"add_#{kind.name}", v) if v }
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Array input is ONE entry — does not splat across multiple entries
|
|
30
|
+
# (mirrors the DSL's plural→singular forwarding).
|
|
31
|
+
#
|
|
32
|
+
# Hash input is only accepted for keyed kinds (values, params, class_maps,
|
|
33
|
+
# outlets) and Actions. Passing a Hash to a non-keyed, non-Action kind
|
|
34
|
+
# (controllers, targets) raises ParseError — use a String or Symbol instead.
|
|
35
|
+
def unwrap_mutator_input(kind, input)
|
|
36
|
+
case input
|
|
37
|
+
in nil
|
|
38
|
+
[]
|
|
39
|
+
in ^(kind.value_class) => v
|
|
40
|
+
[v]
|
|
41
|
+
in ::Vident::Stimulus::Collection => coll
|
|
42
|
+
coll.items
|
|
43
|
+
in Hash => h if kind.keyed?
|
|
44
|
+
h.map { |name, raw| kind.value_class.parse(name, raw, implied: implied_controller, component_id: id) }
|
|
45
|
+
in Hash => h if kind.value_class == ::Vident::Stimulus::Action
|
|
46
|
+
[kind.value_class.parse(h, implied: implied_controller, component_id: id)]
|
|
47
|
+
in Hash
|
|
48
|
+
raise ::Vident::ParseError,
|
|
49
|
+
"add_stimulus_#{kind.plural_name}: Hash input is not valid for #{kind.plural_name}. " \
|
|
50
|
+
"Use a String or Symbol instead, e.g. add_stimulus_#{kind.plural_name}(\"name\"). " \
|
|
51
|
+
"Hash input is only accepted for keyed kinds (values, params, classes, outlets) and actions."
|
|
52
|
+
in Array => a
|
|
53
|
+
[kind.value_class.parse(*a, implied: implied_controller, component_id: id)]
|
|
54
|
+
else
|
|
55
|
+
[kind.value_class.parse(input, implied: implied_controller, component_id: id)]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../internals/registry"
|
|
4
|
+
require_relative "../stimulus/collection"
|
|
5
|
+
|
|
6
|
+
module Vident
|
|
7
|
+
module Capabilities
|
|
8
|
+
# Include order: Identifiable must be included before this module
|
|
9
|
+
# (generated methods call `implied_controller` and `id`).
|
|
10
|
+
module StimulusParsing
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
included do
|
|
14
|
+
unless ancestors.include?(::Vident::Capabilities::Identifiable)
|
|
15
|
+
raise ::Vident::DeclarationError,
|
|
16
|
+
"#{name || "anonymous component"} must include Vident::Capabilities::Identifiable before Vident::Capabilities::StimulusParsing"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
::Vident::Internals::Registry.each do |kind|
|
|
21
|
+
define_method(:"stimulus_#{kind.singular_name}") do |*args|
|
|
22
|
+
return args.first if args.length == 1 && args.first.is_a?(kind.value_class)
|
|
23
|
+
kind.value_class.parse(*args, implied: implied_controller, component_id: id)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
define_method(:"stimulus_#{kind.plural_name}") do |*args|
|
|
27
|
+
return ::Vident::Stimulus::Collection.new(kind: kind, items: []) if args.empty? || args.all?(&:nil?)
|
|
28
|
+
return args.first if args.length == 1 && args.first.is_a?(::Vident::Stimulus::Collection)
|
|
29
|
+
|
|
30
|
+
items = []
|
|
31
|
+
args.each do |arg|
|
|
32
|
+
case arg
|
|
33
|
+
in ^(kind.value_class) => v
|
|
34
|
+
items << v
|
|
35
|
+
in ::Vident::Stimulus::Collection => coll
|
|
36
|
+
items.concat(coll.items)
|
|
37
|
+
in Hash => h if kind.keyed?
|
|
38
|
+
h.each { |name, val| items << kind.value_class.parse(name, val, implied: implied_controller, component_id: id) }
|
|
39
|
+
in Hash => h
|
|
40
|
+
items << kind.value_class.parse(h, implied: implied_controller, component_id: id)
|
|
41
|
+
in Array => a
|
|
42
|
+
items << kind.value_class.parse(*a, implied: implied_controller, component_id: id)
|
|
43
|
+
else
|
|
44
|
+
items << kind.value_class.parse(arg, implied: implied_controller, component_id: id)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
::Vident::Stimulus::Collection.new(kind: kind, items: items)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Class-level builders reject cross-controller forms (receiver's implied
|
|
52
|
+
# controller would be silently ignored) — call `Vident::Stimulus::*.parse`
|
|
53
|
+
# directly for those. `stimulus_outlet` additionally requires an explicit
|
|
54
|
+
# selector (no component_id to auto-scope).
|
|
55
|
+
class_methods do
|
|
56
|
+
def stimulus_controller
|
|
57
|
+
implied_controller_for_class
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def stimulus_target(*args)
|
|
61
|
+
case args
|
|
62
|
+
in [String => ctrl_path, Symbol]
|
|
63
|
+
raise ::Vident::ParseError,
|
|
64
|
+
"#{name}.stimulus_target does not accept cross-controller form at class level. " \
|
|
65
|
+
"Call Vident::Stimulus::Target.parse(#{ctrl_path.inspect}, ...) directly."
|
|
66
|
+
else
|
|
67
|
+
::Vident::Stimulus::Target.parse(*args, implied: implied_controller_for_class)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def stimulus_action(*args)
|
|
72
|
+
case args
|
|
73
|
+
in [String => ctrl_path, Symbol]
|
|
74
|
+
raise ::Vident::ParseError,
|
|
75
|
+
"#{name}.stimulus_action does not accept cross-controller form at class level. " \
|
|
76
|
+
"Call Vident::Stimulus::Action.parse(#{ctrl_path.inspect}, ...) directly."
|
|
77
|
+
in [Symbol, String, Symbol]
|
|
78
|
+
raise ::Vident::ParseError,
|
|
79
|
+
"#{name}.stimulus_action does not accept cross-controller form at class level. " \
|
|
80
|
+
"Call Vident::Stimulus::Action.parse directly."
|
|
81
|
+
else
|
|
82
|
+
::Vident::Stimulus::Action.parse(*args, implied: implied_controller_for_class)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def stimulus_value(*args)
|
|
87
|
+
case args
|
|
88
|
+
in [String, Symbol, *]
|
|
89
|
+
raise ::Vident::ParseError,
|
|
90
|
+
"#{name}.stimulus_value does not accept cross-controller form at class level. " \
|
|
91
|
+
"Call Vident::Stimulus::Value.parse directly."
|
|
92
|
+
else
|
|
93
|
+
::Vident::Stimulus::Value.parse(*args, implied: implied_controller_for_class)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def stimulus_param(*args)
|
|
98
|
+
case args
|
|
99
|
+
in [String, Symbol, *]
|
|
100
|
+
raise ::Vident::ParseError,
|
|
101
|
+
"#{name}.stimulus_param does not accept cross-controller form at class level. " \
|
|
102
|
+
"Call Vident::Stimulus::Param.parse directly."
|
|
103
|
+
else
|
|
104
|
+
::Vident::Stimulus::Param.parse(*args, implied: implied_controller_for_class)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def stimulus_class(*args)
|
|
109
|
+
case args
|
|
110
|
+
in [String, Symbol, *]
|
|
111
|
+
raise ::Vident::ParseError,
|
|
112
|
+
"#{name}.stimulus_class does not accept cross-controller form at class level. " \
|
|
113
|
+
"Call Vident::Stimulus::ClassMap.parse directly."
|
|
114
|
+
else
|
|
115
|
+
::Vident::Stimulus::ClassMap.parse(*args, implied: implied_controller_for_class)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def stimulus_outlet(*args)
|
|
120
|
+
case args
|
|
121
|
+
in [Symbol => _name, String => _selector] | [String => _name, String => _selector]
|
|
122
|
+
::Vident::Stimulus::Outlet.parse(*args, implied: implied_controller_for_class)
|
|
123
|
+
else
|
|
124
|
+
raise ::Vident::ParseError,
|
|
125
|
+
"#{name}.stimulus_outlet requires (name, selector) — no component_id at class level. " \
|
|
126
|
+
"Use instance-level `component.stimulus_outlet(:name)` for auto-selector, " \
|
|
127
|
+
"or `#{name}.stimulus_outlet(:name, '.selector')` with an explicit selector."
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
# Memoised on the class's own singleton — Ruby doesn't share
|
|
134
|
+
# singleton ivars through inheritance, so subclasses get their own.
|
|
135
|
+
def implied_controller_for_class
|
|
136
|
+
@__vident_class_implied_controller ||= ::Vident::Stimulus::Controller.new(
|
|
137
|
+
path: stimulus_identifier_path,
|
|
138
|
+
name: stimulus_identifier
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vident
|
|
4
|
+
module Capabilities
|
|
5
|
+
module Tailwind
|
|
6
|
+
def tailwind_merger
|
|
7
|
+
return unless tailwind_merge_available?
|
|
8
|
+
return @tailwind_merger if defined?(@tailwind_merger)
|
|
9
|
+
|
|
10
|
+
@tailwind_merger = Thread.current[:vident_tailwind_merger] ||= ::TailwindMerge::Merger.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def tailwind_merge_available?
|
|
14
|
+
defined?(::TailwindMerge::Merger) && ::TailwindMerge::Merger.respond_to?(:new)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/vident/component.rb
CHANGED
|
@@ -1,84 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Vident
|
|
4
|
+
# Composition root. Include order mirrors capability dependencies.
|
|
5
|
+
# Caching is opt-in and NOT included here.
|
|
4
6
|
module Component
|
|
5
7
|
extend ActiveSupport::Concern
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class_methods do
|
|
20
|
-
# Returns the names of the properties defined in the component class.
|
|
21
|
-
def prop_names
|
|
22
|
-
literal_properties.properties_index.keys.map(&:to_sym)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
include StimulusComponent
|
|
27
|
-
include ComponentClassLists
|
|
28
|
-
include ComponentAttributeResolver
|
|
29
|
-
|
|
30
|
-
include ChildElementHelper
|
|
31
|
-
include Tailwind
|
|
32
|
-
include StimulusHelper
|
|
33
|
-
|
|
34
|
-
# Override this method to perform any initialisation after attributes are set
|
|
35
|
-
def after_component_initialize
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# This can be overridden to return an array of extra class names, or a string of class names.
|
|
39
|
-
def root_element_classes
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Properties/attributes passed to the "root" element of the component. You normally override this method to
|
|
43
|
-
# return a hash of attributes that should be applied to the root element of your component.
|
|
44
|
-
def root_element_attributes = {}
|
|
45
|
-
|
|
46
|
-
# Create a new component instance with optional overrides for properties.
|
|
47
|
-
def clone(overrides = {}) = self.class.new(**to_h.merge(**overrides))
|
|
48
|
-
|
|
49
|
-
def inspect(klass_name = "Component")
|
|
50
|
-
attr_text = to_h.map { |k, v| "#{k}=#{v.inspect}" }.join(", ")
|
|
51
|
-
"#<#{self.class.name}<Vident::#{klass_name}> #{attr_text}>"
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Generate a unique ID for a component, can be overridden as required. Makes it easier to setup things like ARIA
|
|
55
|
-
# attributes which require elements to reference by ID. Note this overrides the `id` accessor
|
|
56
|
-
def id = @id.presence || random_id
|
|
57
|
-
|
|
58
|
-
# Return the names of the properties defined in the component class.
|
|
59
|
-
def prop_names = self.class.prop_names
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
# Called by Literal::Properties after the component is initialized.
|
|
64
|
-
def after_initialize
|
|
65
|
-
prepare_component_attributes
|
|
66
|
-
after_component_initialize if respond_to?(:after_component_initialize)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def root_element(&block)
|
|
70
|
-
raise NoMethodError, "You must implement the `root_element` method in your component"
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def root(...)
|
|
74
|
-
root_element(...)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def root_element_tag_type = @element_tag.presence&.to_sym || :div
|
|
78
|
-
|
|
79
|
-
# Generate a random ID for the component, which is used to ensure uniqueness in the DOM.
|
|
80
|
-
def random_id
|
|
81
|
-
@random_id ||= "#{component_name}-#{StableId.next_id_in_sequence}"
|
|
82
|
-
end
|
|
9
|
+
include ::Vident::Capabilities::Tailwind
|
|
10
|
+
include ::Vident::Capabilities::Declarable
|
|
11
|
+
include ::Vident::Capabilities::Identifiable
|
|
12
|
+
include ::Vident::Capabilities::StimulusDeclaring
|
|
13
|
+
include ::Vident::Capabilities::StimulusParsing
|
|
14
|
+
include ::Vident::Capabilities::StimulusMutation
|
|
15
|
+
include ::Vident::Capabilities::StimulusDraft
|
|
16
|
+
include ::Vident::Capabilities::StimulusDataEmitting
|
|
17
|
+
include ::Vident::Capabilities::ClassListBuilding
|
|
18
|
+
include ::Vident::Capabilities::RootElementRendering
|
|
19
|
+
include ::Vident::Capabilities::ChildElementRendering
|
|
20
|
+
include ::Vident::Capabilities::Inspectable
|
|
83
21
|
end
|
|
84
22
|
end
|
data/lib/vident/engine.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Vident
|
|
4
|
+
# Registers acronym inflections Zeitwerk needs to load `Vident::Internals::DSL`
|
|
5
|
+
# and `Vident::Phlex::HTML` from their respective files.
|
|
2
6
|
class Engine < ::Rails::Engine
|
|
3
|
-
lib_path = File.expand_path("../../../lib/", __FILE__)
|
|
4
|
-
config.autoload_paths << lib_path
|
|
5
|
-
config.eager_load_paths << lib_path
|
|
6
|
-
|
|
7
7
|
config.before_initialize do
|
|
8
8
|
Rails.autoloaders.each do |autoloader|
|
|
9
9
|
autoloader.inflector.inflect(
|
|
10
|
-
"
|
|
10
|
+
"dsl" => "DSL",
|
|
11
|
+
"html" => "HTML"
|
|
11
12
|
)
|
|
12
13
|
end
|
|
13
14
|
end
|
data/lib/vident/error.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vident
|
|
4
|
+
# Base for all gem-raised exceptions; consumers can rescue by category.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
class DeclarationError < Error; end
|
|
8
|
+
|
|
9
|
+
class ParseError < Error; end
|
|
10
|
+
|
|
11
|
+
class RenderError < Error; end
|
|
12
|
+
|
|
13
|
+
class StateError < Error; end
|
|
14
|
+
|
|
15
|
+
class ConfigurationError < Error; end
|
|
16
|
+
end
|
|
@@ -2,30 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "declaration"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module Vident
|
|
6
6
|
module Internals
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# Each method mutates state and returns `self` so chains compose:
|
|
10
|
-
#
|
|
11
|
-
# action(:submit).on(:form).modifier(:prevent).keyboard("enter")
|
|
12
|
-
#
|
|
13
|
-
# At `to_declaration` time the captured state is folded into a Hash
|
|
14
|
-
# descriptor that the `Stimulus::Action` parser already understands.
|
|
15
|
-
# If no chain method was called, the raw args pass through untouched
|
|
16
|
-
# so existing `action :click` / `action [:click, :handle]` callsites
|
|
17
|
-
# behave exactly as before.
|
|
7
|
+
# Fluent builder returned by `action(...)`. If no chain methods are called,
|
|
8
|
+
# raw args pass through untouched so bare `action :click` still works.
|
|
18
9
|
class ActionBuilder
|
|
19
|
-
|
|
10
|
+
KWARG_KEYS = %i[on call_method modifier keyboard window on_controller when].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(*args, **meta)
|
|
20
13
|
@args = args
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
@
|
|
28
|
-
@
|
|
14
|
+
unknown = meta.keys - KWARG_KEYS
|
|
15
|
+
raise ArgumentError, "action: unknown option(s) #{unknown.inspect}. Valid: #{KWARG_KEYS.inspect}" unless unknown.empty?
|
|
16
|
+
|
|
17
|
+
@event = meta[:on]
|
|
18
|
+
@method_name = meta[:call_method]
|
|
19
|
+
@modifiers = meta.key?(:modifier) ? Array(meta[:modifier]) : nil
|
|
20
|
+
@keyboard = meta[:keyboard]
|
|
21
|
+
@window = meta.fetch(:window, false)
|
|
22
|
+
@controller_ref = meta[:on_controller]
|
|
23
|
+
@when_proc = meta[:when]
|
|
24
|
+
@touched = !meta.empty?
|
|
29
25
|
end
|
|
30
26
|
|
|
31
27
|
def on(event)
|
|
@@ -34,7 +30,7 @@ module Vident2
|
|
|
34
30
|
self
|
|
35
31
|
end
|
|
36
32
|
|
|
37
|
-
def
|
|
33
|
+
def call_method(name)
|
|
38
34
|
@method_name = name
|
|
39
35
|
@touched = true
|
|
40
36
|
self
|