vident 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.standard.yml +3 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +29 -0
- data/LICENSE.txt +21 -0
- data/README.md +349 -0
- data/Rakefile +14 -0
- data/examples/ex1.gif +0 -0
- data/lib/tasks/vident.rake +37 -0
- data/lib/vident/attributes/not_typed.rb +78 -0
- data/lib/vident/attributes/typed.rb +180 -0
- data/lib/vident/attributes/typed_niling_struct.rb +27 -0
- data/lib/vident/attributes/types.rb +16 -0
- data/lib/vident/base.rb +286 -0
- data/lib/vident/component.rb +32 -0
- data/lib/vident/railtie.rb +10 -0
- data/lib/vident/root_component/base.rb +239 -0
- data/lib/vident/root_component/using_phlex_html.rb +44 -0
- data/lib/vident/root_component/using_view_component.rb +44 -0
- data/lib/vident/stable_id.rb +24 -0
- data/lib/vident/typed_component.rb +48 -0
- data/lib/vident/version.rb +5 -0
- data/lib/vident.rb +33 -0
- data/sig/vident.rbs +4 -0
- metadata +85 -0
@@ -0,0 +1,180 @@
|
|
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
|
+
def prepare_attributes(attributes)
|
17
|
+
@__attributes = self.class.schema.new(**attributes)
|
18
|
+
end
|
19
|
+
|
20
|
+
def attributes
|
21
|
+
@__attributes.attributes
|
22
|
+
end
|
23
|
+
|
24
|
+
def attribute_names
|
25
|
+
@attribute_names ||= self.class.attribute_names
|
26
|
+
end
|
27
|
+
|
28
|
+
def attribute(key)
|
29
|
+
if Rails.env.development? && !key?(key)
|
30
|
+
raise StandardError, "Attribute #{key} not found in #{self.class.name}"
|
31
|
+
end
|
32
|
+
@__attributes.attributes[key]
|
33
|
+
end
|
34
|
+
alias_method :[], :attribute
|
35
|
+
|
36
|
+
def key?(key)
|
37
|
+
self.class.schema.attribute_names.include?(key)
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_hash
|
41
|
+
@__attributes.to_h
|
42
|
+
end
|
43
|
+
|
44
|
+
class_methods do
|
45
|
+
def inherited(subclass)
|
46
|
+
subclass.instance_variable_set(:@schema, @schema.clone)
|
47
|
+
subclass.instance_variable_set(:@attribute_ivar_names, @attribute_ivar_names.clone)
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
def attribute_names
|
52
|
+
schema.attribute_names
|
53
|
+
end
|
54
|
+
|
55
|
+
def attribute_metadata(key)
|
56
|
+
schema.schema.key(key).meta
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :schema, :attribute_ivar_names
|
60
|
+
|
61
|
+
def attribute(name, type = :any, **options)
|
62
|
+
type_info = map_primitive_to_dry_type(type, !options[:convert])
|
63
|
+
type_info = set_constraints(type_info, type, options)
|
64
|
+
define_on_schema(name, type_info, options)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def set_constraints(type_info, specified_type, options)
|
70
|
+
member_klass = options[:type] || options[:sub_type]
|
71
|
+
if member_klass && type_info.respond_to?(:of)
|
72
|
+
# Sub types of collections currently can be nil - this should be an option
|
73
|
+
type_info = type_info.of(
|
74
|
+
map_primitive_to_dry_type(member_klass, !options[:convert]).optional.meta(required: false)
|
75
|
+
)
|
76
|
+
end
|
77
|
+
type_info = type_info.optional.meta(required: false) if allows_nil?(options)
|
78
|
+
type_info = type_info.constrained(filled: true) unless allows_blank?(options)
|
79
|
+
if options[:default]&.is_a?(Proc)
|
80
|
+
type_info = type_info.default(options[:default].freeze)
|
81
|
+
elsif !options[:default].nil?
|
82
|
+
type_info = type_info.default(->(_) { options[:default] }.freeze)
|
83
|
+
end
|
84
|
+
type_info = type_info.constrained(included_in: options[:in].freeze) if options[:in]
|
85
|
+
|
86
|
+
# Store adapter type info in the schema for use by typed form
|
87
|
+
metadata = {typed_attribute_type: specified_type, typed_attribute_options: options}
|
88
|
+
type_info.meta(**metadata)
|
89
|
+
end
|
90
|
+
|
91
|
+
def delegates?(options)
|
92
|
+
options[:delegates] != false
|
93
|
+
end
|
94
|
+
|
95
|
+
def define_on_schema(name, type_info, options)
|
96
|
+
@attribute_ivar_names ||= {}
|
97
|
+
@attribute_ivar_names[name] = :"@#{name}"
|
98
|
+
define_attribute_delegate(name) if delegates?(options)
|
99
|
+
@schema ||= Class.new(Vident::Attributes::TypedNilingStruct)
|
100
|
+
@schema.attribute name, type_info
|
101
|
+
end
|
102
|
+
|
103
|
+
def define_attribute_delegate(attr_name)
|
104
|
+
# Define reader & presence check method
|
105
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
106
|
+
def #{attr_name}
|
107
|
+
@__attributes.attributes[:#{attr_name}]
|
108
|
+
end
|
109
|
+
|
110
|
+
def #{attr_name}?
|
111
|
+
send(:#{attr_name}).present?
|
112
|
+
end
|
113
|
+
RUBY
|
114
|
+
end
|
115
|
+
|
116
|
+
def allows_nil?(options)
|
117
|
+
return true unless options
|
118
|
+
allow_nil = options[:allow_nil]
|
119
|
+
return false if allow_nil == false
|
120
|
+
allow_nil || allows_blank?(options)
|
121
|
+
end
|
122
|
+
|
123
|
+
def allows_blank?(options)
|
124
|
+
return true unless options
|
125
|
+
allow_blank = options[:allow_blank]
|
126
|
+
allow_blank.nil? ? true : allow_blank
|
127
|
+
end
|
128
|
+
|
129
|
+
def map_primitive_to_dry_type(type, strict)
|
130
|
+
if type == :any
|
131
|
+
Types::Nominal::Any
|
132
|
+
elsif type == Integer
|
133
|
+
strict ? Types::Strict::Integer : Types::Params::Integer
|
134
|
+
elsif type == BigDecimal
|
135
|
+
strict ? Types::Strict::Decimal : Types::Params::Decimal
|
136
|
+
elsif type == Float
|
137
|
+
strict ? Types::Strict::Float : Types::Params::Float
|
138
|
+
elsif type == Numeric
|
139
|
+
if strict
|
140
|
+
Types::Strict::Float | Types::Strict::Integer | Types::Strict::Decimal
|
141
|
+
else
|
142
|
+
Types::Params::Float | Types::Params::Integer | Types::Params::Decimal
|
143
|
+
end
|
144
|
+
elsif type == Symbol
|
145
|
+
strict ? Types::Strict::Symbol : Types::Coercible::Symbol
|
146
|
+
elsif type == String
|
147
|
+
strict ? Types::Strict::String : Types::Coercible::String
|
148
|
+
elsif type == Time
|
149
|
+
strict ? Types::Strict::Time : Types::Params::Time
|
150
|
+
elsif type == Date
|
151
|
+
strict ? Types::Strict::Date : Types::Params::Date
|
152
|
+
elsif type == Array
|
153
|
+
strict ? Types::Strict::Array : Types::Params::Array
|
154
|
+
elsif type == Hash
|
155
|
+
strict ? Types::Strict::Hash : Types::Coercible::Hash
|
156
|
+
elsif type == :boolean
|
157
|
+
strict ? Types::Strict::Bool : Types::Params::Bool
|
158
|
+
elsif strict
|
159
|
+
# when strict create a Nominal type with a is_a? constraint, otherwise create a Nominal type which constructs
|
160
|
+
# values using the default constructor, `new`.
|
161
|
+
Types.Instance(type)
|
162
|
+
else
|
163
|
+
Types.Constructor(type) { |values| type.new(**values) }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
else
|
171
|
+
module Vident
|
172
|
+
module Attributes
|
173
|
+
module Typed
|
174
|
+
def self.included(base)
|
175
|
+
raise "Vident::Attributes::Typed requires dry-struct to be installed"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,27 @@
|
|
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
|
@@ -0,0 +1,16 @@
|
|
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
|
data/lib/vident/base.rb
ADDED
@@ -0,0 +1,286 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vident
|
4
|
+
module Base
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
def no_stimulus_controller
|
9
|
+
@no_stimulus_controller = true
|
10
|
+
end
|
11
|
+
|
12
|
+
def stimulus_controller?
|
13
|
+
!@no_stimulus_controller
|
14
|
+
end
|
15
|
+
|
16
|
+
# The "name" of the component from its class name and namespace. This is used to generate a HTML class name
|
17
|
+
# that can helps identify the component type in the DOM or for styling purposes.
|
18
|
+
def component_name
|
19
|
+
@component_name ||= stimulus_identifier
|
20
|
+
end
|
21
|
+
|
22
|
+
def slots?
|
23
|
+
registered_slots.present?
|
24
|
+
end
|
25
|
+
|
26
|
+
# TODO: move stuff related to cache key to a module
|
27
|
+
|
28
|
+
# TypedComponents can be used with fragment caching, but you need to be careful! Read on...
|
29
|
+
#
|
30
|
+
# <% cache component do %>
|
31
|
+
# <%= render component %>
|
32
|
+
# <% end %>
|
33
|
+
#
|
34
|
+
# The most important point is that Rails cannot track dependencies on the component itself, so you need to
|
35
|
+
# be careful to be explicit on the attributes, and manually specify any sub Viewcomponent dependencies that the
|
36
|
+
# component has. The assumption is that the subcomponent takes any attributes from the parent, so the cache key
|
37
|
+
# depends on the parent component attributes. Otherwise changes to the parent or sub component views/Ruby class
|
38
|
+
# will result in different cache keys too. Of course if you invalidate all cache keys with a modifier on deploy
|
39
|
+
# then no need to worry about changing the cache key on component changes, only on attribute/data changes.
|
40
|
+
#
|
41
|
+
# A big caveat is that the cache key cannot depend on anything related to the view_context of the component (such
|
42
|
+
# as `helpers` as the key is created before the rending pipline is invoked (which is when the view_context is set).
|
43
|
+
def depends_on(*klasses)
|
44
|
+
@component_dependencies ||= []
|
45
|
+
@component_dependencies += klasses
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :component_dependencies
|
49
|
+
|
50
|
+
def with_cache_key(*attrs, name: :_collection)
|
51
|
+
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 slots?
|
52
|
+
# Add view file to cache key
|
53
|
+
attrs << :component_modified_time
|
54
|
+
super
|
55
|
+
end
|
56
|
+
|
57
|
+
def component_modified_time
|
58
|
+
return @component_modified_time if Rails.env.production? && @component_modified_time
|
59
|
+
# FIXME: This could stack overflow if there are circular dependencies
|
60
|
+
deps = component_dependencies&.map(&:component_modified_time)&.join("-") || ""
|
61
|
+
@component_modified_time = deps + sidecar_view_modified_time + rb_component_modified_time
|
62
|
+
end
|
63
|
+
|
64
|
+
def sidecar_view_modified_time
|
65
|
+
return @sidecar_view_modified_time if Rails.env.production? && defined?(@sidecar_view_modified_time)
|
66
|
+
@sidecar_view_modified_time = ::File.exist?(template_path) ? ::File.mtime(template_path).to_i.to_s : ""
|
67
|
+
end
|
68
|
+
|
69
|
+
def rb_component_modified_time
|
70
|
+
return @rb_component_modified_time if Rails.env.production? && defined?(@rb_component_modified_time)
|
71
|
+
@rb_component_modified_time = ::File.exist?(component_path) ? ::File.mtime(component_path).to_i.to_s : ""
|
72
|
+
end
|
73
|
+
|
74
|
+
def template_path
|
75
|
+
File.join components_base_path, "#{virtual_path}.html.erb"
|
76
|
+
end
|
77
|
+
|
78
|
+
def component_path
|
79
|
+
File.join components_base_path, "#{virtual_path}.rb"
|
80
|
+
end
|
81
|
+
|
82
|
+
def components_base_path
|
83
|
+
::Rails.configuration.view_component.view_component_path || "app/components"
|
84
|
+
end
|
85
|
+
|
86
|
+
# Dont check collection params, we use kwargs
|
87
|
+
def validate_collection_parameter!(validate_default: false)
|
88
|
+
end
|
89
|
+
|
90
|
+
# stimulus controller identifier
|
91
|
+
def stimulus_identifier
|
92
|
+
stimulus_identifier_from_path(identifier_name_path)
|
93
|
+
end
|
94
|
+
|
95
|
+
def identifier_name_path
|
96
|
+
if ancestors.include?(Phlex::HTML)
|
97
|
+
name.remove("Views::").underscore
|
98
|
+
else
|
99
|
+
name.underscore
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def stimulus_identifier_from_path(path)
|
104
|
+
path.split("/").map { |p| p.to_s.dasherize }.join("--")
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
# Define reader & presence check method, for performance use ivar directly
|
110
|
+
def define_attribute_delegate(attr_name)
|
111
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
112
|
+
def #{attr_name}
|
113
|
+
#{@attribute_ivar_names[attr_name]}
|
114
|
+
end
|
115
|
+
|
116
|
+
def #{attr_name}?
|
117
|
+
#{@attribute_ivar_names[attr_name]}.present?
|
118
|
+
end
|
119
|
+
RUBY
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def prepare_attributes(attributes)
|
124
|
+
raise NotImplementedError
|
125
|
+
end
|
126
|
+
|
127
|
+
# Override this method to perform any initialisation before attributes are set
|
128
|
+
def before_initialise(_attrs)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Override this method to perform any initialisation after attributes are set
|
132
|
+
def after_initialise
|
133
|
+
end
|
134
|
+
|
135
|
+
def clone(overrides = {})
|
136
|
+
new_set = to_hash.merge(**overrides)
|
137
|
+
self.class.new(**new_set)
|
138
|
+
end
|
139
|
+
|
140
|
+
def inspect(klass_name = "Component")
|
141
|
+
attr_text = attributes.map { |k, v| "#{k}=#{v.inspect}" }.join(", ")
|
142
|
+
"#<#{self.class.name}<Vident::#{klass_name}> #{attr_text}>"
|
143
|
+
end
|
144
|
+
|
145
|
+
# Generate a unique ID for a component, can be overridden as required. Makes it easier to setup things like ARIA
|
146
|
+
# attributes which require elements to reference by ID. Note this overrides the `id` accessor
|
147
|
+
def id
|
148
|
+
@id.presence || random_id
|
149
|
+
end
|
150
|
+
|
151
|
+
# Methods to use in component views
|
152
|
+
# ---------------------------------
|
153
|
+
|
154
|
+
delegate :params, to: :helpers
|
155
|
+
|
156
|
+
# HTML and attribute definition and creation
|
157
|
+
|
158
|
+
# Helper to create the main element
|
159
|
+
def parent_element(**options)
|
160
|
+
@parent_element ||= begin
|
161
|
+
# Note: we cant mix phlex and view_component render contexts
|
162
|
+
klass = if self.class.ancestors.include?(Phlex::HTML)
|
163
|
+
RootComponent::UsingPhlexHTML
|
164
|
+
else
|
165
|
+
RootComponent::UsingViewComponent
|
166
|
+
end
|
167
|
+
klass.new(**stimulus_options_for_component(options))
|
168
|
+
end
|
169
|
+
end
|
170
|
+
alias_method :root, :parent_element
|
171
|
+
|
172
|
+
# FIXME: if we call them before `root` we will setup the root element before we intended
|
173
|
+
# The separation between component and root element is a bit messy. Might need rethinking.
|
174
|
+
delegate :action, :target, :named_classes, to: :root
|
175
|
+
|
176
|
+
# This can be overridden to return an array of extra class names
|
177
|
+
def element_classes
|
178
|
+
end
|
179
|
+
|
180
|
+
# A HTML class name that can helps identify the component type in the DOM or for styling purposes.
|
181
|
+
def component_class_name
|
182
|
+
self.class.component_name
|
183
|
+
end
|
184
|
+
alias_method :js_event_name_prefix, :component_class_name
|
185
|
+
|
186
|
+
# Generates the full list of HTML classes for the component
|
187
|
+
def render_classes(erb_defined_classes = nil)
|
188
|
+
# TODO: avoid pointless creation of arrays
|
189
|
+
base_classes = [component_class_name] + Array.wrap(element_classes)
|
190
|
+
base_classes += Array.wrap(erb_defined_classes) if erb_defined_classes
|
191
|
+
classes_on_component = attribute(:html_options)&.fetch(:class, nil)
|
192
|
+
base_classes += Array.wrap(classes_on_component) if classes_on_component
|
193
|
+
produce_style_classes(base_classes)
|
194
|
+
end
|
195
|
+
|
196
|
+
def stimulus_identifier
|
197
|
+
self.class.stimulus_identifier
|
198
|
+
end
|
199
|
+
|
200
|
+
# TODO: Move to caching module
|
201
|
+
# Component modified time which is combined with other cache key attributes to generate cache key for an instance
|
202
|
+
def component_modified_time
|
203
|
+
self.class.component_modified_time
|
204
|
+
end
|
205
|
+
|
206
|
+
# The `component` class name is used to create the controller name.
|
207
|
+
# The path of the Stimulus controller when none is explicitly set
|
208
|
+
def default_controller_path
|
209
|
+
self.class.identifier_name_path
|
210
|
+
end
|
211
|
+
|
212
|
+
protected
|
213
|
+
|
214
|
+
# Prepare the stimulus attributes for a StimulusComponent
|
215
|
+
def stimulus_options_for_component(options)
|
216
|
+
{
|
217
|
+
**options.except(:id, :element_tag, :html_options, :controller, :controllers, :actions, :targets, :named_classes, :data_maps),
|
218
|
+
id: respond_to?(:id) ? id : (attribute(:id) || options[:id]),
|
219
|
+
element_tag: attribute(:element_tag) || options[:element_tag] || :div,
|
220
|
+
html_options: prepare_html_options(options[:html_options]),
|
221
|
+
controllers: (
|
222
|
+
self.class.stimulus_controller? ? [default_controller_path] : []
|
223
|
+
) + Array.wrap(options[:controllers]) + attribute(:controllers),
|
224
|
+
actions: attribute(:actions) + Array.wrap(options[:actions]),
|
225
|
+
targets: attribute(:targets) + Array.wrap(options[:targets]),
|
226
|
+
named_classes: merge_stimulus_option(options, :named_classes),
|
227
|
+
data_maps: prepare_stimulus_option(options, :data_maps)
|
228
|
+
}
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
|
233
|
+
def prepare_html_options(erb_options)
|
234
|
+
# Options should override in this order:
|
235
|
+
# - defined on component class methods (lowest priority)
|
236
|
+
# - defined by passing to component erb
|
237
|
+
# - defined by passing to component constructor (highest priority)
|
238
|
+
options = erb_options&.except(:class) || {}
|
239
|
+
classes_from_view = Array.wrap(erb_options[:class]) if erb_options&.key?(:class)
|
240
|
+
options[:class] = render_classes(classes_from_view)
|
241
|
+
options.merge!(attribute(:html_options).except(:class)) if attribute(:html_options)
|
242
|
+
options
|
243
|
+
end
|
244
|
+
|
245
|
+
# TODO: deprecate the ability to set via method on class (responds_to?) and just use component attributes
|
246
|
+
# or attributes passed to parent_element
|
247
|
+
def prepare_stimulus_option(options, name)
|
248
|
+
resolved = respond_to?(name) ? Array.wrap(send(name)) : []
|
249
|
+
resolved.concat(Array.wrap(attribute(name)))
|
250
|
+
resolved.concat(Array.wrap(options[name]))
|
251
|
+
resolved
|
252
|
+
end
|
253
|
+
|
254
|
+
def merge_stimulus_option(options, name)
|
255
|
+
(attribute(name) || {}).merge(options[name] || {})
|
256
|
+
end
|
257
|
+
|
258
|
+
def produce_style_classes(class_names)
|
259
|
+
dedupe_view_component_classes(class_names)
|
260
|
+
end
|
261
|
+
|
262
|
+
def template_path
|
263
|
+
self.class.template_path
|
264
|
+
end
|
265
|
+
|
266
|
+
def random_id
|
267
|
+
@random_id ||= "#{self.class.component_name}-#{StableId.next_id_in_sequence}"
|
268
|
+
end
|
269
|
+
|
270
|
+
CLASSNAME_SEPARATOR = " "
|
271
|
+
|
272
|
+
# Join all the various class definisions possible and dedupe
|
273
|
+
def dedupe_view_component_classes(html_classes)
|
274
|
+
html_classes.reject!(&:blank?)
|
275
|
+
|
276
|
+
# Join, then dedupe.
|
277
|
+
# This ensures that entries from the classes array such as "a b", "a", "b" are correctly deduped.
|
278
|
+
# Note we are trying to do this with less allocations to avoid GC churn
|
279
|
+
# classes = classes.join(" ").split(" ").uniq
|
280
|
+
html_classes.map! { |x| x.include?(CLASSNAME_SEPARATOR) ? x.split(CLASSNAME_SEPARATOR) : x }
|
281
|
+
.flatten!
|
282
|
+
html_classes.uniq!
|
283
|
+
html_classes.present? ? html_classes.join(CLASSNAME_SEPARATOR) : nil
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./attributes/not_typed"
|
4
|
+
|
5
|
+
module Vident
|
6
|
+
module Component
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
include Vident::Base
|
11
|
+
include Vident::Attributes::NotTyped
|
12
|
+
|
13
|
+
attribute :id, delegates: false
|
14
|
+
attribute :html_options, delegates: false
|
15
|
+
attribute :element_tag, delegates: false
|
16
|
+
|
17
|
+
# StimulusJS support
|
18
|
+
attribute :controllers, default: [], delegates: false
|
19
|
+
attribute :actions, default: [], delegates: false
|
20
|
+
attribute :targets, default: [], delegates: false
|
21
|
+
attribute :data_maps, default: [], delegates: false
|
22
|
+
attribute :named_classes, delegates: false
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(attrs = {})
|
26
|
+
before_initialise(attrs)
|
27
|
+
prepare_attributes(attrs)
|
28
|
+
after_initialise
|
29
|
+
super()
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|