view_component 2.82.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of view_component might be problematic. Click here for more details.

Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/view_component/preview_actions.rb +4 -0
  3. data/app/controllers/view_components_system_test_controller.rb +24 -1
  4. data/app/helpers/preview_helper.rb +2 -4
  5. data/docs/CHANGELOG.md +320 -0
  6. data/lib/view_component/base.rb +50 -96
  7. data/lib/view_component/capture_compatibility.rb +44 -0
  8. data/lib/view_component/collection.rb +2 -5
  9. data/lib/view_component/compiler.rb +51 -28
  10. data/lib/view_component/config.rb +9 -13
  11. data/lib/view_component/deprecation.rb +1 -1
  12. data/lib/view_component/docs_builder_component.html.erb +5 -1
  13. data/lib/view_component/docs_builder_component.rb +28 -9
  14. data/lib/view_component/engine.rb +12 -22
  15. data/lib/view_component/errors.rb +223 -0
  16. data/lib/view_component/inline_template.rb +55 -0
  17. data/lib/view_component/preview.rb +1 -7
  18. data/lib/view_component/rails/tasks/view_component.rake +1 -1
  19. data/lib/view_component/slot.rb +109 -1
  20. data/lib/view_component/slotable.rb +364 -96
  21. data/lib/view_component/system_test_helpers.rb +5 -5
  22. data/lib/view_component/test_helpers.rb +67 -54
  23. data/lib/view_component/translatable.rb +35 -23
  24. data/lib/view_component/version.rb +4 -3
  25. data/lib/view_component/with_content_helper.rb +3 -8
  26. data/lib/view_component.rb +3 -12
  27. metadata +45 -34
  28. data/lib/view_component/content_areas.rb +0 -56
  29. data/lib/view_component/polymorphic_slots.rb +0 -103
  30. data/lib/view_component/preview_template_error.rb +0 -6
  31. data/lib/view_component/slot_v2.rb +0 -98
  32. data/lib/view_component/slotable_v2.rb +0 -391
  33. data/lib/view_component/template_error.rb +0 -9
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ # CaptureCompatibility is a module that patches #capture to fix issues
5
+ # related to ViewComponent and functionality that relies on `capture`
6
+ # like forms, capture itself, turbo frames, etc.
7
+ #
8
+ # This underlying incompatibility with ViewComponent and capture is
9
+ # that several features like forms keep a reference to the primary
10
+ # `ActionView::Base` instance which has its own @output_buffer. When
11
+ # `#capture` is called on the original `ActionView::Base` instance while
12
+ # evaluating a block from a ViewComponent the @output_buffer is overridden
13
+ # in the ActionView::Base instance, and *not* the component. This results
14
+ # in a double render due to `#capture` implementation details.
15
+ #
16
+ # To resolve the issue, we override `#capture` so that we can delegate
17
+ # the `capture` logic to the ViewComponent that created the block.
18
+ module CaptureCompatibility
19
+ def self.included(base)
20
+ return if base < InstanceMethods
21
+
22
+ base.class_eval do
23
+ alias_method :original_capture, :capture
24
+ end
25
+
26
+ base.prepend(InstanceMethods)
27
+ end
28
+
29
+ module InstanceMethods
30
+ def capture(*args, &block)
31
+ # Handle blocks that originate from C code and raise, such as `&:method`
32
+ return original_capture(*args, &block) if block.source_location.nil?
33
+
34
+ block_context = block.binding.receiver
35
+
36
+ if block_context != self && block_context.class < ActionView::Base
37
+ block_context.original_capture(*args, &block)
38
+ else
39
+ original_capture(*args, &block)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -53,16 +53,13 @@ module ViewComponent
53
53
  if object.respond_to?(:to_ary)
54
54
  object.to_ary
55
55
  else
56
- raise ArgumentError.new(
57
- "The value of the first argument passed to `with_collection` isn't a valid collection. " \
58
- "Make sure it responds to `to_ary`."
59
- )
56
+ raise InvalidCollectionArgumentError
60
57
  end
61
58
  end
62
59
 
63
60
  def component_options(item, iterator)
64
61
  item_options = {component.collection_parameter => item}
65
- item_options[component.collection_counter_parameter] = iterator.index + 1 if component.counter_argument_present?
62
+ item_options[component.collection_counter_parameter] = iterator.index if component.counter_argument_present?
66
63
  item_options[component.collection_iteration_parameter] = iterator.dup if component.iteration_argument_present?
67
64
 
68
65
  @options.merge(item_options)
@@ -31,50 +31,58 @@ module ViewComponent
31
31
  return if component_class == ViewComponent::Base
32
32
 
33
33
  component_class.superclass.compile(raise_errors: raise_errors) if should_compile_superclass?
34
- subclass_instance_methods = component_class.instance_methods(false)
35
-
36
- if subclass_instance_methods.include?(:with_content) && raise_errors
37
- raise ViewComponent::ComponentError.new(
38
- "#{component_class} implements a reserved method, `#with_content`.\n\n" \
39
- "To fix this issue, change the name of the method."
40
- )
41
- end
42
34
 
43
35
  if template_errors.present?
44
- raise ViewComponent::TemplateError.new(template_errors) if raise_errors
36
+ raise TemplateError.new(template_errors) if raise_errors
45
37
 
46
38
  return false
47
39
  end
48
40
 
49
- if subclass_instance_methods.include?(:before_render_check)
50
- ViewComponent::Deprecation.deprecation_warning(
51
- "`before_render_check`", :"`before_render`"
52
- )
53
- end
54
-
55
41
  if raise_errors
56
42
  component_class.validate_initialization_parameters!
57
43
  component_class.validate_collection_parameter!
58
44
  end
59
45
 
60
- templates.each do |template|
61
- # Remove existing compiled template methods,
62
- # as Ruby warns when redefining a method.
63
- method_name = call_method_name(template[:variant])
46
+ if has_inline_template?
47
+ template = component_class.inline_template
64
48
 
65
49
  redefinition_lock.synchronize do
66
- component_class.silence_redefinition_of_method(method_name)
50
+ component_class.silence_redefinition_of_method("call")
67
51
  # rubocop:disable Style/EvalWithLocation
68
- component_class.class_eval <<-RUBY, template[:path], 0
69
- def #{method_name}
70
- #{compiled_template(template[:path])}
52
+ component_class.class_eval <<-RUBY, template.path, template.lineno
53
+ def call
54
+ #{compiled_inline_template(template)}
71
55
  end
72
56
  RUBY
73
57
  # rubocop:enable Style/EvalWithLocation
58
+
59
+ component_class.silence_redefinition_of_method("render_template_for")
60
+ component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
61
+ def render_template_for(variant = nil)
62
+ call
63
+ end
64
+ RUBY
65
+ end
66
+ else
67
+ templates.each do |template|
68
+ # Remove existing compiled template methods,
69
+ # as Ruby warns when redefining a method.
70
+ method_name = call_method_name(template[:variant])
71
+
72
+ redefinition_lock.synchronize do
73
+ component_class.silence_redefinition_of_method(method_name)
74
+ # rubocop:disable Style/EvalWithLocation
75
+ component_class.class_eval <<-RUBY, template[:path], 0
76
+ def #{method_name}
77
+ #{compiled_template(template[:path])}
78
+ end
79
+ RUBY
80
+ # rubocop:enable Style/EvalWithLocation
81
+ end
74
82
  end
75
- end
76
83
 
77
- define_render_template_for
84
+ define_render_template_for
85
+ end
78
86
 
79
87
  component_class.build_i18n_backend
80
88
 
@@ -109,12 +117,16 @@ module ViewComponent
109
117
  end
110
118
  end
111
119
 
120
+ def has_inline_template?
121
+ component_class.respond_to?(:inline_template) && component_class.inline_template.present?
122
+ end
123
+
112
124
  def template_errors
113
125
  @__vc_template_errors ||=
114
126
  begin
115
127
  errors = []
116
128
 
117
- if (templates + inline_calls).empty?
129
+ if (templates + inline_calls).empty? && !has_inline_template?
118
130
  errors << "Couldn't find a template file or inline render method for #{component_class}."
119
131
  end
120
132
 
@@ -222,9 +234,21 @@ module ViewComponent
222
234
  end
223
235
  end
224
236
 
237
+ def compiled_inline_template(template)
238
+ handler = ActionView::Template.handler_for_extension(template.language)
239
+ template.rstrip! if component_class.strip_trailing_whitespace?
240
+
241
+ compile_template(template.source, handler)
242
+ end
243
+
225
244
  def compiled_template(file_path)
226
245
  handler = ActionView::Template.handler_for_extension(File.extname(file_path).delete("."))
227
246
  template = File.read(file_path)
247
+
248
+ compile_template(template, handler)
249
+ end
250
+
251
+ def compile_template(template, handler)
228
252
  template.rstrip! if component_class.strip_trailing_whitespace?
229
253
 
230
254
  if handler.method(:call).parameters.length > 1
@@ -253,8 +277,7 @@ module ViewComponent
253
277
  end
254
278
 
255
279
  def should_compile_superclass?
256
- development? &&
257
- templates.empty? &&
280
+ development? && templates.empty? && !has_inline_template? &&
258
281
  !(
259
282
  component_class.instance_methods(false).include?(:call) ||
260
283
  component_class.private_instance_methods(false).include?(:call)
@@ -23,7 +23,8 @@ module ViewComponent
23
23
  show_previews: Rails.env.development? || Rails.env.test?,
24
24
  preview_paths: default_preview_paths,
25
25
  test_controller: "ApplicationController",
26
- default_preview_layout: nil
26
+ default_preview_layout: nil,
27
+ capture_compatibility_patch_enabled: false
27
28
  })
28
29
  end
29
30
 
@@ -126,9 +127,6 @@ module ViewComponent
126
127
  # The locations in which component previews will be looked up.
127
128
  # Defaults to `['test/component/previews']` relative to your Rails root.
128
129
 
129
- # @!attribute preview_path
130
- # @deprecated Use #preview_paths instead. Will be removed in v3.0.0.
131
-
132
130
  # @!attribute test_controller
133
131
  # @return [String]
134
132
  # The controller used for testing components.
@@ -141,6 +139,13 @@ module ViewComponent
141
139
  # previews.
142
140
  # Defaults to `nil`. If this is falsy, `"component_preview"` is used.
143
141
 
142
+ # @!attribute capture_compatibility_patch_enabled
143
+ # @return [Boolean]
144
+ # Enables the experimental capture compatibility patch that makes ViewComponent
145
+ # compatible with forms, capture, and other built-ins.
146
+ # previews.
147
+ # Defaults to `false`.
148
+
144
149
  def default_preview_paths
145
150
  return [] unless defined?(Rails.root) && Dir.exist?("#{Rails.root}/test/components/previews")
146
151
 
@@ -158,15 +163,6 @@ module ViewComponent
158
163
  @config = self.class.defaults
159
164
  end
160
165
 
161
- def preview_path
162
- preview_paths
163
- end
164
-
165
- def preview_path=(new_value)
166
- ViewComponent::Deprecation.deprecation_warning("`preview_path`", :"`preview_paths`")
167
- self.preview_paths = Array.wrap(new_value)
168
- end
169
-
170
166
  delegate_missing_to :config
171
167
 
172
168
  private
@@ -3,6 +3,6 @@
3
3
  require "active_support/deprecation"
4
4
 
5
5
  module ViewComponent
6
- DEPRECATION_HORIZON = "3.0.0"
6
+ DEPRECATION_HORIZON = "4.0.0"
7
7
  Deprecation = ActiveSupport::Deprecation.new(DEPRECATION_HORIZON, "ViewComponent")
8
8
  end
@@ -12,7 +12,11 @@ nav_order: 3
12
12
  ## <%= section.heading %>
13
13
 
14
14
  <% section.methods.each do |method| %>
15
- ### <%== render ViewComponent::DocsBuilderComponent::MethodDoc.new(method) %>
15
+ ### <%== render ViewComponent::DocsBuilderComponent::MethodDoc.new(method, section.show_types) %>
16
+
17
+ <% end %>
18
+ <% section.error_klasses.each do |error_klass| %>
19
+ ### <%== render ViewComponent::DocsBuilderComponent::ErrorKlassDoc.new(error_klass, section.show_types) %>
16
20
 
17
21
  <% end %>
18
22
  <% end %>
@@ -2,21 +2,40 @@
2
2
 
3
3
  module ViewComponent
4
4
  class DocsBuilderComponent < Base
5
- class Section < Struct.new(:heading, :methods, :show_types, keyword_init: true)
6
- def initialize(heading: nil, methods: [], show_types: true)
5
+ class Section < Struct.new(:heading, :methods, :error_klasses, :show_types, keyword_init: true)
6
+ def initialize(heading: nil, methods: [], error_klasses: [], show_types: true)
7
7
  methods.sort_by! { |method| method[:name] }
8
+ error_klasses.sort!
8
9
  super
9
10
  end
10
11
  end
11
12
 
12
- class MethodDoc < ViewComponent::Base
13
- def initialize(method, section: Section.new(show_types: true))
14
- @method = method
15
- @section = section
13
+ class ErrorKlassDoc < ViewComponent::Base
14
+ def initialize(error_klass, _show_types)
15
+ @error_klass = error_klass
16
+ end
17
+
18
+ def klass_name
19
+ @error_klass.gsub("ViewComponent::", "").gsub("::MESSAGE", "")
16
20
  end
17
21
 
18
- def show_types?
19
- @section.show_types
22
+ def error_message
23
+ ViewComponent.const_get(@error_klass)
24
+ end
25
+
26
+ def call
27
+ <<~DOCS.chomp
28
+ `#{klass_name}`
29
+
30
+ #{error_message}
31
+ DOCS
32
+ end
33
+ end
34
+
35
+ class MethodDoc < ViewComponent::Base
36
+ def initialize(method, show_types = true)
37
+ @method = method
38
+ @show_types = show_types
20
39
  end
21
40
 
22
41
  def deprecated?
@@ -28,7 +47,7 @@ module ViewComponent
28
47
  end
29
48
 
30
49
  def types
31
- " → [#{@method.tag(:return).types.join(",")}]" if @method.tag(:return)&.types && show_types?
50
+ " → [#{@method.tag(:return).types.join(",")}]" if @method.tag(:return)&.types && @show_types
32
51
  end
33
52
 
34
53
  def signature_or_name
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails"
4
- require "view_component/base"
4
+ require "view_component/config"
5
5
 
6
6
  module ViewComponent
7
7
  class Engine < Rails::Engine # :nodoc:
8
- config.view_component = ViewComponent::Base.config
8
+ config.view_component = ViewComponent::Config.defaults
9
9
 
10
10
  rake_tasks do
11
11
  load "view_component/rails/tasks/view_component.rake"
@@ -14,9 +14,6 @@ module ViewComponent
14
14
  initializer "view_component.set_configs" do |app|
15
15
  options = app.config.view_component
16
16
 
17
- %i[generate preview_controller preview_route show_previews_source].each do |config_option|
18
- options[config_option] ||= ViewComponent::Base.public_send(config_option)
19
- end
20
17
  options.instrumentation_enabled = false if options.instrumentation_enabled.nil?
21
18
  options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
22
19
  options.show_previews = (Rails.env.development? || Rails.env.test?) if options.show_previews.nil?
@@ -39,6 +36,8 @@ module ViewComponent
39
36
 
40
37
  initializer "view_component.enable_instrumentation" do |app|
41
38
  ActiveSupport.on_load(:view_component) do
39
+ Base.config = app.config.view_component
40
+
42
41
  if app.config.view_component.instrumentation_enabled.present?
43
42
  # :nocov:
44
43
  ViewComponent::Base.prepend(ViewComponent::Instrumentation)
@@ -47,6 +46,14 @@ module ViewComponent
47
46
  end
48
47
  end
49
48
 
49
+ # :nocov:
50
+ initializer "view_component.enable_capture_patch" do |app|
51
+ ActiveSupport.on_load(:view_component) do
52
+ ActionView::Base.include(ViewComponent::CaptureCompatibility) if app.config.view_component.capture_compatibility_patch_enabled
53
+ end
54
+ end
55
+ # :nocov:
56
+
50
57
  initializer "view_component.set_autoload_paths" do |app|
51
58
  options = app.config.view_component
52
59
 
@@ -143,20 +150,3 @@ module ViewComponent
143
150
  end
144
151
  end
145
152
  end
146
-
147
- if RUBY_VERSION < "2.7.0"
148
- ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 2.7.0")
149
- end
150
-
151
- # :nocov:
152
- unless defined?(ViewComponent::Base)
153
- require "view_component/deprecation"
154
-
155
- ViewComponent::Deprecation.deprecation_warning(
156
- "Manually loading the engine",
157
- "remove `require \"view_component/engine\"`"
158
- )
159
-
160
- require "view_component"
161
- end
162
- # :nocov:
@@ -0,0 +1,223 @@
1
+ module ViewComponent
2
+ class BaseError < StandardError
3
+ def initialize
4
+ super(self.class::MESSAGE)
5
+ end
6
+ end
7
+
8
+ class DuplicateSlotContentError < StandardError
9
+ MESSAGE =
10
+ "It looks like a block was provided after calling `with_content` on COMPONENT, " \
11
+ "which means that ViewComponent doesn't know which content to use.\n\n" \
12
+ "To fix this issue, use either `with_content` or a block."
13
+
14
+ def initialize(klass_name)
15
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
16
+ end
17
+ end
18
+
19
+ class TemplateError < StandardError
20
+ def initialize(errors)
21
+ super(errors.join(", "))
22
+ end
23
+ end
24
+
25
+ class MultipleInlineTemplatesError < BaseError
26
+ MESSAGE = "Inline templates can only be defined once per-component."
27
+ end
28
+
29
+ class MissingPreviewTemplateError < StandardError
30
+ MESSAGE =
31
+ "A preview template for example EXAMPLE doesn't exist.\n\n" \
32
+ "To fix this issue, create a template for the example."
33
+
34
+ def initialize(example)
35
+ super(MESSAGE.gsub("EXAMPLE", example))
36
+ end
37
+ end
38
+
39
+ class DuplicateContentError < StandardError
40
+ MESSAGE =
41
+ "It looks like a block was provided after calling `with_content` on COMPONENT, " \
42
+ "which means that ViewComponent doesn't know which content to use.\n\n" \
43
+ "To fix this issue, use either `with_content` or a block."
44
+
45
+ def initialize(klass_name)
46
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
47
+ end
48
+ end
49
+
50
+ class EmptyOrInvalidInitializerError < StandardError
51
+ MESSAGE =
52
+ "The COMPONENT initializer is empty or invalid. " \
53
+ "It must accept the parameter `PARAMETER` to render it as a collection.\n\n" \
54
+ "To fix this issue, update the initializer to accept `PARAMETER`.\n\n" \
55
+ "See [the collections docs](https://viewcomponent.org/guide/collections.html) for more information on rendering collections."
56
+
57
+ def initialize(klass_name, parameter)
58
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
59
+ end
60
+ end
61
+
62
+ class MissingCollectionArgumentError < StandardError
63
+ MESSAGE =
64
+ "The initializer for COMPONENT doesn't accept the parameter `PARAMETER`, " \
65
+ "which is required to render it as a collection.\n\n" \
66
+ "To fix this issue, update the initializer to accept `PARAMETER`.\n\n" \
67
+ "See [the collections docs](https://viewcomponent.org/guide/collections.html) for more information on rendering collections."
68
+
69
+ def initialize(klass_name, parameter)
70
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
71
+ end
72
+ end
73
+
74
+ class ReservedParameterError < StandardError
75
+ MESSAGE =
76
+ "COMPONENT initializer can't accept the parameter `PARAMETER`, as it will override a " \
77
+ "public ViewComponent method. To fix this issue, rename the parameter."
78
+
79
+ def initialize(klass_name, parameter)
80
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
81
+ end
82
+ end
83
+
84
+ class InvalidCollectionArgumentError < BaseError
85
+ MESSAGE =
86
+ "The value of the first argument passed to `with_collection` isn't a valid collection. " \
87
+ "Make sure it responds to `to_ary`."
88
+ end
89
+
90
+ class ContentSlotNameError < StandardError
91
+ MESSAGE =
92
+ "COMPONENT declares a slot named content, which is a reserved word in ViewComponent.\n\n" \
93
+ "Content passed to a ViewComponent as a block is captured and assigned to the `content` accessor without having to create an explicit slot.\n\n" \
94
+ "To fix this issue, either use the `content` accessor directly or choose a different slot name."
95
+
96
+ def initialize(klass_name)
97
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
98
+ end
99
+ end
100
+
101
+ class InvalidSlotDefinitionError < BaseError
102
+ MESSAGE =
103
+ "Invalid slot definition. Please pass a class, " \
104
+ "string, or callable (that is proc, lambda, etc)"
105
+ end
106
+
107
+ class SlotPredicateNameError < StandardError
108
+ MESSAGE =
109
+ "COMPONENT declares a slot named SLOT_NAME, which ends with a question mark.\n\n" \
110
+ "This isn't allowed because the ViewComponent framework already provides predicate " \
111
+ "methods ending in `?`.\n\n" \
112
+ "To fix this issue, choose a different name."
113
+
114
+ def initialize(klass_name, slot_name)
115
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
116
+ end
117
+ end
118
+
119
+ class RedefinedSlotError < StandardError
120
+ MESSAGE =
121
+ "COMPONENT declares the SLOT_NAME slot multiple times.\n\n" \
122
+ "To fix this issue, choose a different slot name."
123
+
124
+ def initialize(klass_name, slot_name)
125
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
126
+ end
127
+ end
128
+
129
+ class ReservedSingularSlotNameError < StandardError
130
+ MESSAGE =
131
+ "COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \
132
+ "To fix this issue, choose a different name."
133
+
134
+ def initialize(klass_name, slot_name)
135
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
136
+ end
137
+ end
138
+
139
+ class ReservedPluralSlotNameError < StandardError
140
+ MESSAGE =
141
+ "COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \
142
+ "To fix this issue, choose a different name."
143
+
144
+ def initialize(klass_name, slot_name)
145
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
146
+ end
147
+ end
148
+
149
+ class ContentAlreadySetForPolymorphicSlotError < StandardError
150
+ MESSAGE = "Content for slot SLOT_NAME has already been provided."
151
+
152
+ def initialize(slot_name)
153
+ super(MESSAGE.gsub("SLOT_NAME", slot_name.to_s))
154
+ end
155
+ end
156
+
157
+ class NilWithContentError < BaseError
158
+ MESSAGE =
159
+ "No content provided to `#with_content` for #{self}.\n\n" \
160
+ "To fix this issue, pass a value."
161
+ end
162
+
163
+ class TranslateCalledBeforeRenderError < BaseError
164
+ MESSAGE =
165
+ "`#translate` can't be used during initialization as it depends " \
166
+ "on the view context that only exists once a ViewComponent is passed to " \
167
+ "the Rails render pipeline.\n\n" \
168
+ "It's sometimes possible to fix this issue by moving code dependent on " \
169
+ "`#translate` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
170
+ end
171
+
172
+ class HelpersCalledBeforeRenderError < BaseError
173
+ MESSAGE =
174
+ "`#helpers` can't be used during initialization as it depends " \
175
+ "on the view context that only exists once a ViewComponent is passed to " \
176
+ "the Rails render pipeline.\n\n" \
177
+ "It's sometimes possible to fix this issue by moving code dependent on " \
178
+ "`#helpers` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
179
+ end
180
+
181
+ class ControllerCalledBeforeRenderError < BaseError
182
+ MESSAGE =
183
+ "`#controller` can't be used during initialization, as it depends " \
184
+ "on the view context that only exists once a ViewComponent is passed to " \
185
+ "the Rails render pipeline.\n\n" \
186
+ "It's sometimes possible to fix this issue by moving code dependent on " \
187
+ "`#controller` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
188
+ end
189
+
190
+ class NoMatchingTemplatesForPreviewError < StandardError
191
+ MESSAGE = "Found 0 matches for templates for TEMPLATE_IDENTIFIER."
192
+
193
+ def initialize(template_identifier)
194
+ super(MESSAGE.gsub("TEMPLATE_IDENTIFIER", template_identifier))
195
+ end
196
+ end
197
+
198
+ class MultipleMatchingTemplatesForPreviewError < StandardError
199
+ MESSAGE = "Found multiple templates for TEMPLATE_IDENTIFIER."
200
+
201
+ def initialize(template_identifier)
202
+ super(MESSAGE.gsub("TEMPLATE_IDENTIFIER", template_identifier))
203
+ end
204
+ end
205
+
206
+ class SystemTestControllerOnlyAllowedInTestError < BaseError
207
+ MESSAGE = "ViewComponent SystemTest controller must only be called in a test environment for security reasons."
208
+ end
209
+
210
+ class SystemTestControllerNefariousPathError < BaseError
211
+ MESSAGE = "ViewComponent SystemTest controller attempted to load a file outside of the expected directory."
212
+ end
213
+
214
+ class AlreadyDefinedPolymorphicSlotSetterError < StandardError
215
+ MESSAGE =
216
+ "A method called 'SETTER_METHOD_NAME' already exists and would be overwritten by the 'SETTER_NAME' polymorphic " \
217
+ "slot setter.\n\nPlease choose a different setter name."
218
+
219
+ def initialize(setter_method_name, setter_name)
220
+ super(MESSAGE.gsub("SETTER_METHOD_NAME", setter_method_name.to_s).gsub("SETTER_NAME", setter_name.to_s))
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent # :nodoc:
4
+ module InlineTemplate
5
+ extend ActiveSupport::Concern
6
+ Template = Struct.new(:source, :language, :path, :lineno)
7
+
8
+ class_methods do
9
+ def method_missing(method, *args)
10
+ return super if !method.end_with?("_template")
11
+
12
+ if defined?(@__vc_inline_template_defined) && @__vc_inline_template_defined
13
+ raise MultipleInlineTemplatesError
14
+ end
15
+
16
+ if args.size != 1
17
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)"
18
+ end
19
+
20
+ ext = method.to_s.gsub("_template", "")
21
+ template = args.first
22
+
23
+ @__vc_inline_template_language = ext
24
+
25
+ caller = caller_locations(1..1)[0]
26
+ @__vc_inline_template = Template.new(
27
+ template,
28
+ ext,
29
+ caller.absolute_path || caller.path,
30
+ caller.lineno
31
+ )
32
+
33
+ @__vc_inline_template_defined = true
34
+ end
35
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
36
+
37
+ def respond_to_missing?(method, include_all = false)
38
+ method.end_with?("_template") || super
39
+ end
40
+
41
+ def inline_template
42
+ @__vc_inline_template
43
+ end
44
+
45
+ def inline_template_language
46
+ @__vc_inline_template_language if defined?(@__vc_inline_template_language)
47
+ end
48
+
49
+ def inherited(subclass)
50
+ super
51
+ subclass.instance_variable_set(:@__vc_inline_template_language, inline_template_language)
52
+ end
53
+ end
54
+ end
55
+ end