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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/view_component/preview_actions.rb +8 -1
  3. data/app/helpers/preview_helper.rb +1 -1
  4. data/app/views/view_components/_preview_source.html.erb +1 -1
  5. data/docs/CHANGELOG.md +299 -5
  6. data/lib/rails/generators/abstract_generator.rb +9 -1
  7. data/lib/rails/generators/component/component_generator.rb +2 -1
  8. data/lib/rails/generators/component/templates/component.rb.tt +3 -2
  9. data/lib/rails/generators/erb/component_generator.rb +1 -1
  10. data/lib/rails/generators/preview/templates/component_preview.rb.tt +2 -0
  11. data/lib/rails/generators/rspec/templates/component_spec.rb.tt +1 -1
  12. data/lib/rails/generators/stimulus/component_generator.rb +8 -3
  13. data/lib/rails/generators/stimulus/templates/component_controller.ts.tt +9 -0
  14. data/lib/rails/generators/test_unit/templates/component_test.rb.tt +1 -1
  15. data/lib/view_component/base.rb +52 -59
  16. data/lib/view_component/collection.rb +18 -3
  17. data/lib/view_component/compiler.rb +164 -240
  18. data/lib/view_component/config.rb +26 -2
  19. data/lib/view_component/configurable.rb +17 -0
  20. data/lib/view_component/engine.rb +21 -11
  21. data/lib/view_component/errors.rb +7 -5
  22. data/lib/view_component/instrumentation.rb +1 -1
  23. data/lib/view_component/preview.rb +1 -1
  24. data/lib/view_component/rails/tasks/view_component.rake +8 -2
  25. data/lib/view_component/slotable.rb +28 -14
  26. data/lib/view_component/slotable_default.rb +20 -0
  27. data/lib/view_component/template.rb +134 -0
  28. data/lib/view_component/test_helpers.rb +29 -2
  29. data/lib/view_component/use_helpers.rb +32 -10
  30. data/lib/view_component/version.rb +2 -2
  31. metadata +112 -19
  32. data/lib/rails/generators/component/USAGE +0 -13
  33. data/lib/view_component/docs_builder_component.html.erb +0 -22
  34. data/lib/view_component/docs_builder_component.rb +0 -96
  35. 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
- # * development (a blocking mode which ensures thread safety when redefining the `call` method for components,
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
- # * production (a non-blocking mode, default in Rails production mode)
11
- DEVELOPMENT_MODE = :development
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
- class_attribute :mode, default: PRODUCTION_MODE
15
-
16
- def initialize(component_class)
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?(component_class)
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 component_class == ViewComponent::Base
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
- return false
40
- end
41
-
42
- if raise_errors
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
- if has_inline_template?
48
- template = component_class.inline_template
31
+ gather_templates
49
32
 
50
- redefinition_lock.synchronize do
51
- component_class.silence_redefinition_of_method("call")
52
- # rubocop:disable Style/EvalWithLocation
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
- component_class.define_method(:"_call_#{safe_class_name}", component_class.instance_method(:call))
37
+ if template_errors.present?
38
+ raise TemplateError.new(template_errors) if raise_errors
61
39
 
62
- component_class.silence_redefinition_of_method("render_template_for")
63
- component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
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
- else
70
- templates.each do |template|
71
- method_name = call_method_name(template[:variant])
72
- @variants_rendering_templates << template[:variant]
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
- component_class.build_i18n_backend
51
+ @component.register_default_slots
52
+ @component.build_i18n_backend
90
53
 
91
- CompileCache.register(component_class)
92
- end
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 :component_class, :redefinition_lock
60
+ attr_reader :templates
101
61
 
102
62
  def define_render_template_for
103
- variant_elsifs = variants.compact.uniq.map do |variant|
104
- safe_name = "_call_variant_#{normalized_variant_name(variant)}_#{safe_class_name}"
105
- component_class.define_method(safe_name, component_class.instance_method(call_method_name(variant)))
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
- body = <<-RUBY
113
- if variant.nil?
114
- _call_#{safe_class_name}
115
- #{variant_elsifs}
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
- _call_#{safe_class_name}
118
- end
119
- RUBY
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
- redefinition_lock.synchronize do
122
- component_class.silence_redefinition_of_method(:render_template_for)
123
- component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
124
- def render_template_for(variant = nil)
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
- def has_inline_template?
132
- component_class.respond_to?(:inline_template) && component_class.inline_template.present?
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
- @__vc_template_errors ||=
137
- begin
138
- errors = []
104
+ @_template_errors ||= begin
105
+ errors = []
139
106
 
140
- if (templates + inline_calls).empty? && !has_inline_template?
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
- if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
165
- errors <<
166
- "Template file and inline render method found for #{component_class}. " \
167
- "There can only be a template file or inline render method per component."
168
- end
169
-
170
- duplicate_template_file_and_inline_variant_calls =
171
- templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
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
- unless duplicate_template_file_and_inline_variant_calls.empty?
174
- count = duplicate_template_file_and_inline_variant_calls.count
119
+ variant_string = " for variant `#{variant}`" if variant.present?
175
120
 
176
- errors <<
177
- "Template #{"file".pluralize(count)} and inline render #{"method".pluralize(count)} " \
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
- uniq_variants = variants.compact.uniq
185
- normalized_variants = uniq_variants.map { |variant| normalized_variant_name(variant) }
124
+ default_template_types = @templates.each_with_object(Set.new) do |template, memo|
125
+ next if template.variant
186
126
 
187
- colliding_variants = uniq_variants.select do |variant|
188
- normalized_variants.count(normalized_variant_name(variant)) > 1
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
- unless colliding_variants.empty?
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
- component_class.sidecar_files(extensions).each_with_object([]) do |path, memo|
207
- pieces = File.basename(path).split(".")
208
- memo << {
209
- path: path,
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
- view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) }.uniq
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
- def compiled_inline_template(template)
249
- handler = ActionView::Template.handler_for_extension(template.language)
250
- template.rstrip! if component_class.strip_trailing_whitespace?
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
- compile_template(template.source, handler)
253
- end
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
- def compile_template(template, handler)
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 call_method_name(variant)
281
- if variant.present? && variants.include?(variant)
282
- "call_#{normalized_variant_name(variant)}"
283
- else
284
- "call"
285
- end
286
- end
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
- def normalized_variant_name(variant)
289
- variant.to_s.gsub("-", "__").gsub(".", "___")
290
- end
194
+ out
195
+ end
291
196
 
292
- def safe_class_name
293
- @safe_class_name ||= component_class.name.underscore.gsub("/", "__")
294
- end
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
- def should_compile_superclass?
297
- development? && templates.empty? && !has_inline_template? && !call_defined?
298
- end
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
- def call_defined?
301
- component_class.instance_methods(false).include?(:call) ||
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 default to `false` in next major version.
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/component/previews']` relative to your Rails root.
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
- rake_tasks do
12
- load "view_component/rails/tasks/view_component.rake"
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.mode = if Rails.env.development? || Rails.env.test?
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.0.0"
169
- ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 3.0.0", "ViewComponent 4.0 will remove support for Ruby versions < 3.0.0 ")
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 < 6.1
173
- ViewComponent::Deprecation.deprecation_warning("Support for Rails versions < 6.1", "ViewComponent 4.0 will remove support for Rails versions < 6.1 ")
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
- super(errors.join(", "))
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 during initialization as it depends " \
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 during initialization as it depends " \
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 during initialization, as it depends " \
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 " \
@@ -16,7 +16,7 @@ module ViewComponent # :nodoc:
16
16
  identifier: self.class.identifier
17
17
  }
18
18
  ) do
19
- super(view_context, &block)
19
+ super
20
20
  end
21
21
  end
22
22
 
@@ -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}/**/*_preview.rb"].sort.each { |file| require_dependency file }
105
+ Dir["#{preview_path}/**/*preview.rb"].sort.each { |file| require_dependency file }
106
106
  end
107
107
  end
108
108