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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +49 -18
- 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/vident/internals/action_builder.rb +97 -0
- data/lib/vident/internals/attribute_writer.rb +17 -0
- data/lib/vident/internals/class_list_builder.rb +62 -0
- data/lib/vident/internals/declaration.rb +13 -0
- data/lib/vident/internals/declarations.rb +64 -0
- data/lib/vident/internals/draft.rb +47 -0
- data/lib/vident/internals/dsl.rb +172 -0
- data/lib/vident/internals/plan.rb +9 -0
- data/lib/vident/internals/registry.rb +37 -0
- data/lib/vident/internals/resolver.rb +316 -0
- data/lib/vident/internals/target_builder.rb +23 -0
- data/lib/vident/stable_id.rb +3 -3
- data/lib/vident/stimulus/action.rb +127 -0
- data/lib/vident/stimulus/base.rb +26 -0
- data/lib/vident/stimulus/class_map.rb +57 -0
- data/lib/vident/stimulus/collection.rb +40 -0
- 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/vident/stimulus/outlet.rb +93 -0
- data/lib/vident/stimulus/param.rb +56 -0
- data/lib/vident/stimulus/target.rb +48 -0
- 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 +44 -36
- data/skills/vident/SKILL.md +133 -21
- data/skills/vident/api-reference.md +662 -0
- data/skills/vident/examples.md +505 -0
- metadata +40 -28
- 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 -87
- data/lib/vident/component_class_lists.rb +0 -34
- 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 -119
- 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
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "set"
|
|
4
|
-
|
|
5
|
-
module Vident
|
|
6
|
-
class ClassListBuilder
|
|
7
|
-
CLASSNAME_SEPARATOR = " "
|
|
8
|
-
|
|
9
|
-
# If the HTML "class" option is provided, it is taken in order of precedence of source.
|
|
10
|
-
# The order of precedence is:
|
|
11
|
-
# lowest | root_element_classes => whatever is returned
|
|
12
|
-
# ....... | root_element_attributes => the `html_options[:class]` value
|
|
13
|
-
# ....... | root_element(class: ...) => the `class` value of the arguments passed to the root element
|
|
14
|
-
# highest | render MyComponent.new(html_options: { class: ... }) => the `html_options[:class]` value
|
|
15
|
-
# The "classes" prop on the component on the other hand is used to add additional classes to the component.
|
|
16
|
-
# eg: render MyComponent.new(classes: "my-additional-class another-class")
|
|
17
|
-
def initialize(tailwind_merger: nil, component_name: nil, root_element_attributes_classes: nil, root_element_classes: nil, root_element_html_class: nil, html_class: nil, additional_classes: nil)
|
|
18
|
-
@class_list = component_name ? [component_name] : []
|
|
19
|
-
@class_list.concat(Array.wrap(root_element_classes)) if root_element_classes && !root_element_attributes_classes && !root_element_html_class && !html_class
|
|
20
|
-
@class_list.concat(Array.wrap(root_element_attributes_classes)) if root_element_attributes_classes && !root_element_html_class && !html_class
|
|
21
|
-
@class_list.concat(Array.wrap(root_element_html_class)) if root_element_html_class && !html_class
|
|
22
|
-
@class_list.concat(Array.wrap(html_class)) if html_class
|
|
23
|
-
@class_list.concat(Array.wrap(additional_classes)) if additional_classes
|
|
24
|
-
@class_list.compact!
|
|
25
|
-
|
|
26
|
-
@tailwind_merger = tailwind_merger
|
|
27
|
-
|
|
28
|
-
if @tailwind_merger && !defined?(::TailwindMerge::Merger)
|
|
29
|
-
raise LoadError, "TailwindMerge gem is required when using tailwind_merger:. Add 'gem \"tailwind_merge\"' to your Gemfile."
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Main method to build a final class list from multiple sources
|
|
34
|
-
# @param class_lists [Array<String, Array, StimulusClass, nil>] Multiple class sources to merge
|
|
35
|
-
# @param stimulus_class_names [Array<Symbol, String>] Optional names of stimulus classes to include
|
|
36
|
-
# @return [String, nil] Final space-separated class string or nil if no classes
|
|
37
|
-
def build(extra_classes = nil, stimulus_class_names: [])
|
|
38
|
-
class_list = @class_list + Array.wrap(extra_classes).compact
|
|
39
|
-
flattened_classes = flatten_and_normalize_classes(class_list, stimulus_class_names)
|
|
40
|
-
return nil if flattened_classes.empty?
|
|
41
|
-
|
|
42
|
-
deduplicated_classes = dedupe_classes(flattened_classes)
|
|
43
|
-
return nil if deduplicated_classes.blank?
|
|
44
|
-
|
|
45
|
-
class_string = deduplicated_classes.join(CLASSNAME_SEPARATOR)
|
|
46
|
-
|
|
47
|
-
if @tailwind_merger
|
|
48
|
-
dedupe_with_tailwind(class_string)
|
|
49
|
-
else
|
|
50
|
-
class_string
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
# Flatten and normalize all input class sources
|
|
57
|
-
def flatten_and_normalize_classes(class_lists, stimulus_class_names)
|
|
58
|
-
stimulus_class_names_set = stimulus_class_names.map { |name| name.to_s.dasherize }.to_set
|
|
59
|
-
|
|
60
|
-
class_lists.compact.flat_map do |class_source|
|
|
61
|
-
case class_source
|
|
62
|
-
when String
|
|
63
|
-
class_source.split(CLASSNAME_SEPARATOR).reject(&:empty?)
|
|
64
|
-
when Array
|
|
65
|
-
class_source.flat_map { |item| normalize_single_class_item(item, stimulus_class_names_set) }
|
|
66
|
-
else
|
|
67
|
-
normalize_single_class_item(class_source, stimulus_class_names_set)
|
|
68
|
-
end
|
|
69
|
-
end.compact
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Normalize a single class item (could be string, StimulusClass, object with to_s, etc.)
|
|
73
|
-
def normalize_single_class_item(item, stimulus_class_names_set)
|
|
74
|
-
return [] if item.blank?
|
|
75
|
-
|
|
76
|
-
# Handle StimulusClass instances
|
|
77
|
-
if stimulus_class_instance?(item)
|
|
78
|
-
# Only include if the class name matches one of the requested names
|
|
79
|
-
# If stimulus_class_names_set is empty, exclude all stimulus classes
|
|
80
|
-
if stimulus_class_names_set.present? && stimulus_class_names_set.include?(item.class_name)
|
|
81
|
-
class_value = item.to_s
|
|
82
|
-
class_value.include?(CLASSNAME_SEPARATOR) ?
|
|
83
|
-
class_value.split(CLASSNAME_SEPARATOR).reject(&:empty?) :
|
|
84
|
-
[class_value]
|
|
85
|
-
else
|
|
86
|
-
[]
|
|
87
|
-
end
|
|
88
|
-
else
|
|
89
|
-
# Handle regular strings and other objects
|
|
90
|
-
item_string = item.to_s
|
|
91
|
-
item_string.include?(CLASSNAME_SEPARATOR) ?
|
|
92
|
-
item_string.split(CLASSNAME_SEPARATOR).reject(&:empty?) :
|
|
93
|
-
[item_string]
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Check if an item is a StimulusClass instance
|
|
98
|
-
def stimulus_class_instance?(item)
|
|
99
|
-
item.respond_to?(:class_name) && item.respond_to?(:to_s)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Deduplicate classes while preserving order (first occurrence wins)
|
|
103
|
-
def dedupe_classes(class_array)
|
|
104
|
-
class_array.reject(&:blank?).uniq
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Merge classes using Tailwind CSS merge
|
|
108
|
-
def dedupe_with_tailwind(class_string)
|
|
109
|
-
@tailwind_merger.merge(class_string)
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Vident
|
|
4
|
-
module ComponentAttributeResolver
|
|
5
|
-
include Stimulus::Naming
|
|
6
|
-
|
|
7
|
-
private
|
|
8
|
-
|
|
9
|
-
# Prepare attributes set at initialization, which will later be merged together before rendering.
|
|
10
|
-
def prepare_component_attributes
|
|
11
|
-
prepare_stimulus_collections
|
|
12
|
-
|
|
13
|
-
# Add stimulus attributes from DSL first (lower precedence)
|
|
14
|
-
add_stimulus_attributes_from_dsl
|
|
15
|
-
|
|
16
|
-
# Process root_element_attributes (higher precedence)
|
|
17
|
-
extra = root_element_attributes
|
|
18
|
-
@html_options = (extra[:html_options] || {}).merge(@html_options) if extra.key?(:html_options)
|
|
19
|
-
@root_element_attributes_classes = extra[:classes]
|
|
20
|
-
@root_element_attributes_id = extra[:id] || id
|
|
21
|
-
@element_tag = extra[:element_tag] if extra.key?(:element_tag)
|
|
22
|
-
|
|
23
|
-
Stimulus::PRIMITIVES.each do |primitive|
|
|
24
|
-
send(mutator_method(primitive), extra[primitive.key]) if extra.key?(primitive.key)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def resolve_root_element_attributes_before_render(root_element_html_options = nil)
|
|
29
|
-
extra = root_element_html_options || {}
|
|
30
|
-
|
|
31
|
-
# Options set on component at render time take precedence over attributes set by methods on the component
|
|
32
|
-
# or attributes passed to root_element in the template
|
|
33
|
-
final_attributes = {
|
|
34
|
-
data: stimulus_data_attributes # Lowest precedence
|
|
35
|
-
}
|
|
36
|
-
if root_element_html_options.present? # Mid precedence
|
|
37
|
-
root_element_tag_html_options_merge(final_attributes, root_element_html_options)
|
|
38
|
-
end
|
|
39
|
-
if @html_options.present? # Highest precedence
|
|
40
|
-
root_element_tag_html_options_merge(final_attributes, @html_options)
|
|
41
|
-
end
|
|
42
|
-
final_attributes[:class] = render_classes(extra[:class])
|
|
43
|
-
final_attributes[:id] = (extra[:id] || @root_element_attributes_id) unless final_attributes.key?(:id)
|
|
44
|
-
final_attributes
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def root_element_tag_html_options_merge(final_attributes, other_html_options)
|
|
48
|
-
if other_html_options[:data].present?
|
|
49
|
-
final_attributes[:data].merge!(other_html_options[:data])
|
|
50
|
-
end
|
|
51
|
-
final_attributes.merge!(other_html_options.except(:data))
|
|
52
|
-
end
|
|
53
|
-
|
|
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)
|
|
58
|
-
return if dsl_attrs.empty?
|
|
59
|
-
|
|
60
|
-
Stimulus::PRIMITIVES.each do |primitive|
|
|
61
|
-
value = dsl_attrs[primitive.key]
|
|
62
|
-
send(mutator_method(primitive), value) if value
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
if dsl_attrs[:stimulus_values_from_props]
|
|
66
|
-
resolved_values = resolve_values_from_props(dsl_attrs[:stimulus_values_from_props])
|
|
67
|
-
add_stimulus_values(resolved_values) unless resolved_values.empty?
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Seed the collection ivars from each prop's raw value.
|
|
72
|
-
def prepare_stimulus_collections
|
|
73
|
-
Stimulus::PRIMITIVES.each do |primitive|
|
|
74
|
-
raw = instance_variable_get(prop_ivar(primitive))
|
|
75
|
-
collection = send(primitive.key, *Array.wrap(raw))
|
|
76
|
-
instance_variable_set(collection_ivar(primitive), collection)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
@stimulus_outlet_host.add_stimulus_outlets(self) if @stimulus_outlet_host
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def stimulus_data_attributes
|
|
83
|
-
collections = Stimulus::PRIMITIVES.to_h { |primitive| [primitive.name, instance_variable_get(collection_ivar(primitive))] }
|
|
84
|
-
StimulusDataAttributeBuilder.new(**collections).build
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Vident
|
|
4
|
-
module ComponentClassLists
|
|
5
|
-
# Generates the full list of HTML classes for the component
|
|
6
|
-
def render_classes(root_element_html_class = nil) = class_list_builder(root_element_html_class).build
|
|
7
|
-
|
|
8
|
-
# Getter for a stimulus classes list so can be used in view to set initial state on SSR
|
|
9
|
-
# Returns a String of classes that can be used in a `class` attribute.
|
|
10
|
-
def class_list_for_stimulus_classes(*names)
|
|
11
|
-
ClassListBuilder.new(tailwind_merger:).build(
|
|
12
|
-
@stimulus_classes_collection&.to_a,
|
|
13
|
-
stimulus_class_names: names
|
|
14
|
-
) || ""
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
# Not memoised: the per-thread TailwindMerger is the only expensive piece
|
|
20
|
-
# and it's already cached; the builder itself just copies a few ivars.
|
|
21
|
-
# Memoising here would latch the first caller's `root_element_html_class:`.
|
|
22
|
-
def class_list_builder(root_element_html_class = nil)
|
|
23
|
-
ClassListBuilder.new(
|
|
24
|
-
tailwind_merger:,
|
|
25
|
-
component_name:,
|
|
26
|
-
root_element_attributes_classes: @root_element_attributes_classes,
|
|
27
|
-
root_element_classes:,
|
|
28
|
-
root_element_html_class:,
|
|
29
|
-
additional_classes: @classes,
|
|
30
|
-
html_class: @html_options&.fetch(:class, nil)
|
|
31
|
-
)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Vident
|
|
4
|
-
module Stimulus
|
|
5
|
-
# A Stimulus primitive kind: name, plus the Value/Collection classes that
|
|
6
|
-
# back it. Two concrete subclasses distinguish how the primitive behaves
|
|
7
|
-
# when a Hash is passed to the plural parser:
|
|
8
|
-
#
|
|
9
|
-
# - `Keyed` — `{a: 1, b: 2}` expands to one value object per pair.
|
|
10
|
-
# Used for values / params / classes / outlets.
|
|
11
|
-
# - `Positional` — `{...}` is a single-arg descriptor (e.g. Action's
|
|
12
|
-
# `{event:, method:, ...}` short form).
|
|
13
|
-
# Used for controllers / actions / targets.
|
|
14
|
-
class Primitive < ::Data.define(:name, :value_class, :collection_class)
|
|
15
|
-
# Short forms. `name` (Data field) is the plural — `:values`; `plural`
|
|
16
|
-
# is an alias for symmetry with `singular`.
|
|
17
|
-
alias_method :plural, :name
|
|
18
|
-
def singular = name.to_s.singularize.to_sym
|
|
19
|
-
|
|
20
|
-
# The primitive's key in Vident's attribute namespace. Used both as the
|
|
21
|
-
# parser method name (`def stimulus_values(...)`) and as the hash key
|
|
22
|
-
# in DSL attrs / component props / `root_element_attributes` — the same
|
|
23
|
-
# Symbol serves all three roles.
|
|
24
|
-
def key = :"stimulus_#{name}"
|
|
25
|
-
def singular_key = :"stimulus_#{singular}"
|
|
26
|
-
|
|
27
|
-
def keyed? = raise NotImplementedError
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
class KeyedPrimitive < Primitive
|
|
31
|
-
def keyed? = true
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
class PositionalPrimitive < Primitive
|
|
35
|
-
def keyed? = false
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
data/lib/vident/stimulus.rb
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Vident
|
|
4
|
-
module Stimulus
|
|
5
|
-
# Registry of primitive kinds. Add an entry (paired with a Value/Collection
|
|
6
|
-
# class pair) to extend; plural parsers, mutators, and the data-attribute
|
|
7
|
-
# builder pick it up. Array order = data attribute emission order.
|
|
8
|
-
PRIMITIVES = [
|
|
9
|
-
PositionalPrimitive.new(:controllers, StimulusController, StimulusControllerCollection),
|
|
10
|
-
PositionalPrimitive.new(:actions, StimulusAction, StimulusActionCollection),
|
|
11
|
-
PositionalPrimitive.new(:targets, StimulusTarget, StimulusTargetCollection),
|
|
12
|
-
KeyedPrimitive.new(:outlets, StimulusOutlet, StimulusOutletCollection),
|
|
13
|
-
KeyedPrimitive.new(:values, StimulusValue, StimulusValueCollection),
|
|
14
|
-
KeyedPrimitive.new(:params, StimulusParam, StimulusParamCollection),
|
|
15
|
-
KeyedPrimitive.new(:classes, StimulusClass, StimulusClassCollection)
|
|
16
|
-
].freeze
|
|
17
|
-
|
|
18
|
-
PRIMITIVES_BY_NAME = PRIMITIVES.to_h { |primitive| [primitive.name, primitive] }.freeze
|
|
19
|
-
|
|
20
|
-
class << self
|
|
21
|
-
def primitive(name)
|
|
22
|
-
PRIMITIVES_BY_NAME[name] or
|
|
23
|
-
raise ArgumentError, "Unknown stimulus primitive #{name.inspect}; valid: #{PRIMITIVES_BY_NAME.keys.inspect}"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def each(&block) = PRIMITIVES.each(&block)
|
|
27
|
-
|
|
28
|
-
def names = PRIMITIVES.map(&:name)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Vident
|
|
4
|
-
class StimulusAction < StimulusAttributeBase
|
|
5
|
-
# https://stimulus.hotwired.dev/reference/actions#options
|
|
6
|
-
VALID_OPTIONS = [:once, :prevent, :stop, :passive, :"!passive", :capture, :self].freeze
|
|
7
|
-
|
|
8
|
-
# Typed descriptor for modifiers (`:once`/`:prevent`/etc., keyboard filter,
|
|
9
|
-
# `@window`) that the plain Array form can't express. Hash input to the
|
|
10
|
-
# parsers (`{event:, method:, ...}`) is desugared into one of these.
|
|
11
|
-
class Descriptor < ::Literal::Data
|
|
12
|
-
prop :method, _Union(Symbol, String)
|
|
13
|
-
prop :event, _Nilable(_Union(Symbol, String)), default: nil
|
|
14
|
-
prop :controller, _Nilable(String), default: nil
|
|
15
|
-
prop :options, _Array(Symbol), default: -> { [] }
|
|
16
|
-
prop :keyboard, _Nilable(String), default: nil
|
|
17
|
-
prop :window, _Boolean, default: false
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
attr_reader :event, :controller, :action, :options, :keyboard, :window
|
|
21
|
-
|
|
22
|
-
def initialize(*args, implied_controller: nil)
|
|
23
|
-
@options = []
|
|
24
|
-
@keyboard = nil
|
|
25
|
-
@window = false
|
|
26
|
-
super
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def to_s
|
|
30
|
-
head =
|
|
31
|
-
if @event
|
|
32
|
-
ev = @event.to_s
|
|
33
|
-
ev = "#{ev}.#{@keyboard}" if @keyboard
|
|
34
|
-
ev = "#{ev}#{@options.map { |o| ":#{o}" }.join}" if @options.any?
|
|
35
|
-
ev = "#{ev}@window" if @window
|
|
36
|
-
"#{ev}->"
|
|
37
|
-
else
|
|
38
|
-
""
|
|
39
|
-
end
|
|
40
|
-
"#{head}#{@controller}##{@action}"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def data_attribute_name = "action"
|
|
44
|
-
|
|
45
|
-
def data_attribute_value = to_s
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def parse_arguments(*args)
|
|
50
|
-
part1, part2, part3 = args
|
|
51
|
-
|
|
52
|
-
case args.size
|
|
53
|
-
when 1
|
|
54
|
-
parse_single_argument(part1)
|
|
55
|
-
when 2
|
|
56
|
-
parse_two_arguments(part1, part2)
|
|
57
|
-
when 3
|
|
58
|
-
parse_three_arguments(part1, part2, part3)
|
|
59
|
-
else
|
|
60
|
-
raise ArgumentError, "Invalid number of 'action' arguments: #{args.size}"
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def parse_single_argument(arg)
|
|
65
|
-
case arg
|
|
66
|
-
when Descriptor then apply_descriptor(arg)
|
|
67
|
-
when Hash then apply_descriptor(Descriptor.new(**arg))
|
|
68
|
-
when Symbol
|
|
69
|
-
@event = nil
|
|
70
|
-
@controller = implied_controller_name
|
|
71
|
-
@action = js_name(arg)
|
|
72
|
-
when String then parse_qualified_action_string(arg)
|
|
73
|
-
else raise ArgumentError, "Invalid 'action' argument type (1): #{arg.class}"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# (:event, :method) or ("controller/path", :method)
|
|
78
|
-
def parse_two_arguments(part1, part2)
|
|
79
|
-
if part1.is_a?(Symbol) && part2.is_a?(Symbol)
|
|
80
|
-
@event = part1.to_s
|
|
81
|
-
@controller = implied_controller_name
|
|
82
|
-
@action = js_name(part2)
|
|
83
|
-
elsif part1.is_a?(String) && part2.is_a?(Symbol)
|
|
84
|
-
@event = nil
|
|
85
|
-
@controller = stimulize_path(part1)
|
|
86
|
-
@action = js_name(part2)
|
|
87
|
-
else
|
|
88
|
-
raise ArgumentError, "Invalid 'action' argument types (2): #{part1.class}, #{part2.class}"
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# (:event, "controller/path", :method)
|
|
93
|
-
def parse_three_arguments(part1, part2, part3)
|
|
94
|
-
if part1.is_a?(Symbol) && part2.is_a?(String) && part3.is_a?(Symbol)
|
|
95
|
-
@event = part1.to_s
|
|
96
|
-
@controller = stimulize_path(part2)
|
|
97
|
-
@action = js_name(part3)
|
|
98
|
-
else
|
|
99
|
-
raise ArgumentError, "Invalid 'action' argument types (3): #{part1.class}, #{part2.class}, #{part3.class}"
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def apply_descriptor(d)
|
|
104
|
-
invalid = d.options - VALID_OPTIONS
|
|
105
|
-
unless invalid.empty?
|
|
106
|
-
raise ArgumentError,
|
|
107
|
-
"Invalid action option(s) #{invalid.inspect}. Valid: #{VALID_OPTIONS.inspect}"
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
@event = d.event&.to_s
|
|
111
|
-
@controller = d.controller ? stimulize_path(d.controller) : implied_controller_name
|
|
112
|
-
@action = d.method.is_a?(Symbol) ? js_name(d.method) : d.method.to_s
|
|
113
|
-
@options = d.options
|
|
114
|
-
@keyboard = d.keyboard
|
|
115
|
-
@window = d.window
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def parse_qualified_action_string(action_string)
|
|
119
|
-
if action_string.include?("->")
|
|
120
|
-
event_part, controller_action = action_string.split("->", 2)
|
|
121
|
-
@event = event_part
|
|
122
|
-
controller_part, action_part = controller_action.split("#", 2)
|
|
123
|
-
@controller = controller_part
|
|
124
|
-
@action = action_part
|
|
125
|
-
else
|
|
126
|
-
@event = nil
|
|
127
|
-
controller_part, action_part = action_string.split("#", 2)
|
|
128
|
-
@controller = controller_part
|
|
129
|
-
@action = action_part
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
@@ -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
|