vident 0.6.3 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +224 -38
- data/Rakefile +1 -12
- data/lib/tasks/vident_tasks.rake +4 -0
- data/lib/vident/attributes/not_typed.rb +3 -0
- data/lib/vident/base.rb +8 -29
- data/lib/vident/component.rb +0 -2
- data/lib/vident/engine.rb +15 -0
- data/lib/vident/root_component.rb +235 -0
- data/lib/vident/version.rb +1 -1
- data/lib/vident.rb +3 -36
- metadata +11 -28
- data/.standard.yml +0 -3
- data/CHANGELOG.md +0 -69
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -34
- data/LICENSE.txt +0 -21
- data/examples/ex1.gif +0 -0
- data/lib/tasks/vident.rake +0 -37
- data/lib/vident/attributes/typed.rb +0 -229
- data/lib/vident/attributes/typed_niling_struct.rb +0 -27
- data/lib/vident/attributes/types.rb +0 -16
- data/lib/vident/caching/cache_key.rb +0 -145
- data/lib/vident/railtie.rb +0 -10
- data/lib/vident/root_component/base.rb +0 -237
- data/lib/vident/root_component/using_better_html.rb +0 -41
- data/lib/vident/root_component/using_phlex_html.rb +0 -49
- data/lib/vident/root_component/using_view_component.rb +0 -51
- data/lib/vident/test_case.rb +0 -8
- data/lib/vident/testing/attributes_tester.rb +0 -176
- data/lib/vident/testing/auto_test.rb +0 -70
- data/lib/vident/typed_component.rb +0 -48
- data/sig/vident.rbs +0 -4
@@ -1,229 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "active_support/concern"
|
4
|
-
|
5
|
-
if Gem.loaded_specs.has_key? "dry-struct"
|
6
|
-
require_relative "./types"
|
7
|
-
require_relative "./typed_niling_struct"
|
8
|
-
|
9
|
-
module Vident
|
10
|
-
# Adapts Dry Types to confinus Typed Attributes. We use dry-struct (see ::Core::NilingStruct) but
|
11
|
-
# we could probably also use dry-initializer directly, saving us from maintaining the schema.
|
12
|
-
module Attributes
|
13
|
-
module Typed
|
14
|
-
extend ActiveSupport::Concern
|
15
|
-
|
16
|
-
# TODO: better handling of when either class.schema is undefined (as no attributes configured) or when
|
17
|
-
# other methods ar called before prepare_attributes is called
|
18
|
-
def prepare_attributes(attributes)
|
19
|
-
@__attributes = self.class.schema.new(**attributes)
|
20
|
-
end
|
21
|
-
|
22
|
-
def attributes
|
23
|
-
@__attributes.attributes
|
24
|
-
end
|
25
|
-
|
26
|
-
def attribute_names
|
27
|
-
@attribute_names ||= self.class.attribute_names
|
28
|
-
end
|
29
|
-
|
30
|
-
def attribute(key)
|
31
|
-
if Rails.env.development? && !key?(key)
|
32
|
-
raise StandardError, "Attribute #{key} not found in #{self.class.name}"
|
33
|
-
end
|
34
|
-
@__attributes.attributes[key]
|
35
|
-
end
|
36
|
-
alias_method :[], :attribute
|
37
|
-
|
38
|
-
def key?(key)
|
39
|
-
self.class.schema.attribute_names.include?(key)
|
40
|
-
end
|
41
|
-
|
42
|
-
def to_hash
|
43
|
-
@__attributes.to_h
|
44
|
-
end
|
45
|
-
|
46
|
-
class_methods do
|
47
|
-
def inherited(subclass)
|
48
|
-
subclass.instance_variable_set(:@schema, @schema.clone)
|
49
|
-
subclass.instance_variable_set(:@attribute_ivar_names, @attribute_ivar_names.clone)
|
50
|
-
super
|
51
|
-
end
|
52
|
-
|
53
|
-
def attribute_names
|
54
|
-
schema.attribute_names
|
55
|
-
end
|
56
|
-
|
57
|
-
def attribute_metadata(key)
|
58
|
-
schema.schema.key(key).meta
|
59
|
-
end
|
60
|
-
|
61
|
-
attr_reader :schema, :attribute_ivar_names
|
62
|
-
|
63
|
-
def attribute(name, signature = :any, **options, &converter)
|
64
|
-
strict = !options[:convert]
|
65
|
-
signatures = extract_member_type_and_subclass(signature, options)
|
66
|
-
type_info = map_primitive_to_dry_type(signatures, strict, converter)
|
67
|
-
type_info = set_constraints(type_info, options)
|
68
|
-
type_info = set_metadata(type_info, signatures, options)
|
69
|
-
define_on_schema(name, type_info, options)
|
70
|
-
end
|
71
|
-
|
72
|
-
private
|
73
|
-
|
74
|
-
def set_constraints(type_info, options)
|
75
|
-
if allows_nil?(options)
|
76
|
-
type_info = type_info.optional.meta(required: false)
|
77
|
-
end
|
78
|
-
unless allows_blank?(options)
|
79
|
-
type_info = type_info.constrained(filled: true)
|
80
|
-
end
|
81
|
-
if options[:default]&.is_a?(Proc)
|
82
|
-
type_info = type_info.default(options[:default].freeze)
|
83
|
-
elsif !options[:default].nil?
|
84
|
-
type_info = type_info.default(->(_) { options[:default] }.freeze)
|
85
|
-
end
|
86
|
-
if options[:in]
|
87
|
-
type_info.constrained(included_in: options[:in].freeze)
|
88
|
-
end
|
89
|
-
type_info
|
90
|
-
end
|
91
|
-
|
92
|
-
def set_metadata(type_info, signatures, options)
|
93
|
-
metadata = {typed_attribute_type: signatures, typed_attribute_options: options}
|
94
|
-
type_info.meta(**metadata)
|
95
|
-
end
|
96
|
-
|
97
|
-
def delegates?(options)
|
98
|
-
options[:delegates] != false
|
99
|
-
end
|
100
|
-
|
101
|
-
def define_on_schema(attribute_name, type_info, options)
|
102
|
-
@attribute_ivar_names ||= {}
|
103
|
-
@attribute_ivar_names[attribute_name] = :"@#{attribute_name}"
|
104
|
-
define_attribute_delegate(attribute_name) if delegates?(options)
|
105
|
-
@schema ||= const_set(:TypedSchema, Class.new(Vident::Attributes::TypedNilingStruct))
|
106
|
-
@schema.attribute attribute_name, type_info
|
107
|
-
end
|
108
|
-
|
109
|
-
def define_attribute_delegate(attr_name)
|
110
|
-
# Define reader & presence check method
|
111
|
-
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
112
|
-
def #{attr_name}
|
113
|
-
@__attributes.attributes[:#{attr_name}]
|
114
|
-
end
|
115
|
-
|
116
|
-
def #{attr_name}?
|
117
|
-
send(:#{attr_name}).present?
|
118
|
-
end
|
119
|
-
RUBY
|
120
|
-
end
|
121
|
-
|
122
|
-
def allows_nil?(options)
|
123
|
-
return true unless options
|
124
|
-
allow_nil = options[:allow_nil]
|
125
|
-
return false if allow_nil == false
|
126
|
-
allow_nil || allows_blank?(options)
|
127
|
-
end
|
128
|
-
|
129
|
-
def allows_blank?(options)
|
130
|
-
return true unless options
|
131
|
-
allow_blank = options[:allow_blank]
|
132
|
-
allow_blank.nil? ? true : allow_blank
|
133
|
-
end
|
134
|
-
|
135
|
-
def map_primitive_to_dry_type(signatures, strict, converter)
|
136
|
-
types = signatures.map do |type, subtype|
|
137
|
-
dry_type = dry_type_from_primary_type(type, strict, converter)
|
138
|
-
if subtype && dry_type.respond_to?(:of)
|
139
|
-
subtype_info = dry_type_from_primary_type(subtype, strict, converter)
|
140
|
-
# Sub types of collections currently can be nil - this should be an option
|
141
|
-
dry_type.of(subtype_info.optional.meta(required: false))
|
142
|
-
else
|
143
|
-
dry_type
|
144
|
-
end
|
145
|
-
end
|
146
|
-
types.reduce(:|)
|
147
|
-
end
|
148
|
-
|
149
|
-
def extract_member_type_and_subclass(signature, options)
|
150
|
-
case signature
|
151
|
-
when Set
|
152
|
-
signature.flat_map { |s| extract_member_type_and_subclass(s, options) }
|
153
|
-
when Array
|
154
|
-
[[Array, signature.first]]
|
155
|
-
else
|
156
|
-
[[signature, options[:type] || options[:sub_type]]]
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
def dry_type_from_primary_type(type, strict, converter)
|
161
|
-
# If a converter is provided, we should use it to coerce the value
|
162
|
-
if converter && !strict && !type.is_a?(Symbol)
|
163
|
-
return Types.Constructor(type) do |value|
|
164
|
-
next value if value.is_a?(type)
|
165
|
-
|
166
|
-
converter.call(value).tap do |new_value|
|
167
|
-
unless new_value.is_a?(type)
|
168
|
-
raise ArgumentError, "Type conversion proc did not convert #{value} to #{type}"
|
169
|
-
end
|
170
|
-
end
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
if type == :any
|
175
|
-
Types::Nominal::Any
|
176
|
-
elsif type == Integer
|
177
|
-
strict ? Types::Strict::Integer : Types::Params::Integer
|
178
|
-
elsif type == BigDecimal
|
179
|
-
strict ? Types::Strict::Decimal : Types::Params::Decimal
|
180
|
-
elsif type == Float
|
181
|
-
strict ? Types::Strict::Float : Types::Params::Float
|
182
|
-
elsif type == Numeric
|
183
|
-
if strict
|
184
|
-
Types::Strict::Float | Types::Strict::Integer | Types::Strict::Decimal
|
185
|
-
else
|
186
|
-
Types::Params::Float | Types::Params::Integer | Types::Params::Decimal
|
187
|
-
end
|
188
|
-
elsif type == Symbol
|
189
|
-
strict ? Types::Strict::Symbol : Types::Coercible::Symbol
|
190
|
-
elsif type == String
|
191
|
-
strict ? Types::Strict::String : Types::Coercible::String
|
192
|
-
elsif type == Time
|
193
|
-
strict ? Types::Strict::Time : Types::Params::Time
|
194
|
-
elsif type == Date
|
195
|
-
strict ? Types::Strict::Date : Types::Params::Date
|
196
|
-
elsif type == Array
|
197
|
-
strict ? Types::Strict::Array : Types::Params::Array
|
198
|
-
elsif type == Hash
|
199
|
-
strict ? Types::Strict::Hash : Types::Coercible::Hash
|
200
|
-
elsif type == :boolean
|
201
|
-
strict ? Types::Strict::Bool : Types::Params::Bool
|
202
|
-
elsif strict
|
203
|
-
# when strict create a Nominal type with a is_a? constraint, otherwise create a Nominal type which constructs
|
204
|
-
# values using the default constructor, `new`.
|
205
|
-
Types.Instance(type)
|
206
|
-
else
|
207
|
-
# dry calls this when initialising the Type. Check if type of input is correct or create new instance
|
208
|
-
Types.Constructor(type) do |value|
|
209
|
-
next value if value.is_a?(type)
|
210
|
-
|
211
|
-
type.new(**value)
|
212
|
-
end
|
213
|
-
end
|
214
|
-
end
|
215
|
-
end
|
216
|
-
end
|
217
|
-
end
|
218
|
-
end
|
219
|
-
else
|
220
|
-
module Vident
|
221
|
-
module Attributes
|
222
|
-
module Typed
|
223
|
-
def self.included(base)
|
224
|
-
raise "Vident::Attributes::Typed requires dry-struct to be installed"
|
225
|
-
end
|
226
|
-
end
|
227
|
-
end
|
228
|
-
end
|
229
|
-
end
|
@@ -1,27 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Vident
|
4
|
-
module Attributes
|
5
|
-
# A dry struct that is loose about keys provided on initialization. It sets them to nil if not provided.
|
6
|
-
# It is strict about types but not about provided keys
|
7
|
-
class TypedNilingStruct < ::Dry::Struct
|
8
|
-
# convert string keys to symbols
|
9
|
-
transform_keys(&:to_sym)
|
10
|
-
|
11
|
-
# resolve default types on nil
|
12
|
-
transform_types do |type|
|
13
|
-
if type.default?
|
14
|
-
type.constructor { |value| value.nil? ? ::Dry::Types::Undefined : value }
|
15
|
-
else
|
16
|
-
type
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
class << self
|
21
|
-
def check_schema_duplication(new_keys)
|
22
|
-
# allow overriding keys
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
@@ -1,16 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "dry-struct"
|
4
|
-
|
5
|
-
module Vident
|
6
|
-
module Attributes
|
7
|
-
module Types
|
8
|
-
include ::Dry.Types
|
9
|
-
|
10
|
-
StrippedString = Types::String.constructor(&:strip)
|
11
|
-
BooleanDefaultFalse = Types::Bool.default(false)
|
12
|
-
BooleanDefaultTrue = Types::Bool.default(true)
|
13
|
-
HashDefaultEmpty = Types::Hash.default({}.freeze)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
@@ -1,145 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Rails fragment caching works by either expecting the cached key object to respond to `cache_key` or for that object
|
4
|
-
# to be an array or hash. In our case the object maybe an instance of Core::Presenter so here we add a default
|
5
|
-
# `cache_key` implementation.
|
6
|
-
module Vident
|
7
|
-
module Caching
|
8
|
-
module CacheKey
|
9
|
-
extend ActiveSupport::Concern
|
10
|
-
|
11
|
-
class_methods do
|
12
|
-
def inherited(subclass)
|
13
|
-
subclass.instance_variable_set(
|
14
|
-
:@named_cache_key_attributes,
|
15
|
-
@named_cache_key_attributes.clone
|
16
|
-
)
|
17
|
-
super
|
18
|
-
end
|
19
|
-
|
20
|
-
def with_cache_key(*attrs, name: :_collection)
|
21
|
-
raise StandardError, "with_cache_key can only be used on components *without* slots as there is no eary way to track their content changes so too risky" if respond_to?(:slots?) && slots?
|
22
|
-
# Add view file to cache key
|
23
|
-
attrs << :component_modified_time
|
24
|
-
named_cache_key_includes(name, *attrs)
|
25
|
-
end
|
26
|
-
|
27
|
-
attr_reader :named_cache_key_attributes
|
28
|
-
|
29
|
-
# TypedComponents can be used with fragment caching, but you need to be careful! Read on...
|
30
|
-
#
|
31
|
-
# <% cache component do %>
|
32
|
-
# <%= render component %>
|
33
|
-
# <% end %>
|
34
|
-
#
|
35
|
-
# The most important point is that Rails cannot track dependencies on the component itself, so you need to
|
36
|
-
# be careful to be explicit on the attributes, and manually specify any sub Viewcomponent dependencies that the
|
37
|
-
# component has. The assumption is that the subcomponent takes any attributes from the parent, so the cache key
|
38
|
-
# depends on the parent component attributes. Otherwise changes to the parent or sub component views/Ruby class
|
39
|
-
# will result in different cache keys too. Of course if you invalidate all cache keys with a modifier on deploy
|
40
|
-
# then no need to worry about changing the cache key on component changes, only on attribute/data changes.
|
41
|
-
#
|
42
|
-
# A big caveat is that the cache key cannot depend on anything related to the view_context of the component (such
|
43
|
-
# as `helpers` as the key is created before the rending pipline is invoked (which is when the view_context is set).
|
44
|
-
def depends_on(*klasses)
|
45
|
-
@component_dependencies ||= []
|
46
|
-
@component_dependencies += klasses
|
47
|
-
end
|
48
|
-
|
49
|
-
attr_reader :component_dependencies
|
50
|
-
|
51
|
-
def component_modified_time
|
52
|
-
return @component_modified_time if Rails.env.production? && @component_modified_time
|
53
|
-
# FIXME: This could stack overflow if there are circular dependencies
|
54
|
-
deps = component_dependencies&.map(&:component_modified_time)&.join("-") || ""
|
55
|
-
@component_modified_time = deps + sidecar_view_modified_time + rb_component_modified_time
|
56
|
-
end
|
57
|
-
|
58
|
-
def sidecar_view_modified_time
|
59
|
-
return @sidecar_view_modified_time if Rails.env.production? && defined?(@sidecar_view_modified_time)
|
60
|
-
@sidecar_view_modified_time = ::File.exist?(template_path) ? ::File.mtime(template_path).to_i.to_s : ""
|
61
|
-
end
|
62
|
-
|
63
|
-
def rb_component_modified_time
|
64
|
-
return @rb_component_modified_time if Rails.env.production? && defined?(@rb_component_modified_time)
|
65
|
-
@rb_component_modified_time = ::File.exist?(component_path) ? ::File.mtime(component_path).to_i.to_s : ""
|
66
|
-
end
|
67
|
-
|
68
|
-
def template_path
|
69
|
-
File.join components_base_path, "#{virtual_path}.html.erb"
|
70
|
-
end
|
71
|
-
|
72
|
-
def component_path
|
73
|
-
File.join components_base_path, "#{virtual_path}.rb"
|
74
|
-
end
|
75
|
-
|
76
|
-
def components_base_path
|
77
|
-
::Rails.configuration.view_component.view_component_path || "app/components"
|
78
|
-
end
|
79
|
-
|
80
|
-
private
|
81
|
-
|
82
|
-
def named_cache_key_includes(name, *attrs)
|
83
|
-
define_cache_key_method unless @named_cache_key_attributes
|
84
|
-
@named_cache_key_attributes ||= {}
|
85
|
-
@named_cache_key_attributes[name] = attrs
|
86
|
-
end
|
87
|
-
|
88
|
-
def define_cache_key_method
|
89
|
-
# If the presenter defines cache key setup then define the method. Otherwise Rails assumes this
|
90
|
-
# will return a valid key if the class will respond to this
|
91
|
-
define_method :cache_key do |n = :_collection|
|
92
|
-
if defined?(@cache_key)
|
93
|
-
return @cache_key[n] if @cache_key.key?(n)
|
94
|
-
else
|
95
|
-
@cache_key ||= {}
|
96
|
-
end
|
97
|
-
generate_cache_key(n)
|
98
|
-
@cache_key[n]
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
# Component modified time which is combined with other cache key attributes to generate cache key for an instance
|
104
|
-
def component_modified_time
|
105
|
-
self.class.component_modified_time
|
106
|
-
end
|
107
|
-
|
108
|
-
def cacheable?
|
109
|
-
respond_to? :cache_key
|
110
|
-
end
|
111
|
-
|
112
|
-
def cache_key_modifier
|
113
|
-
ENV["RAILS_CACHE_ID"]
|
114
|
-
end
|
115
|
-
|
116
|
-
def cache_keys_for_sources(key_attributes)
|
117
|
-
sources = key_attributes.flat_map { |n| n.is_a?(Proc) ? instance_eval(&n) : send(n) }
|
118
|
-
sources.compact.map do |item|
|
119
|
-
next if item == self
|
120
|
-
generate_item_cache_key_from(item)
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def generate_item_cache_key_from(item)
|
125
|
-
if item.respond_to? :cache_key_with_version
|
126
|
-
item.cache_key_with_version
|
127
|
-
elsif item.respond_to? :cache_key
|
128
|
-
item.cache_key
|
129
|
-
elsif item.is_a?(String)
|
130
|
-
Digest::SHA1.hexdigest(item)
|
131
|
-
else
|
132
|
-
Digest::SHA1.hexdigest(Marshal.dump(item))
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def generate_cache_key(index)
|
137
|
-
key_attributes = self.class.named_cache_key_attributes[index]
|
138
|
-
return nil unless key_attributes
|
139
|
-
key = "#{self.class.name}/#{cache_keys_for_sources(key_attributes).join("/")}"
|
140
|
-
raise StandardError, "Cache key for key #{key} is blank!" if key.blank?
|
141
|
-
@cache_key[index] = cache_key_modifier.present? ? "#{key}/#{cache_key_modifier}" : key
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
data/lib/vident/railtie.rb
DELETED
@@ -1,237 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Vident
|
4
|
-
module RootComponent
|
5
|
-
module Base
|
6
|
-
def initialize(
|
7
|
-
controllers: nil,
|
8
|
-
actions: nil,
|
9
|
-
targets: nil,
|
10
|
-
named_classes: nil, # https://stimulus.hotwired.dev/reference/css-classes
|
11
|
-
data_maps: nil,
|
12
|
-
element_tag: nil,
|
13
|
-
id: nil,
|
14
|
-
html_options: nil
|
15
|
-
)
|
16
|
-
@element_tag = element_tag
|
17
|
-
@html_options = html_options
|
18
|
-
@id = id
|
19
|
-
@controllers = Array.wrap(controllers)
|
20
|
-
@actions = actions
|
21
|
-
@targets = targets
|
22
|
-
@named_classes = named_classes
|
23
|
-
@data_map_kvs = {}
|
24
|
-
@data_maps = data_maps
|
25
|
-
end
|
26
|
-
|
27
|
-
# The view component's helpers for setting stimulus data-* attributes on this component.
|
28
|
-
|
29
|
-
# TODO: rename
|
30
|
-
# Create a Stimulus action string, and returns it
|
31
|
-
# examples:
|
32
|
-
# action(:my_thing) => "current_controller#myThing"
|
33
|
-
# action(:click, :my_thing) => "click->current_controller#myThing"
|
34
|
-
# action("click->current_controller#myThing") => "click->current_controller#myThing"
|
35
|
-
# action("path/to/current", :my_thing) => "path--to--current_controller#myThing"
|
36
|
-
# action(:click, "path/to/current", :my_thing) => "click->path--to--current_controller#myThing"
|
37
|
-
def action(*args)
|
38
|
-
part1, part2, part3 = args
|
39
|
-
(args.size == 1) ? parse_action_arg(part1) : parse_multiple_action_args(part1, part2, part3)
|
40
|
-
end
|
41
|
-
|
42
|
-
def action_data_attribute(*actions)
|
43
|
-
{action: parse_actions(actions).join(" ")}
|
44
|
-
end
|
45
|
-
|
46
|
-
# TODO: rename & make stimulus Target class instance and returns it, which can convert to String
|
47
|
-
# Create a Stimulus Target and returns it
|
48
|
-
# examples:
|
49
|
-
# target(:my_target) => {controller: 'current_controller' name: 'myTarget'}
|
50
|
-
# target("path/to/current", :my_target) => {controller: 'path--to--current_controller', name: 'myTarget'}
|
51
|
-
def target(name, part2 = nil)
|
52
|
-
if part2.nil?
|
53
|
-
{controller: implied_controller_name, name: js_name(name)}
|
54
|
-
else
|
55
|
-
{controller: stimulize_path(name), name: js_name(part2)}
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def target_data_attribute(name)
|
60
|
-
build_target_data_attributes([target(name)])
|
61
|
-
end
|
62
|
-
|
63
|
-
# Getter for a named classes list so can be used in view to set initial state on SSR
|
64
|
-
# Returns a String of classes that can be used in a `class` attribute.
|
65
|
-
def named_classes(*names)
|
66
|
-
names.map { |name| convert_classes_list_to_string(@named_classes[name]) }.join(" ")
|
67
|
-
end
|
68
|
-
|
69
|
-
# Helpers for generating the Stimulus data-* attributes directly
|
70
|
-
|
71
|
-
# Return the HTML `data-controller` attribute for the given controllers
|
72
|
-
def with_controllers(*controllers_to_set)
|
73
|
-
"data-controller=\"#{controller_list(controllers_to_set)}\"".html_safe
|
74
|
-
end
|
75
|
-
|
76
|
-
# Return the HTML `data-target` attribute for the given targets
|
77
|
-
def as_targets(*targets)
|
78
|
-
attrs = build_target_data_attributes(parse_targets(targets))
|
79
|
-
attrs.map { |dt, n| "data-#{dt}=\"#{n}\"" }.join(" ").html_safe
|
80
|
-
end
|
81
|
-
alias_method :as_target, :as_targets
|
82
|
-
|
83
|
-
# Return the HTML `data-action` attribute for the given actions
|
84
|
-
def with_actions(*actions_to_set)
|
85
|
-
"data-action='#{parse_actions(actions_to_set).join(" ")}'".html_safe
|
86
|
-
end
|
87
|
-
alias_method :with_action, :with_actions
|
88
|
-
|
89
|
-
private
|
90
|
-
|
91
|
-
# An implicit Stimulus controller name is built from the implicit controller path
|
92
|
-
def implied_controller_name
|
93
|
-
stimulize_path(implied_controller_path)
|
94
|
-
end
|
95
|
-
|
96
|
-
# When using the DSL if you dont specify, the first controller is implied
|
97
|
-
def implied_controller_path
|
98
|
-
@controllers&.first || raise(StandardError, "No controllers have been specified")
|
99
|
-
end
|
100
|
-
|
101
|
-
# A complete list of Stimulus controllers for this component
|
102
|
-
def controller_list(controllers_to_set)
|
103
|
-
controllers_to_set&.map { |c| stimulize_path(c) }&.join(" ")
|
104
|
-
end
|
105
|
-
|
106
|
-
# Complete list of actions ready to be use in the data-action attribute
|
107
|
-
def action_list(actions_to_parse)
|
108
|
-
return nil unless actions_to_parse&.size&.positive?
|
109
|
-
parse_actions(actions_to_parse).join(" ")
|
110
|
-
end
|
111
|
-
|
112
|
-
# Complete list of targets ready to be use in the data attributes
|
113
|
-
def target_list
|
114
|
-
return {} unless @targets&.size&.positive?
|
115
|
-
build_target_data_attributes(parse_targets(@targets))
|
116
|
-
end
|
117
|
-
|
118
|
-
def named_classes_list
|
119
|
-
return {} unless @named_classes&.size&.positive?
|
120
|
-
build_named_classes_data_attributes(@named_classes)
|
121
|
-
end
|
122
|
-
|
123
|
-
# stimulus "data-*" attributes map for this component
|
124
|
-
def tag_data_attributes
|
125
|
-
{controller: controller_list(@controllers), action: action_list(@actions)}
|
126
|
-
.merge!(target_list)
|
127
|
-
.merge!(named_classes_list)
|
128
|
-
.merge!(data_map_attributes)
|
129
|
-
.compact_blank!
|
130
|
-
end
|
131
|
-
|
132
|
-
# Actions can be specified as a symbol, in which case they imply an action on the primary
|
133
|
-
# controller, or as a string in which case it implies an action that is already fully qualified
|
134
|
-
# stimulus action.
|
135
|
-
# 1 Symbol: :my_action => "my_controller#myAction"
|
136
|
-
# 1 String: "my_controller#myAction"
|
137
|
-
# 2 Symbols: [:click, :my_action] => "click->my_controller#myAction"
|
138
|
-
# 1 String, 1 Symbol: ["path/to/controller", :my_action] => "path--to--controller#myAction"
|
139
|
-
# 1 Symbol, 1 String, 1 Symbol: [:hover, "path/to/controller", :my_action] => "hover->path--to--controller#myAction"
|
140
|
-
|
141
|
-
def parse_action_arg(part1)
|
142
|
-
if part1.is_a?(Symbol)
|
143
|
-
# 1 symbol arg, name of method on this controller
|
144
|
-
"#{implied_controller_name}##{js_name(part1)}"
|
145
|
-
elsif part1.is_a?(String)
|
146
|
-
# 1 string arg, fully qualified action
|
147
|
-
part1
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
def parse_multiple_action_args(part1, part2, part3)
|
152
|
-
if part3.nil? && part1.is_a?(Symbol)
|
153
|
-
# 2 symbol args = event + action
|
154
|
-
"#{part1}->#{implied_controller_name}##{js_name(part2)}"
|
155
|
-
elsif part3.nil?
|
156
|
-
# 1 string arg, 1 symbol = controller + action
|
157
|
-
"#{stimulize_path(part1)}##{js_name(part2)}"
|
158
|
-
else
|
159
|
-
# 1 symbol, 1 string, 1 symbol = as above but with event
|
160
|
-
"#{part1}->#{stimulize_path(part2)}##{js_name(part3)}"
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
# Parse actions, targets and attributes that are passed in as symbols or strings
|
165
|
-
|
166
|
-
def parse_targets(targets)
|
167
|
-
targets.map { |n| parse_target(n) }
|
168
|
-
end
|
169
|
-
|
170
|
-
def parse_target(raw_target)
|
171
|
-
return raw_target if raw_target.is_a?(String)
|
172
|
-
return raw_target if raw_target.is_a?(Hash)
|
173
|
-
target(raw_target)
|
174
|
-
end
|
175
|
-
|
176
|
-
def build_target_data_attributes(targets)
|
177
|
-
targets.map { |t| ["#{t[:controller]}-target".to_sym, t[:name]] }.to_h
|
178
|
-
end
|
179
|
-
|
180
|
-
def parse_actions(actions)
|
181
|
-
actions.map! { |a| a.is_a?(String) ? a : action(*a) }
|
182
|
-
end
|
183
|
-
|
184
|
-
def parse_attributes(attrs, controller = nil)
|
185
|
-
attrs.transform_keys { |k| "#{controller || implied_controller_name}-#{k}" }
|
186
|
-
end
|
187
|
-
|
188
|
-
def data_map_attributes
|
189
|
-
return {} unless @data_maps
|
190
|
-
@data_maps.each_with_object({}) do |m, obj|
|
191
|
-
if m.is_a?(Hash)
|
192
|
-
obj.merge!(parse_attributes(m))
|
193
|
-
elsif m.is_a?(Array)
|
194
|
-
controller_path = m.first
|
195
|
-
data = m.last
|
196
|
-
obj.merge!(parse_attributes(data, stimulize_path(controller_path)))
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
def parse_named_classes_hash(named_classes)
|
202
|
-
named_classes.map do |name, classes|
|
203
|
-
logical_name = name.to_s.dasherize
|
204
|
-
classes_str = convert_classes_list_to_string(classes)
|
205
|
-
if classes.is_a?(Hash)
|
206
|
-
{controller: stimulize_path(classes[:controller_path]), name: logical_name, classes: classes_str}
|
207
|
-
else
|
208
|
-
{controller: implied_controller_name, name: logical_name, classes: classes_str}
|
209
|
-
end
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
def build_named_classes_data_attributes(named_classes)
|
214
|
-
parse_named_classes_hash(named_classes)
|
215
|
-
.map { |c| ["#{c[:controller]}-#{c[:name]}-class", c[:classes]] }
|
216
|
-
.to_h
|
217
|
-
end
|
218
|
-
|
219
|
-
def convert_classes_list_to_string(classes)
|
220
|
-
return "" if classes.nil?
|
221
|
-
return classes if classes.is_a?(String)
|
222
|
-
return classes.join(" ") if classes.is_a?(Array)
|
223
|
-
classes[:classes].is_a?(Array) ? classes[:classes].join(" ") : classes[:classes]
|
224
|
-
end
|
225
|
-
|
226
|
-
# Convert a file path to a stimulus controller name
|
227
|
-
def stimulize_path(path)
|
228
|
-
path.split("/").map { |p| p.to_s.dasherize }.join("--")
|
229
|
-
end
|
230
|
-
|
231
|
-
# Convert a Ruby 'snake case' string to a JavaScript camel case strings
|
232
|
-
def js_name(name)
|
233
|
-
name.to_s.camelize(:lower)
|
234
|
-
end
|
235
|
-
end
|
236
|
-
end
|
237
|
-
end
|