view_component 3.12.1 → 3.23.2
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/app/controllers/concerns/view_component/preview_actions.rb +8 -1
- data/app/helpers/preview_helper.rb +1 -1
- data/app/views/view_components/_preview_source.html.erb +1 -1
- data/docs/CHANGELOG.md +299 -5
- data/lib/rails/generators/abstract_generator.rb +9 -1
- data/lib/rails/generators/component/component_generator.rb +2 -1
- data/lib/rails/generators/component/templates/component.rb.tt +3 -2
- data/lib/rails/generators/erb/component_generator.rb +1 -1
- data/lib/rails/generators/preview/templates/component_preview.rb.tt +2 -0
- data/lib/rails/generators/rspec/templates/component_spec.rb.tt +1 -1
- data/lib/rails/generators/stimulus/component_generator.rb +8 -3
- data/lib/rails/generators/stimulus/templates/component_controller.ts.tt +9 -0
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +1 -1
- data/lib/view_component/base.rb +52 -59
- data/lib/view_component/collection.rb +18 -3
- data/lib/view_component/compiler.rb +164 -240
- data/lib/view_component/config.rb +26 -2
- data/lib/view_component/configurable.rb +17 -0
- data/lib/view_component/engine.rb +21 -11
- data/lib/view_component/errors.rb +7 -5
- data/lib/view_component/instrumentation.rb +1 -1
- data/lib/view_component/preview.rb +1 -1
- data/lib/view_component/rails/tasks/view_component.rake +8 -2
- data/lib/view_component/slotable.rb +28 -14
- data/lib/view_component/slotable_default.rb +20 -0
- data/lib/view_component/template.rb +134 -0
- data/lib/view_component/test_helpers.rb +29 -2
- data/lib/view_component/use_helpers.rb +32 -10
- data/lib/view_component/version.rb +2 -2
- metadata +112 -19
- data/lib/rails/generators/component/USAGE +0 -13
- data/lib/view_component/docs_builder_component.html.erb +0 -22
- data/lib/view_component/docs_builder_component.rb +0 -96
- data/lib/yard/mattr_accessor_handler.rb +0 -19
@@ -4,302 +4,226 @@ require "concurrent-ruby"
|
|
4
4
|
|
5
5
|
module ViewComponent
|
6
6
|
class Compiler
|
7
|
-
# Compiler mode. Can be either:
|
8
|
-
# *
|
7
|
+
# Compiler development mode. Can be either:
|
8
|
+
# * true (a blocking mode which ensures thread safety when redefining the `call` method for components,
|
9
9
|
# default in Rails development and test mode)
|
10
|
-
# *
|
11
|
-
|
12
|
-
PRODUCTION_MODE = :production
|
10
|
+
# * false(a non-blocking mode, default in Rails production mode)
|
11
|
+
class_attribute :development_mode, default: false
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
@component_class = component_class
|
18
|
-
@redefinition_lock = Mutex.new
|
19
|
-
@variants_rendering_templates = Set.new
|
13
|
+
def initialize(component)
|
14
|
+
@component = component
|
15
|
+
@lock = Mutex.new
|
20
16
|
end
|
21
17
|
|
22
18
|
def compiled?
|
23
|
-
CompileCache.compiled?(
|
24
|
-
end
|
25
|
-
|
26
|
-
def development?
|
27
|
-
self.class.mode == DEVELOPMENT_MODE
|
19
|
+
CompileCache.compiled?(@component)
|
28
20
|
end
|
29
21
|
|
30
22
|
def compile(raise_errors: false, force: false)
|
31
23
|
return if compiled? && !force
|
32
|
-
return if
|
33
|
-
|
34
|
-
component_class.superclass.compile(raise_errors: raise_errors) if should_compile_superclass?
|
35
|
-
|
36
|
-
if template_errors.present?
|
37
|
-
raise TemplateError.new(template_errors) if raise_errors
|
24
|
+
return if @component == ViewComponent::Base
|
38
25
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
component_class.validate_initialization_parameters!
|
44
|
-
component_class.validate_collection_parameter!
|
45
|
-
end
|
26
|
+
@lock.synchronize do
|
27
|
+
# this check is duplicated so that concurrent compile calls can still
|
28
|
+
# early exit
|
29
|
+
return if compiled? && !force
|
46
30
|
|
47
|
-
|
48
|
-
template = component_class.inline_template
|
31
|
+
gather_templates
|
49
32
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
component_class.class_eval <<-RUBY, template.path, template.lineno
|
54
|
-
def call
|
55
|
-
#{compiled_inline_template(template)}
|
56
|
-
end
|
57
|
-
RUBY
|
58
|
-
# rubocop:enable Style/EvalWithLocation
|
33
|
+
if self.class.development_mode && @templates.any?(&:requires_compiled_superclass?)
|
34
|
+
@component.superclass.compile(raise_errors: raise_errors)
|
35
|
+
end
|
59
36
|
|
60
|
-
|
37
|
+
if template_errors.present?
|
38
|
+
raise TemplateError.new(template_errors) if raise_errors
|
61
39
|
|
62
|
-
|
63
|
-
|
64
|
-
def render_template_for(variant = nil)
|
65
|
-
_call_#{safe_class_name}
|
66
|
-
end
|
67
|
-
RUBY
|
40
|
+
# this return is load bearing, and prevents the component from being considered "compiled?"
|
41
|
+
return false
|
68
42
|
end
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
@
|
73
|
-
|
74
|
-
redefinition_lock.synchronize do
|
75
|
-
component_class.silence_redefinition_of_method(method_name)
|
76
|
-
# rubocop:disable Style/EvalWithLocation
|
77
|
-
component_class.class_eval <<-RUBY, template[:path], 0
|
78
|
-
def #{method_name}
|
79
|
-
#{compiled_template(template[:path])}
|
80
|
-
end
|
81
|
-
RUBY
|
82
|
-
# rubocop:enable Style/EvalWithLocation
|
83
|
-
end
|
43
|
+
|
44
|
+
if raise_errors
|
45
|
+
@component.validate_initialization_parameters!
|
46
|
+
@component.validate_collection_parameter!
|
84
47
|
end
|
85
48
|
|
86
49
|
define_render_template_for
|
87
|
-
end
|
88
50
|
|
89
|
-
|
51
|
+
@component.register_default_slots
|
52
|
+
@component.build_i18n_backend
|
90
53
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
def renders_template_for_variant?(variant)
|
95
|
-
@variants_rendering_templates.include?(variant)
|
54
|
+
CompileCache.register(@component)
|
55
|
+
end
|
96
56
|
end
|
97
57
|
|
98
58
|
private
|
99
59
|
|
100
|
-
attr_reader :
|
60
|
+
attr_reader :templates
|
101
61
|
|
102
62
|
def define_render_template_for
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
"elsif variant.to_sym == :'#{variant}'\n #{safe_name}"
|
108
|
-
end.join("\n")
|
109
|
-
|
110
|
-
component_class.define_method(:"_call_#{safe_class_name}", component_class.instance_method(:call))
|
63
|
+
@templates.each do |template|
|
64
|
+
template.compile_to_component
|
65
|
+
end
|
111
66
|
|
112
|
-
|
113
|
-
if
|
114
|
-
|
115
|
-
|
67
|
+
method_body =
|
68
|
+
if @templates.one?
|
69
|
+
@templates.first.safe_method_name_call
|
70
|
+
elsif (template = @templates.find(&:inline?))
|
71
|
+
template.safe_method_name_call
|
116
72
|
else
|
117
|
-
|
118
|
-
|
119
|
-
|
73
|
+
branches = []
|
74
|
+
|
75
|
+
@templates.each do |template|
|
76
|
+
conditional =
|
77
|
+
if template.inline_call?
|
78
|
+
"variant&.to_sym == #{template.variant.inspect}"
|
79
|
+
else
|
80
|
+
[
|
81
|
+
template.default_format? ? "(format == #{ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT.inspect} || format.nil?)" : "format == #{template.format.inspect}",
|
82
|
+
template.variant.nil? ? "variant.nil?" : "variant&.to_sym == #{template.variant.inspect}"
|
83
|
+
].join(" && ")
|
84
|
+
end
|
85
|
+
|
86
|
+
branches << [conditional, template.safe_method_name_call]
|
87
|
+
end
|
120
88
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
#{body}
|
89
|
+
out = branches.each_with_object(+"") do |(conditional, branch_body), memo|
|
90
|
+
memo << "#{(!memo.present?) ? "if" : "elsif"} #{conditional}\n #{branch_body}\n"
|
91
|
+
end
|
92
|
+
out << "else\n #{templates.find { _1.variant.nil? && _1.default_format? }.safe_method_name_call}\nend"
|
126
93
|
end
|
127
|
-
RUBY
|
128
|
-
end
|
129
|
-
end
|
130
94
|
|
131
|
-
|
132
|
-
|
95
|
+
@component.silence_redefinition_of_method(:render_template_for)
|
96
|
+
@component.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
97
|
+
def render_template_for(variant = nil, format = nil)
|
98
|
+
#{method_body}
|
99
|
+
end
|
100
|
+
RUBY
|
133
101
|
end
|
134
102
|
|
135
103
|
def template_errors
|
136
|
-
@
|
137
|
-
|
138
|
-
errors = []
|
104
|
+
@_template_errors ||= begin
|
105
|
+
errors = []
|
139
106
|
|
140
|
-
|
141
|
-
errors << "Couldn't find a template file or inline render method for #{component_class}."
|
142
|
-
end
|
143
|
-
|
144
|
-
if templates.count { |template| template[:variant].nil? } > 1
|
145
|
-
errors <<
|
146
|
-
"More than one template found for #{component_class}. " \
|
147
|
-
"There can only be one default template file per component."
|
148
|
-
end
|
149
|
-
|
150
|
-
invalid_variants =
|
151
|
-
templates
|
152
|
-
.group_by { |template| template[:variant] }
|
153
|
-
.map { |variant, grouped| variant if grouped.length > 1 }
|
154
|
-
.compact
|
155
|
-
.sort
|
156
|
-
|
157
|
-
unless invalid_variants.empty?
|
158
|
-
errors <<
|
159
|
-
"More than one template found for #{"variant".pluralize(invalid_variants.count)} " \
|
160
|
-
"#{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. " \
|
161
|
-
"There can only be one template file per variant."
|
162
|
-
end
|
107
|
+
errors << "Couldn't find a template file or inline render method for #{@component}." if @templates.empty?
|
163
108
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
109
|
+
# We currently allow components to have both an inline call method and a template for a variant, with the
|
110
|
+
# inline call method overriding the template. We should aim to change this in v4 to instead
|
111
|
+
# raise an error.
|
112
|
+
@templates.reject(&:inline_call?)
|
113
|
+
.map { |template| [template.variant, template.format] }
|
114
|
+
.tally
|
115
|
+
.select { |_, count| count > 1 }
|
116
|
+
.each do |tally|
|
117
|
+
variant, this_format = tally.first
|
172
118
|
|
173
|
-
|
174
|
-
count = duplicate_template_file_and_inline_variant_calls.count
|
119
|
+
variant_string = " for variant `#{variant}`" if variant.present?
|
175
120
|
|
176
|
-
|
177
|
-
|
178
|
-
"found for #{"variant".pluralize(count)} " \
|
179
|
-
"#{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} " \
|
180
|
-
"in #{component_class}. " \
|
181
|
-
"There can only be a template file or inline render method per variant."
|
182
|
-
end
|
121
|
+
errors << "More than one #{this_format.upcase} template found#{variant_string} for #{@component}. "
|
122
|
+
end
|
183
123
|
|
184
|
-
|
185
|
-
|
124
|
+
default_template_types = @templates.each_with_object(Set.new) do |template, memo|
|
125
|
+
next if template.variant
|
186
126
|
|
187
|
-
|
188
|
-
|
189
|
-
end
|
127
|
+
memo << :template_file if !template.inline_call?
|
128
|
+
memo << :inline_render if template.inline_call? && template.defined_on_self?
|
190
129
|
|
191
|
-
|
192
|
-
errors <<
|
193
|
-
"Colliding templates #{colliding_variants.sort.map { |v| "'#{v}'" }.to_sentence} " \
|
194
|
-
"found in #{component_class}."
|
195
|
-
end
|
196
|
-
|
197
|
-
errors
|
130
|
+
memo
|
198
131
|
end
|
199
|
-
end
|
200
|
-
|
201
|
-
def templates
|
202
|
-
@templates ||=
|
203
|
-
begin
|
204
|
-
extensions = ActionView::Template.template_handler_extensions
|
205
132
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
variant: pieces[1..-2].join(".").split("+").second&.to_sym,
|
211
|
-
handler: pieces.last
|
212
|
-
}
|
213
|
-
end
|
133
|
+
if default_template_types.length > 1
|
134
|
+
errors <<
|
135
|
+
"Template file and inline render method found for #{@component}. " \
|
136
|
+
"There can only be a template file or inline render method per component."
|
214
137
|
end
|
215
|
-
end
|
216
|
-
|
217
|
-
def inline_calls
|
218
|
-
@inline_calls ||=
|
219
|
-
begin
|
220
|
-
# Fetch only ViewComponent ancestor classes to limit the scope of
|
221
|
-
# finding inline calls
|
222
|
-
view_component_ancestors =
|
223
|
-
(
|
224
|
-
component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } -
|
225
|
-
component_class.included_modules
|
226
|
-
)
|
227
138
|
|
228
|
-
|
139
|
+
# If a template has inline calls, they can conflict with template files the component may use
|
140
|
+
# to render. This attempts to catch and raise that issue before run time. For example,
|
141
|
+
# `def render_mobile` would conflict with a sidecar template of `component.html+mobile.erb`
|
142
|
+
duplicate_template_file_and_inline_call_variants =
|
143
|
+
@templates.reject(&:inline_call?).map(&:variant) &
|
144
|
+
@templates.select { _1.inline_call? && _1.defined_on_self? }.map(&:variant)
|
145
|
+
|
146
|
+
unless duplicate_template_file_and_inline_call_variants.empty?
|
147
|
+
count = duplicate_template_file_and_inline_call_variants.count
|
148
|
+
|
149
|
+
errors <<
|
150
|
+
"Template #{"file".pluralize(count)} and inline render #{"method".pluralize(count)} " \
|
151
|
+
"found for #{"variant".pluralize(count)} " \
|
152
|
+
"#{duplicate_template_file_and_inline_call_variants.map { |v| "'#{v}'" }.to_sentence} " \
|
153
|
+
"in #{@component}. There can only be a template file or inline render method per variant."
|
229
154
|
end
|
230
|
-
end
|
231
|
-
|
232
|
-
def inline_calls_defined_on_self
|
233
|
-
@inline_calls_defined_on_self ||= component_class.instance_methods(false).grep(/^call(_|$)/)
|
234
|
-
end
|
235
|
-
|
236
|
-
def variants
|
237
|
-
@__vc_variants = (
|
238
|
-
templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
|
239
|
-
).compact.uniq
|
240
|
-
end
|
241
|
-
|
242
|
-
def variants_from_inline_calls(calls)
|
243
|
-
calls.reject { |call| call == :call }.map do |variant_call|
|
244
|
-
variant_call.to_s.sub("call_", "").to_sym
|
245
|
-
end
|
246
|
-
end
|
247
155
|
|
248
|
-
|
249
|
-
|
250
|
-
|
156
|
+
@templates.select(&:variant).each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |template, memo|
|
157
|
+
memo[template.normalized_variant_name] << template.variant
|
158
|
+
memo
|
159
|
+
end.each do |_, variant_names|
|
160
|
+
next unless variant_names.length > 1
|
251
161
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
def compiled_template(file_path)
|
256
|
-
handler = ActionView::Template.handler_for_extension(File.extname(file_path).delete("."))
|
257
|
-
template = File.read(file_path)
|
258
|
-
|
259
|
-
compile_template(template, handler)
|
260
|
-
end
|
162
|
+
errors << "Colliding templates #{variant_names.sort.map { |v| "'#{v}'" }.to_sentence} found in #{@component}."
|
163
|
+
end
|
261
164
|
|
262
|
-
|
263
|
-
template.rstrip! if component_class.strip_trailing_whitespace?
|
264
|
-
|
265
|
-
if handler.method(:call).parameters.length > 1
|
266
|
-
handler.call(component_class, template)
|
267
|
-
# :nocov:
|
268
|
-
else
|
269
|
-
handler.call(
|
270
|
-
OpenStruct.new(
|
271
|
-
source: template,
|
272
|
-
identifier: component_class.identifier,
|
273
|
-
type: component_class.type
|
274
|
-
)
|
275
|
-
)
|
165
|
+
errors
|
276
166
|
end
|
277
|
-
# :nocov:
|
278
167
|
end
|
279
168
|
|
280
|
-
def
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
169
|
+
def gather_templates
|
170
|
+
@templates ||=
|
171
|
+
begin
|
172
|
+
templates = @component.sidecar_files(
|
173
|
+
ActionView::Template.template_handler_extensions
|
174
|
+
).map do |path|
|
175
|
+
# Extract format and variant from template filename
|
176
|
+
this_format, variant =
|
177
|
+
File
|
178
|
+
.basename(path) # "variants_component.html+mini.watch.erb"
|
179
|
+
.split(".")[1..-2] # ["html+mini", "watch"]
|
180
|
+
.join(".") # "html+mini.watch"
|
181
|
+
.split("+") # ["html", "mini.watch"]
|
182
|
+
.map(&:to_sym) # [:html, :"mini.watch"]
|
183
|
+
|
184
|
+
out = Template.new(
|
185
|
+
component: @component,
|
186
|
+
type: :file,
|
187
|
+
path: path,
|
188
|
+
lineno: 0,
|
189
|
+
extension: path.split(".").last,
|
190
|
+
this_format: this_format.to_s.split(".").last&.to_sym, # strip locale from this_format, see #2113
|
191
|
+
variant: variant
|
192
|
+
)
|
287
193
|
|
288
|
-
|
289
|
-
|
290
|
-
end
|
194
|
+
out
|
195
|
+
end
|
291
196
|
|
292
|
-
|
293
|
-
|
294
|
-
|
197
|
+
component_instance_methods_on_self = @component.instance_methods(false)
|
198
|
+
|
199
|
+
(
|
200
|
+
@component.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - @component.included_modules
|
201
|
+
).flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) }
|
202
|
+
.uniq
|
203
|
+
.each do |method_name|
|
204
|
+
templates << Template.new(
|
205
|
+
component: @component,
|
206
|
+
type: :inline_call,
|
207
|
+
this_format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT,
|
208
|
+
variant: method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil,
|
209
|
+
method_name: method_name,
|
210
|
+
defined_on_self: component_instance_methods_on_self.include?(method_name)
|
211
|
+
)
|
212
|
+
end
|
295
213
|
|
296
|
-
|
297
|
-
|
298
|
-
|
214
|
+
if @component.inline_template.present?
|
215
|
+
templates << Template.new(
|
216
|
+
component: @component,
|
217
|
+
type: :inline,
|
218
|
+
path: @component.inline_template.path,
|
219
|
+
lineno: @component.inline_template.lineno,
|
220
|
+
source: @component.inline_template.source.dup,
|
221
|
+
extension: @component.inline_template.language
|
222
|
+
)
|
223
|
+
end
|
299
224
|
|
300
|
-
|
301
|
-
|
302
|
-
component_class.private_instance_methods(false).include?(:call)
|
225
|
+
templates
|
226
|
+
end
|
303
227
|
end
|
304
228
|
end
|
305
229
|
end
|
@@ -48,6 +48,12 @@ module ViewComponent
|
|
48
48
|
#
|
49
49
|
# config.view_component.generate.stimulus_controller = true
|
50
50
|
#
|
51
|
+
# #### `#typescript`
|
52
|
+
#
|
53
|
+
# Generate TypeScript files instead of JavaScript files:
|
54
|
+
#
|
55
|
+
# config.view_component.generate.typescript = true
|
56
|
+
#
|
51
57
|
# #### `#locale`
|
52
58
|
#
|
53
59
|
# Always generate translations file alongside the component:
|
@@ -117,7 +123,7 @@ module ViewComponent
|
|
117
123
|
# @return [Boolean]
|
118
124
|
# Whether ActiveSupport Notifications use the private name `"!render.view_component"`
|
119
125
|
# or are made more publicly available via `"render.view_component"`.
|
120
|
-
# Will
|
126
|
+
# Will be removed in next major version.
|
121
127
|
# Defaults to `true`.
|
122
128
|
|
123
129
|
# @!attribute render_monkey_patch_enabled
|
@@ -146,7 +152,7 @@ module ViewComponent
|
|
146
152
|
# @!attribute preview_paths
|
147
153
|
# @return [Array<String>]
|
148
154
|
# The locations in which component previews will be looked up.
|
149
|
-
# Defaults to `['test/
|
155
|
+
# Defaults to `['test/components/previews']` relative to your Rails root.
|
150
156
|
|
151
157
|
# @!attribute test_controller
|
152
158
|
# @return [String]
|
@@ -168,11 +174,29 @@ module ViewComponent
|
|
168
174
|
# Defaults to `false`.
|
169
175
|
|
170
176
|
def default_preview_paths
|
177
|
+
(default_rails_preview_paths + default_rails_engines_preview_paths).uniq
|
178
|
+
end
|
179
|
+
|
180
|
+
def default_rails_preview_paths
|
171
181
|
return [] unless defined?(Rails.root) && Dir.exist?("#{Rails.root}/test/components/previews")
|
172
182
|
|
173
183
|
["#{Rails.root}/test/components/previews"]
|
174
184
|
end
|
175
185
|
|
186
|
+
def default_rails_engines_preview_paths
|
187
|
+
return [] unless defined?(Rails::Engine)
|
188
|
+
|
189
|
+
registered_rails_engines_with_previews.map do |descendant|
|
190
|
+
"#{descendant.root}/test/components/previews"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def registered_rails_engines_with_previews
|
195
|
+
Rails::Engine.descendants.select do |descendant|
|
196
|
+
defined?(descendant.root) && Dir.exist?("#{descendant.root}/test/components/previews")
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
176
200
|
def default_generate_options
|
177
201
|
options = ActiveSupport::OrderedOptions.new(false)
|
178
202
|
options.preview_path = ""
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
module Configurable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
next if respond_to?(:config) && config.respond_to?(:view_component) && config.respond_to_missing?(:test_controller)
|
9
|
+
|
10
|
+
include ActiveSupport::Configurable
|
11
|
+
|
12
|
+
configure do |config|
|
13
|
+
config.view_component ||= ActiveSupport::InheritableOptions.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -8,8 +8,22 @@ module ViewComponent
|
|
8
8
|
class Engine < Rails::Engine # :nodoc:
|
9
9
|
config.view_component = ViewComponent::Config.current
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
if Rails.version.to_f < 8.0
|
12
|
+
rake_tasks do
|
13
|
+
load "view_component/rails/tasks/view_component.rake"
|
14
|
+
end
|
15
|
+
else
|
16
|
+
initializer "view_component.stats_directories" do |app|
|
17
|
+
require "rails/code_statistics"
|
18
|
+
|
19
|
+
if Rails.root.join(ViewComponent::Base.view_component_path).directory?
|
20
|
+
Rails::CodeStatistics.register_directory("ViewComponents", ViewComponent::Base.view_component_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
if Rails.root.join("test/components").directory?
|
24
|
+
Rails::CodeStatistics.register_directory("ViewComponent tests", "test/components", test_directory: true)
|
25
|
+
end
|
26
|
+
end
|
13
27
|
end
|
14
28
|
|
15
29
|
initializer "view_component.set_configs" do |app|
|
@@ -128,11 +142,7 @@ module ViewComponent
|
|
128
142
|
end
|
129
143
|
|
130
144
|
initializer "compiler mode" do |_app|
|
131
|
-
ViewComponent::Compiler.
|
132
|
-
ViewComponent::Compiler::DEVELOPMENT_MODE
|
133
|
-
else
|
134
|
-
ViewComponent::Compiler::PRODUCTION_MODE
|
135
|
-
end
|
145
|
+
ViewComponent::Compiler.development_mode = (Rails.env.development? || Rails.env.test?)
|
136
146
|
end
|
137
147
|
|
138
148
|
config.after_initialize do |app|
|
@@ -165,12 +175,12 @@ module ViewComponent
|
|
165
175
|
end
|
166
176
|
|
167
177
|
# :nocov:
|
168
|
-
if RUBY_VERSION < "3.
|
169
|
-
ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 3.
|
178
|
+
if RUBY_VERSION < "3.2.0"
|
179
|
+
ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 3.2.0", "ViewComponent v4 will remove support for Ruby versions < 3.2.0 no earlier than April 1, 2025")
|
170
180
|
end
|
171
181
|
|
172
|
-
if Rails.version.to_f <
|
173
|
-
ViewComponent::Deprecation.deprecation_warning("Support for Rails versions <
|
182
|
+
if Rails.version.to_f < 7.1
|
183
|
+
ViewComponent::Deprecation.deprecation_warning("Support for Rails versions < 7.1", "ViewComponent v4 will remove support for Rails versions < 7.1 no earlier than April 1, 2025")
|
174
184
|
end
|
175
185
|
# :nocov:
|
176
186
|
|
@@ -17,8 +17,10 @@ module ViewComponent
|
|
17
17
|
end
|
18
18
|
|
19
19
|
class TemplateError < StandardError
|
20
|
-
def initialize(errors)
|
21
|
-
|
20
|
+
def initialize(errors, templates = nil)
|
21
|
+
message = errors.join("\n")
|
22
|
+
|
23
|
+
super(message)
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
@@ -175,7 +177,7 @@ module ViewComponent
|
|
175
177
|
|
176
178
|
class TranslateCalledBeforeRenderError < BaseError
|
177
179
|
MESSAGE =
|
178
|
-
"`#translate` can't be used
|
180
|
+
"`#translate` can't be used before rendering as it depends " \
|
179
181
|
"on the view context that only exists once a ViewComponent is passed to " \
|
180
182
|
"the Rails render pipeline.\n\n" \
|
181
183
|
"It's sometimes possible to fix this issue by moving code dependent on " \
|
@@ -184,7 +186,7 @@ module ViewComponent
|
|
184
186
|
|
185
187
|
class HelpersCalledBeforeRenderError < BaseError
|
186
188
|
MESSAGE =
|
187
|
-
"`#helpers` can't be used
|
189
|
+
"`#helpers` can't be used before rendering as it depends " \
|
188
190
|
"on the view context that only exists once a ViewComponent is passed to " \
|
189
191
|
"the Rails render pipeline.\n\n" \
|
190
192
|
"It's sometimes possible to fix this issue by moving code dependent on " \
|
@@ -193,7 +195,7 @@ module ViewComponent
|
|
193
195
|
|
194
196
|
class ControllerCalledBeforeRenderError < BaseError
|
195
197
|
MESSAGE =
|
196
|
-
"`#controller` can't be used
|
198
|
+
"`#controller` can't be used before rendering, as it depends " \
|
197
199
|
"on the view context that only exists once a ViewComponent is passed to " \
|
198
200
|
"the Rails render pipeline.\n\n" \
|
199
201
|
"It's sometimes possible to fix this issue by moving code dependent on " \
|
@@ -102,7 +102,7 @@ module ViewComponent # :nodoc:
|
|
102
102
|
|
103
103
|
def load_previews
|
104
104
|
Array(preview_paths).each do |preview_path|
|
105
|
-
Dir["#{preview_path}/**/*
|
105
|
+
Dir["#{preview_path}/**/*preview.rb"].sort.each { |file| require_dependency file }
|
106
106
|
end
|
107
107
|
end
|
108
108
|
|