vident 0.6.3 → 0.8.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/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
|