vident 0.1.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 +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
|