vident 0.13.0 → 1.0.0.alpha1
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 +31 -1
- data/README.md +522 -681
- data/lib/vident/caching.rb +3 -3
- data/lib/vident/class_list_builder.rb +101 -0
- data/lib/vident/component.rb +76 -22
- data/lib/vident/component_attribute_resolver.rb +77 -0
- data/lib/vident/component_class_lists.rb +28 -0
- data/lib/vident/stimulus_action.rb +98 -0
- data/lib/vident/stimulus_action_collection.rb +11 -0
- data/lib/vident/stimulus_attribute_base.rb +63 -0
- data/lib/vident/stimulus_attributes.rb +257 -0
- data/lib/vident/stimulus_builder.rb +116 -0
- data/lib/vident/stimulus_class.rb +65 -0
- data/lib/vident/stimulus_class_collection.rb +15 -0
- data/lib/vident/stimulus_collection_base.rb +59 -0
- data/lib/vident/stimulus_component.rb +74 -0
- data/lib/vident/stimulus_controller.rb +44 -0
- data/lib/vident/stimulus_controller_collection.rb +14 -0
- data/lib/vident/stimulus_data_attribute_builder.rb +91 -0
- data/lib/vident/stimulus_dsl.rb +74 -0
- data/lib/vident/stimulus_outlet.rb +97 -0
- data/lib/vident/stimulus_outlet_collection.rb +15 -0
- data/lib/vident/stimulus_target.rb +57 -0
- data/lib/vident/stimulus_target_collection.rb +22 -0
- data/lib/vident/stimulus_value.rb +69 -0
- data/lib/vident/stimulus_value_collection.rb +15 -0
- data/lib/vident/tag_helper.rb +64 -0
- data/lib/vident/tailwind.rb +23 -0
- data/lib/vident/version.rb +1 -1
- data/lib/vident.rb +44 -3
- metadata +47 -6
- data/lib/vident/attributes/not_typed.rb +0 -81
- data/lib/vident/base.rb +0 -247
- data/lib/vident/root_component.rb +0 -309
data/lib/vident/caching.rb
CHANGED
@@ -15,7 +15,7 @@ module Vident
|
|
15
15
|
def with_cache_key(*attrs, name: :_collection)
|
16
16
|
# Add view file to cache key
|
17
17
|
attrs << :component_modified_time
|
18
|
-
attrs << :
|
18
|
+
attrs << :to_h if respond_to?(:to_h)
|
19
19
|
named_cache_key_includes(name, *attrs.uniq)
|
20
20
|
end
|
21
21
|
|
@@ -46,11 +46,11 @@ module Vident
|
|
46
46
|
def component_modified_time
|
47
47
|
return @component_modified_time if Rails.env.production? && @component_modified_time
|
48
48
|
|
49
|
-
raise StandardError, "Must implement
|
49
|
+
raise StandardError, "Must implement cache_component_modified_time" unless respond_to?(:cache_component_modified_time)
|
50
50
|
|
51
51
|
# FIXME: This could stack overflow if there are circular dependencies
|
52
52
|
deps = component_dependencies&.map(&:component_modified_time)&.join("-") || ""
|
53
|
-
@component_modified_time = deps +
|
53
|
+
@component_modified_time = deps + cache_component_modified_time
|
54
54
|
end
|
55
55
|
|
56
56
|
private
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module Vident
|
6
|
+
class ClassListBuilder
|
7
|
+
CLASSNAME_SEPARATOR = " "
|
8
|
+
|
9
|
+
def initialize(tailwind_merger: nil, component_name: nil, element_classes: nil, html_class: nil, additional_classes: nil)
|
10
|
+
@class_list = component_name ? [component_name] : []
|
11
|
+
@class_list.concat(Array.wrap(element_classes)) if element_classes
|
12
|
+
@class_list.concat(Array.wrap(html_class)) if html_class
|
13
|
+
@class_list.concat(Array.wrap(additional_classes)) if additional_classes
|
14
|
+
@class_list.compact!
|
15
|
+
@tailwind_merger = tailwind_merger
|
16
|
+
|
17
|
+
if @tailwind_merger && !defined?(::TailwindMerge::Merger)
|
18
|
+
raise LoadError, "TailwindMerge gem is required when using tailwind_merger:. Add 'gem \"tailwind_merge\"' to your Gemfile."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Main method to build a final class list from multiple sources
|
23
|
+
# @param class_lists [Array<String, Array, StimulusClass, nil>] Multiple class sources to merge
|
24
|
+
# @param stimulus_class_names [Array<Symbol, String>] Optional names of stimulus classes to include
|
25
|
+
# @return [String, nil] Final space-separated class string or nil if no classes
|
26
|
+
def build(extra_classes = nil, stimulus_class_names: [])
|
27
|
+
class_list = @class_list + Array.wrap(extra_classes).compact
|
28
|
+
flattened_classes = flatten_and_normalize_classes(class_list, stimulus_class_names)
|
29
|
+
return nil if flattened_classes.empty?
|
30
|
+
|
31
|
+
deduplicated_classes = dedupe_classes(flattened_classes)
|
32
|
+
return nil if deduplicated_classes.blank?
|
33
|
+
|
34
|
+
class_string = deduplicated_classes.join(CLASSNAME_SEPARATOR)
|
35
|
+
|
36
|
+
if @tailwind_merger
|
37
|
+
dedupe_with_tailwind(class_string)
|
38
|
+
else
|
39
|
+
class_string
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Flatten and normalize all input class sources
|
46
|
+
def flatten_and_normalize_classes(class_lists, stimulus_class_names)
|
47
|
+
stimulus_class_names_set = stimulus_class_names.map { |name| name.to_s.dasherize }.to_set
|
48
|
+
|
49
|
+
class_lists.compact.flat_map do |class_source|
|
50
|
+
case class_source
|
51
|
+
when String
|
52
|
+
class_source.split(CLASSNAME_SEPARATOR).reject(&:empty?)
|
53
|
+
when Array
|
54
|
+
class_source.flat_map { |item| normalize_single_class_item(item, stimulus_class_names_set) }
|
55
|
+
else
|
56
|
+
normalize_single_class_item(class_source, stimulus_class_names_set)
|
57
|
+
end
|
58
|
+
end.compact
|
59
|
+
end
|
60
|
+
|
61
|
+
# Normalize a single class item (could be string, StimulusClass, object with to_s, etc.)
|
62
|
+
def normalize_single_class_item(item, stimulus_class_names_set)
|
63
|
+
return [] if item.blank?
|
64
|
+
|
65
|
+
# Handle StimulusClass instances
|
66
|
+
if stimulus_class_instance?(item)
|
67
|
+
# Only include if the class name matches one of the requested names
|
68
|
+
# If stimulus_class_names_set is empty, exclude all stimulus classes
|
69
|
+
if stimulus_class_names_set.present? && stimulus_class_names_set.include?(item.class_name)
|
70
|
+
class_value = item.to_s
|
71
|
+
class_value.include?(CLASSNAME_SEPARATOR) ?
|
72
|
+
class_value.split(CLASSNAME_SEPARATOR).reject(&:empty?) :
|
73
|
+
[class_value]
|
74
|
+
else
|
75
|
+
[]
|
76
|
+
end
|
77
|
+
else
|
78
|
+
# Handle regular strings and other objects
|
79
|
+
item_string = item.to_s
|
80
|
+
item_string.include?(CLASSNAME_SEPARATOR) ?
|
81
|
+
item_string.split(CLASSNAME_SEPARATOR).reject(&:empty?) :
|
82
|
+
[item_string]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Check if an item is a StimulusClass instance
|
87
|
+
def stimulus_class_instance?(item)
|
88
|
+
item.respond_to?(:class_name) && item.respond_to?(:to_s)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Deduplicate classes while preserving order (first occurrence wins)
|
92
|
+
def dedupe_classes(class_array)
|
93
|
+
class_array.reject(&:blank?).uniq
|
94
|
+
end
|
95
|
+
|
96
|
+
# Merge classes using Tailwind CSS merge
|
97
|
+
def dedupe_with_tailwind(class_string)
|
98
|
+
@tailwind_merger.merge(class_string)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/vident/component.rb
CHANGED
@@ -4,29 +4,83 @@ module Vident
|
|
4
4
|
module Component
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
|
+
# Base class for all Vident components, which provides common functionality and properties.
|
7
8
|
included do
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
9
|
+
# The HTML tag to use for the root element of the component, defaults to `:div`.
|
10
|
+
prop :element_tag, Symbol, default: :div
|
11
|
+
# ID of the component. If not set, a random ID is generated.
|
12
|
+
prop :id, _Nilable(String)
|
13
|
+
# Classes to apply to the root element (they add to the `class` attribute)
|
14
|
+
prop :classes, _Union(String, _Array(String)), default: -> { [] }
|
15
|
+
# HTML options to apply to the root element (will merge into and potentially override html_options of the element)
|
16
|
+
prop :html_options, Hash, default: -> { {} }
|
17
|
+
end
|
18
|
+
|
19
|
+
include StimulusComponent
|
20
|
+
include ComponentClassLists
|
21
|
+
include ComponentAttributeResolver
|
22
|
+
|
23
|
+
include TagHelper
|
24
|
+
include Tailwind
|
25
|
+
include StimulusDSL
|
26
|
+
|
27
|
+
# Override this method to perform any initialisation after attributes are set
|
28
|
+
def after_component_initialize
|
29
|
+
end
|
30
|
+
|
31
|
+
# This can be overridden to return an array of extra class names, or a string of class names.
|
32
|
+
def element_classes
|
33
|
+
end
|
34
|
+
|
35
|
+
# Properties/attributes passed to the "root" element of the component. You normally override this method to
|
36
|
+
# return a hash of attributes that should be applied to the root element of your component.
|
37
|
+
def root_element_attributes
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Create a new component instance with optional overrides for properties.
|
42
|
+
def clone(overrides = {}) = self.class.new(**to_h.merge(**overrides))
|
43
|
+
|
44
|
+
def inspect(klass_name = "Component")
|
45
|
+
attr_text = to_h.map { |k, v| "#{k}=#{v.inspect}" }.join(", ")
|
46
|
+
"#<#{self.class.name}<Vident::#{klass_name}> #{attr_text}>"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Generate a unique ID for a component, can be overridden as required. Makes it easier to setup things like ARIA
|
50
|
+
# attributes which require elements to reference by ID. Note this overrides the `id` accessor
|
51
|
+
def id = @id.presence || random_id
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Called by Literal::Properties after the component is initialized.
|
56
|
+
def after_initialize
|
57
|
+
prepare_component_attributes
|
58
|
+
after_component_initialize if respond_to?(:after_component_initialize)
|
59
|
+
end
|
60
|
+
|
61
|
+
def root_element(&block)
|
62
|
+
raise NoMethodError, "You must implement the `root_element` method in your component"
|
63
|
+
end
|
64
|
+
|
65
|
+
def root(...)
|
66
|
+
root_element(...)
|
67
|
+
end
|
68
|
+
|
69
|
+
def root_element_tag_options
|
70
|
+
options = @html_options&.dup || {}
|
71
|
+
data_attrs = stimulus_data_attributes
|
72
|
+
options[:data] = options[:data].present? ? data_attrs.merge(options[:data]) : data_attrs
|
73
|
+
return options unless @id
|
74
|
+
options.merge(id: @id)
|
75
|
+
end
|
76
|
+
|
77
|
+
def root_element_tag_type
|
78
|
+
@element_tag.presence&.to_sym || :div
|
79
|
+
end
|
80
|
+
|
81
|
+
# Generate a random ID for the component, which is used to ensure uniqueness in the DOM.
|
82
|
+
def random_id
|
83
|
+
@random_id ||= "#{component_name}-#{StableId.next_id_in_sequence}"
|
30
84
|
end
|
31
85
|
end
|
32
86
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vident
|
4
|
+
module ComponentAttributeResolver
|
5
|
+
private
|
6
|
+
|
7
|
+
# FIXME: in a view_component the parsing of html_options might have to be in `before_render`
|
8
|
+
def prepare_component_attributes
|
9
|
+
prepare_stimulus_collections
|
10
|
+
|
11
|
+
# Add stimulus attributes from DSL first (lower precedence)
|
12
|
+
add_stimulus_attributes_from_dsl
|
13
|
+
|
14
|
+
# Process root_element_attributes (higher precedence)
|
15
|
+
extra = root_element_attributes
|
16
|
+
@html_options = (extra[:html_options] || {}).merge(@html_options) if extra.key?(:html_options)
|
17
|
+
@html_options[:class] = render_classes(extra[:classes])
|
18
|
+
@html_options[:id] = (extra[:id] || id) unless @html_options.key?(:id)
|
19
|
+
@element_tag = extra[:element_tag] if extra.key?(:element_tag)
|
20
|
+
|
21
|
+
add_stimulus_controllers(extra[:stimulus_controllers]) if extra.key?(:stimulus_controllers)
|
22
|
+
add_stimulus_actions(extra[:stimulus_actions]) if extra.key?(:stimulus_actions)
|
23
|
+
add_stimulus_targets(extra[:stimulus_targets]) if extra.key?(:stimulus_targets)
|
24
|
+
add_stimulus_outlets(extra[:stimulus_outlets]) if extra.key?(:stimulus_outlets)
|
25
|
+
add_stimulus_values(extra[:stimulus_values]) if extra.key?(:stimulus_values)
|
26
|
+
add_stimulus_classes(extra[:stimulus_classes]) if extra.key?(:stimulus_classes)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add stimulus attributes from DSL declarations using existing add_* methods
|
30
|
+
def add_stimulus_attributes_from_dsl
|
31
|
+
dsl_attrs = self.class.stimulus_dsl_attributes(self)
|
32
|
+
return if dsl_attrs.empty?
|
33
|
+
|
34
|
+
# Use existing add_* methods to integrate DSL attributes
|
35
|
+
add_stimulus_controllers(dsl_attrs[:stimulus_controllers]) if dsl_attrs[:stimulus_controllers]
|
36
|
+
add_stimulus_actions(dsl_attrs[:stimulus_actions]) if dsl_attrs[:stimulus_actions]
|
37
|
+
add_stimulus_targets(dsl_attrs[:stimulus_targets]) if dsl_attrs[:stimulus_targets]
|
38
|
+
add_stimulus_outlets(dsl_attrs[:stimulus_outlets]) if dsl_attrs[:stimulus_outlets]
|
39
|
+
|
40
|
+
# Add static values (now includes resolved proc values)
|
41
|
+
add_stimulus_values(dsl_attrs[:stimulus_values]) if dsl_attrs[:stimulus_values]
|
42
|
+
|
43
|
+
# Resolve and add values from props
|
44
|
+
if dsl_attrs[:stimulus_values_from_props]
|
45
|
+
resolved_values = resolve_values_from_props(dsl_attrs[:stimulus_values_from_props])
|
46
|
+
add_stimulus_values(resolved_values) unless resolved_values.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
add_stimulus_classes(dsl_attrs[:stimulus_classes]) if dsl_attrs[:stimulus_classes]
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# Prepare stimulus collections and implied controller path from the given attributes, called after initialization
|
54
|
+
def prepare_stimulus_collections # Convert raw attributes to stimulus attribute collections
|
55
|
+
@stimulus_controllers_collection = stimulus_controllers(*Array.wrap(@stimulus_controllers))
|
56
|
+
@stimulus_actions_collection = stimulus_actions(*Array.wrap(@stimulus_actions))
|
57
|
+
@stimulus_targets_collection = stimulus_targets(*Array.wrap(@stimulus_targets))
|
58
|
+
@stimulus_outlets_collection = stimulus_outlets(*Array.wrap(@stimulus_outlets))
|
59
|
+
@stimulus_values_collection = stimulus_values(*Array.wrap(@stimulus_values))
|
60
|
+
@stimulus_classes_collection = stimulus_classes(*Array.wrap(@stimulus_classes))
|
61
|
+
|
62
|
+
@stimulus_outlet_host.add_stimulus_outlets(self) if @stimulus_outlet_host
|
63
|
+
end
|
64
|
+
|
65
|
+
# Build stimulus data attributes using collection splat
|
66
|
+
def stimulus_data_attributes
|
67
|
+
StimulusDataAttributeBuilder.new(
|
68
|
+
controllers: @stimulus_controllers_collection,
|
69
|
+
actions: @stimulus_actions_collection,
|
70
|
+
targets: @stimulus_targets_collection,
|
71
|
+
outlets: @stimulus_outlets_collection,
|
72
|
+
values: @stimulus_values_collection,
|
73
|
+
classes: @stimulus_classes_collection
|
74
|
+
).build
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,28 @@
|
|
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(extra_classes = nil) = class_list_builder.build(extra_classes)
|
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
|
+
class_list_builder.build(@stimulus_classes_collection&.to_a, stimulus_class_names: names) || ""
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# Get or create a class list builder instance
|
17
|
+
# Automatically detects if Tailwind module is included and TailwindMerge gem is available
|
18
|
+
def class_list_builder
|
19
|
+
@class_list_builder ||= ClassListBuilder.new(
|
20
|
+
tailwind_merger:,
|
21
|
+
component_name:,
|
22
|
+
element_classes:,
|
23
|
+
additional_classes: @classes,
|
24
|
+
html_class: @html_options&.fetch(:class, nil)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vident
|
4
|
+
class StimulusAction < StimulusAttributeBase
|
5
|
+
attr_reader :event, :controller, :action
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
if @event
|
9
|
+
"#{@event}->#{@controller}##{@action}"
|
10
|
+
else
|
11
|
+
"#{@controller}##{@action}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def data_attribute_name
|
16
|
+
"action"
|
17
|
+
end
|
18
|
+
|
19
|
+
def data_attribute_value
|
20
|
+
to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def parse_arguments(*args)
|
26
|
+
part1, part2, part3 = args
|
27
|
+
|
28
|
+
case args.size
|
29
|
+
when 1
|
30
|
+
parse_single_argument(part1)
|
31
|
+
when 2
|
32
|
+
parse_two_arguments(part1, part2)
|
33
|
+
when 3
|
34
|
+
parse_three_arguments(part1, part2, part3)
|
35
|
+
else
|
36
|
+
raise ArgumentError, "Invalid number of 'action' arguments: #{args.size}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_single_argument(arg)
|
41
|
+
if arg.is_a?(Symbol)
|
42
|
+
# 1 symbol arg, name of method on implied controller
|
43
|
+
@event = nil
|
44
|
+
@controller = implied_controller_name
|
45
|
+
@action = js_name(arg)
|
46
|
+
elsif arg.is_a?(String)
|
47
|
+
# 1 string arg, fully qualified action - parse it
|
48
|
+
parse_qualified_action_string(arg)
|
49
|
+
else
|
50
|
+
raise ArgumentError, "Invalid 'action' argument types (1): #{arg.class}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_two_arguments(part1, part2)
|
55
|
+
if part1.is_a?(Symbol) && part2.is_a?(Symbol)
|
56
|
+
# 2 symbol args = event + action
|
57
|
+
@event = part1.to_s
|
58
|
+
@controller = implied_controller_name
|
59
|
+
@action = js_name(part2)
|
60
|
+
elsif part1.is_a?(String) && part2.is_a?(Symbol)
|
61
|
+
# 1 string arg, 1 symbol = controller + action
|
62
|
+
@event = nil
|
63
|
+
@controller = stimulize_path(part1)
|
64
|
+
@action = js_name(part2)
|
65
|
+
else
|
66
|
+
raise ArgumentError, "Invalid 'action' argument types (2): #{part1.class}, #{part2.class}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_three_arguments(part1, part2, part3)
|
71
|
+
if part1.is_a?(Symbol) && part2.is_a?(String) && part3.is_a?(Symbol)
|
72
|
+
# 1 symbol, 1 string, 1 symbol = event + controller + action
|
73
|
+
@event = part1.to_s
|
74
|
+
@controller = stimulize_path(part2)
|
75
|
+
@action = js_name(part3)
|
76
|
+
else
|
77
|
+
raise ArgumentError, "Invalid 'action' argument types (3): #{part1.class}, #{part2.class}, #{part3.class}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def parse_qualified_action_string(action_string)
|
82
|
+
if action_string.include?("->")
|
83
|
+
# Has event: "click->controller#action"
|
84
|
+
event_part, controller_action = action_string.split("->", 2)
|
85
|
+
@event = event_part
|
86
|
+
controller_part, action_part = controller_action.split("#", 2)
|
87
|
+
@controller = controller_part
|
88
|
+
@action = action_part
|
89
|
+
else
|
90
|
+
# No event: "controller#action"
|
91
|
+
@event = nil
|
92
|
+
controller_part, action_part = action_string.split("#", 2)
|
93
|
+
@controller = controller_part
|
94
|
+
@action = action_part
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/string/inflections"
|
4
|
+
|
5
|
+
module Vident
|
6
|
+
class StimulusAttributeBase
|
7
|
+
attr_reader :implied_controller
|
8
|
+
|
9
|
+
def initialize(*args, implied_controller: nil)
|
10
|
+
@implied_controller = implied_controller
|
11
|
+
parse_arguments(*args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def inspect
|
15
|
+
"#<#{self.class.name} #{to_h}>"
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
raise NoMethodError, "Subclasses must implement to_s"
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{data_attribute_name => data_attribute_value}
|
24
|
+
end
|
25
|
+
|
26
|
+
alias_method :to_hash, :to_h
|
27
|
+
|
28
|
+
def data_attribute_name
|
29
|
+
raise NoMethodError, "Subclasses must implement data_attribute_name"
|
30
|
+
end
|
31
|
+
|
32
|
+
def data_attribute_value
|
33
|
+
raise NoMethodError, "Subclasses must implement data_attribute_value"
|
34
|
+
end
|
35
|
+
|
36
|
+
def implied_controller_path
|
37
|
+
raise ArgumentError, "implied_controller is required to get implied controller path" unless implied_controller
|
38
|
+
implied_controller.path
|
39
|
+
end
|
40
|
+
|
41
|
+
def implied_controller_name
|
42
|
+
raise ArgumentError, "implied_controller is required to get implied controller name" unless implied_controller
|
43
|
+
implied_controller.name
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Convert a file path to a stimulus controller name
|
49
|
+
def stimulize_path(path)
|
50
|
+
path.split("/").map { |p| p.to_s.dasherize }.join("--")
|
51
|
+
end
|
52
|
+
|
53
|
+
# Convert a Ruby 'snake case' string to a JavaScript camel case strings
|
54
|
+
def js_name(name)
|
55
|
+
name.to_s.camelize(:lower)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Subclasses must implement this method
|
59
|
+
def parse_arguments(*args)
|
60
|
+
raise NotImplementedError, "Subclasses must implement parse_arguments"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|