view_component 3.0.0.rc5 → 3.0.0.rc6

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.

Potentially problematic release.


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

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3746041e156447e727a9974ad435795bd08333c13f6004ba5a03f629764d301d
4
- data.tar.gz: 47e3d85474b5cb69fbc12abb48a59a85776f14475508ff55f40f672f393bd309
3
+ metadata.gz: d82093cc048c5af21538ee6ec01aa2ef474b71b97d2cae0fd02413f036b97616
4
+ data.tar.gz: 376d3bf8597d3eccd871ab9f8f1fa71a55f9564e44f7fcf920115c50e89b6224
5
5
  SHA512:
6
- metadata.gz: cc5edccc814c845eb63ce951e621365ec9ec3013363b32b0bc5f05db8a0fb15ea26688eae22f54fcc17bbc477c5e93350cd0b3fbf3af9bd1e27f1f3f9e53cbec
7
- data.tar.gz: d87a2ee301acf5b4e67ec159bc3433ce2d46da343de5da26af975314b862b4befbfdd247a0046239b99bb322e26b4062625073424ba0fd7e54a1e973a2d56ebd
6
+ metadata.gz: e410d228fd97a8007d50f7a4bfc262c20481ac3e1279f7e2cfada131fb475ca65991e99a2af6ee059c16dea38913974c72aca62d1d4c1d4530dcce8c13159721
7
+ data.tar.gz: 1d7bccb010f977dde4435af3aab5c4df57af2157684b924dea54b43b788d3624d1a914e15602f138184d9c222e9a43c80c2088c95b34269a8d994ec6eb5058c7
@@ -15,7 +15,7 @@ class ViewComponentsSystemTestController < ActionController::Base # :nodoc:
15
15
  private
16
16
 
17
17
  def validate_test_env
18
- raise "ViewComponentsSystemTestController must only be called in a test environment" unless Rails.env.test?
18
+ raise ViewComponent::SystemTestControllerOnlyAllowedInTestError unless Rails.env.test?
19
19
  end
20
20
 
21
21
  # Ensure that the file path is valid and doesn't target files outside
@@ -24,7 +24,7 @@ class ViewComponentsSystemTestController < ActionController::Base # :nodoc:
24
24
  base_path = ::File.realpath(self.class.temp_dir)
25
25
  @path = ::File.realpath(params.permit(:file)[:file], base_path)
26
26
  unless @path.start_with?(base_path)
27
- raise ArgumentError, "Invalid file path"
27
+ raise ViewComponent::SystemTestControllerNefariousPathError
28
28
  end
29
29
  end
30
30
  end
@@ -31,10 +31,8 @@ module PreviewHelper
31
31
  path =~ /#{template_identifier}*.(html)/
32
32
  end
33
33
 
34
- # In-case of a conflict due to multiple template files with
35
- # the same name
36
- raise "found 0 matches for templates for #{template_identifier}." if matching_templates.empty?
37
- raise "found multiple templates for #{template_identifier}." if matching_templates.size > 1
34
+ raise ViewComponent::NoMatchingTemplatesForPreviewError.new(template_identifier) if matching_templates.empty?
35
+ raise ViewComponent::MultipleMatchingTemplatesForPreviewError.new(template_identifier) if matching_templates.size > 1
38
36
 
39
37
  template_file_path = matching_templates.first
40
38
  template_source = File.read(template_file_path)
data/docs/CHANGELOG.md CHANGED
@@ -10,13 +10,63 @@ nav_order: 5
10
10
 
11
11
  ## main
12
12
 
13
+ ### v3.0.0.rc6
14
+
15
+ Run into an issue with this release candidate? [Let us know](https://github.com/ViewComponent/view_component/issues/1629). We hope to release v3.0.0 in the near future!
16
+
17
+ * BREAKING: `#SLOT_NAME` getter no longer accepts arguments. This change was missed as part of the earlier deprecation in `3.0.0.rc1`.
18
+
19
+ *Joel Hawksley*
20
+
21
+ * BREAKING: Raise `TranslateCalledBeforeRenderError`, `ControllerCalledBeforeRenderError`, or `HelpersCalledBeforeRenderError` instead of `ViewContextCalledBeforeRenderError`.
22
+
23
+ *Joel Hawksley*
24
+
25
+ * BREAKING: Raise `SlotPredicateNameError`, `RedefinedSlotError`, `ReservedSingularSlotNameError`, `ContentSlotNameError`, `InvalidSlotDefinitionError`, `ReservedPluralSlotNameError`, `ContentAlreadySetForPolymorphicSlotErrror`, `SystemTestControllerOnlyAllowedInTestError`, `SystemTestControllerNefariousPathError`, `NoMatchingTemplatesForPreviewError`, `MultipleMatchingTemplatesForPreviewError`, `DuplicateContentError`, `EmptyOrInvalidInitializerError`, `MissingCollectionArgumentError`, `ReservedParameterError`, `InvalidCollectionArgumentError`, `MultipleInlineTemplatesError`, `MissingPreviewTemplateError`, `DuplicateSlotContentError` or `NilWithContentError` instead of generic error classes.
26
+
27
+ *Joel Hawksley*
28
+
29
+ * Fix bug where `content?` and `with_content` didn't work reliably with slots.
30
+
31
+ *Derek Kniffin, Joel Hawksley*
32
+
33
+ * Add `with_SLOT_NAME_content` helper.
34
+
35
+ *Will Cosgrove*
36
+
37
+ * Allow ActiveRecord objects to be passed to `renders_many`.
38
+
39
+ *Leigh Halliday*
40
+
41
+ * Fix broken links in documentation.
42
+
43
+ *Ellen Keal*
44
+
45
+ * Run `standardrb` against markdown in docs.
46
+
47
+ *Joel Hawksley*
48
+
49
+ * Allow `.with_content` to be redefined by components.
50
+
51
+ *Joel Hawksley*
52
+
53
+ * Run `standardrb` against markdown in docs.
54
+
55
+ *Joel Hawksley*
56
+
57
+ * Raise error if translations are used in initializer.
58
+
59
+ *Joel Hawksley*
60
+
13
61
  ## v3.0.0.rc5
14
62
 
63
+ Run into an issue with this release candidate? [Let us know](https://github.com/ViewComponent/view_component/issues/1629).
64
+
15
65
  * Fix bug where `mkdir_p` failed due to incorrect permissions.
16
66
 
17
67
  *Joel Hawksley*
18
68
 
19
- * Check for inline `erb_template` calls when deciding whether or not to compile a component's superclass.
69
+ * Check for inline `erb_template` calls when deciding whether to compile a component's superclass.
20
70
 
21
71
  *Justin Kenyon*
22
72
 
@@ -72,7 +122,7 @@ Run into an issue with this release? [Let us know](https://github.com/ViewCompon
72
122
 
73
123
  *Joel Hawksley*
74
124
 
75
- * BREAKING: Rename private TestHelpers#controller, #build_controller, #request, and #preview_class to avoid conflicts. Note: While these methods were undocumented and marked as private, they was easily accessible in tests. As such, we're cautiously considering this to be a breaking change.
125
+ * BREAKING: Rename private TestHelpers#controller, #build_controller, #request, and #preview_class to avoid conflicts. Note: While these methods were undocumented and marked as private, they were accessible in tests. As such, we're considering this to be a breaking change.
76
126
 
77
127
  *Joel Hawksley*
78
128
 
@@ -112,7 +162,7 @@ Run into an issue with this release? [Let us know](https://github.com/ViewCompon
112
162
 
113
163
  1,000+ days and 100+ releases later, the 200+ contributors to ViewComponent are proud to ship v3.0.0!
114
164
 
115
- We're so grateful for all of the work of community members to get us to this release. Whether it’s filing bug reports, designing APIs in long-winded discussion threads, or writing code itself, ViewComponent is built by the community, for the community. We couldn’t be more proud of what we’re building together :heart:
165
+ We're so grateful for all the work of community members to get us to this release. Whether it’s filing bug reports, designing APIs in long-winded discussion threads, or writing code itself, ViewComponent is built by the community, for the community. We couldn’t be more proud of what we’re building together :heart:
116
166
 
117
167
  This release makes the following breaking changes, many of which have long been deprecated:
118
168
 
@@ -6,6 +6,7 @@ require "view_component/collection"
6
6
  require "view_component/compile_cache"
7
7
  require "view_component/compiler"
8
8
  require "view_component/config"
9
+ require "view_component/errors"
9
10
  require "view_component/preview"
10
11
  require "view_component/slotable"
11
12
  require "view_component/translatable"
@@ -32,8 +33,6 @@ module ViewComponent
32
33
  include ViewComponent::Translatable
33
34
  include ViewComponent::WithContentHelper
34
35
 
35
- ViewContextCalledBeforeRenderError = Class.new(StandardError)
36
-
37
36
  RESERVED_PARAMETER = :content
38
37
 
39
38
  # For CSRF authenticity tokens in forms
@@ -94,9 +93,7 @@ module ViewComponent
94
93
  @current_template = self
95
94
 
96
95
  if block && defined?(@__vc_content_set_by_with_content)
97
- raise ArgumentError, "It looks like a block was provided after calling `with_content` on #{self.class.name}, " \
98
- "which means that ViewComponent doesn't know which content to use.\n\n" \
99
- "To fix this issue, use either `with_content` or a block."
96
+ raise DuplicateContentError.new(self.class.name)
100
97
  end
101
98
 
102
99
  @__vc_content_evaluated = false
@@ -175,16 +172,7 @@ module ViewComponent
175
172
  #
176
173
  # @return [ActionController::Base]
177
174
  def controller
178
- if view_context.nil?
179
- raise(
180
- ViewContextCalledBeforeRenderError,
181
- "`#controller` can't be used during initialization, as it depends " \
182
- "on the view context that only exists once a ViewComponent is passed to " \
183
- "the Rails render pipeline.\n\n" \
184
- "It's sometimes possible to fix this issue by moving code dependent on " \
185
- "`#controller` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
186
- )
187
- end
175
+ raise ControllerCalledBeforeRenderError if view_context.nil?
188
176
 
189
177
  @__vc_controller ||= view_context.controller
190
178
  end
@@ -194,16 +182,7 @@ module ViewComponent
194
182
  #
195
183
  # @return [ActionView::Base]
196
184
  def helpers
197
- if view_context.nil?
198
- raise(
199
- ViewContextCalledBeforeRenderError,
200
- "`#helpers` can't be used during initialization, as it depends " \
201
- "on the view context that only exists once a ViewComponent is passed to " \
202
- "the Rails render pipeline.\n\n" \
203
- "It's sometimes possible to fix this issue by moving code dependent on " \
204
- "`#helpers` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
205
- )
206
- end
185
+ raise HelpersCalledBeforeRenderError if view_context.nil?
207
186
 
208
187
  # Attempt to re-use the original view_context passed to the first
209
188
  # component rendered in the rendering pipeline. This prevents the
@@ -270,7 +249,7 @@ module ViewComponent
270
249
  attr_reader :view_context
271
250
 
272
251
  def __vc_render_in_block_provided?
273
- @view_context && @__vc_render_in_block
252
+ defined?(@view_context) && @view_context && @__vc_render_in_block
274
253
  end
275
254
 
276
255
  def __vc_content_set_by_with_content_defined?
@@ -278,7 +257,7 @@ module ViewComponent
278
257
  end
279
258
 
280
259
  def content_evaluated?
281
- @__vc_content_evaluated
260
+ defined?(@__vc_content_evaluated) && @__vc_content_evaluated
282
261
  end
283
262
 
284
263
  # Set the controller used for testing components:
@@ -535,7 +514,7 @@ module ViewComponent
535
514
  # end
536
515
  # ```
537
516
  #
538
- # @param value [Boolean] Whether or not to strip newlines.
517
+ # @param value [Boolean] Whether to strip newlines.
539
518
  def strip_trailing_whitespace(value = true)
540
519
  self.__vc_strip_trailing_whitespace = value
541
520
  end
@@ -559,20 +538,14 @@ module ViewComponent
559
538
  return unless parameter
560
539
  return if initialize_parameter_names.include?(parameter) || splatted_keyword_argument_present?
561
540
 
562
- # If Ruby can't parse the component class, then the initalize
541
+ # If Ruby can't parse the component class, then the initialize
563
542
  # parameters will be empty and ViewComponent will not be able to render
564
543
  # the component.
565
544
  if initialize_parameters.empty?
566
- raise ArgumentError, "The #{self} initializer is empty or invalid." \
567
- "It must accept the parameter `#{parameter}` to render it as a collection.\n\n" \
568
- "To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
569
- "See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
545
+ raise EmptyOrInvalidInitializerError.new(name, parameter)
570
546
  end
571
547
 
572
- raise ArgumentError, "The initializer for #{self} doesn't accept the parameter `#{parameter}`, " \
573
- "which is required in order to render it as a collection.\n\n" \
574
- "To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
575
- "See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
548
+ raise MissingCollectionArgumentError.new(name, parameter)
576
549
  end
577
550
 
578
551
  # Ensure the component initializer doesn't define
@@ -582,8 +555,7 @@ module ViewComponent
582
555
  def validate_initialization_parameters!
583
556
  return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
584
557
 
585
- raise ViewComponent::ComponentError, "#{self} initializer can't accept the parameter `#{RESERVED_PARAMETER}`, as it will override a " \
586
- "public ViewComponent method. To fix this issue, rename the parameter."
558
+ raise ReservedParameterError.new(name, RESERVED_PARAMETER)
587
559
  end
588
560
 
589
561
  # @private
@@ -53,10 +53,7 @@ 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
 
@@ -31,17 +31,9 @@ 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
@@ -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
@@ -0,0 +1,213 @@
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
+ end
@@ -10,7 +10,7 @@ module ViewComponent # :nodoc:
10
10
  return super if !method.end_with?("_template")
11
11
 
12
12
  if defined?(@__vc_inline_template_defined) && @__vc_inline_template_defined
13
- raise ViewComponent::ComponentError, "inline templates can only be defined once per-component"
13
+ raise MultipleInlineTemplatesError
14
14
  end
15
15
 
16
16
  if args.size != 1
@@ -80,13 +80,7 @@ module ViewComponent # :nodoc:
80
80
  Dir["#{path}/#{preview_name}_preview/#{example}.html.*"].first
81
81
  end
82
82
 
83
- if preview_path.nil?
84
- raise(
85
- PreviewTemplateError,
86
- "A preview template for example #{example} doesn't exist.\n\n" \
87
- "To fix this issue, create a template for the example."
88
- )
89
- end
83
+ raise MissingPreviewTemplateError.new(example) if preview_path.nil?
90
84
 
91
85
  path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
92
86
  Pathname.new(path)
@@ -12,6 +12,23 @@ module ViewComponent
12
12
  @parent = parent
13
13
  end
14
14
 
15
+ def content?
16
+ return true if defined?(@__vc_content) && @__vc_content.present?
17
+ return true if defined?(@__vc_content_set_by_with_content) && @__vc_content_set_by_with_content.present?
18
+ return true if defined?(@__vc_content_block) && @__vc_content_block.present?
19
+ return false if !__vc_component_instance?
20
+
21
+ @__vc_component_instance.content?
22
+ end
23
+
24
+ def with_content(args)
25
+ if __vc_component_instance?
26
+ @__vc_component_instance.with_content(args)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
15
32
  # Used to render the slot content in the template
16
33
  #
17
34
  # There's currently 3 different values that may be set, that we can render.
@@ -31,22 +48,14 @@ module ViewComponent
31
48
  view_context = @parent.send(:view_context)
32
49
 
33
50
  if defined?(@__vc_content_block) && defined?(@__vc_content_set_by_with_content)
34
- raise ArgumentError.new(
35
- "It looks like a block was provided after calling `with_content` on #{self.class.name}, " \
36
- "which means that ViewComponent doesn't know which content to use.\n\n" \
37
- "To fix this issue, use either `with_content` or a block."
38
- )
51
+ raise DuplicateSlotContentError.new(self.class.name)
39
52
  end
40
53
 
41
54
  @content =
42
- if defined?(@__vc_component_instance)
55
+ if __vc_component_instance?
43
56
  @__vc_component_instance.__vc_original_view_context = @parent.__vc_original_view_context
44
57
 
45
- if defined?(@__vc_content_set_by_with_content)
46
- @__vc_component_instance.with_content(@__vc_content_set_by_with_content)
47
-
48
- @__vc_component_instance.render_in(view_context)
49
- elsif defined?(@__vc_content_block)
58
+ if defined?(@__vc_content_block)
50
59
  # render_in is faster than `parent.render`
51
60
  @__vc_component_instance.render_in(view_context, &@__vc_content_block)
52
61
  else
@@ -92,7 +101,13 @@ module ViewComponent
92
101
  end
93
102
 
94
103
  def respond_to_missing?(symbol, include_all = false)
95
- defined?(@__vc_component_instance) && @__vc_component_instance.respond_to?(symbol, include_all)
104
+ __vc_component_instance? && @__vc_component_instance.respond_to?(symbol, include_all)
105
+ end
106
+
107
+ private
108
+
109
+ def __vc_component_instance?
110
+ defined?(@__vc_component_instance)
96
111
  end
97
112
  end
98
113
  end
@@ -68,6 +68,11 @@ module ViewComponent
68
68
  # <p>Bar</p>
69
69
  # <% end %>
70
70
  # <% end %>
71
+ #
72
+ # Additionally, content can be set by calling `with_SLOT_NAME_content`
73
+ # on the component instance.
74
+ #
75
+ # <%= render_inline(MyComponent.new.with_header_content("Foo")) %>
71
76
  def renders_one(slot_name, callable = nil)
72
77
  validate_singular_slot_name(slot_name)
73
78
 
@@ -76,20 +81,27 @@ module ViewComponent
76
81
  else
77
82
  validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
78
83
 
79
- define_method :"with_#{slot_name}" do |*args, &block|
84
+ setter_method_name = :"with_#{slot_name}"
85
+
86
+ define_method setter_method_name do |*args, &block|
80
87
  set_slot(slot_name, nil, *args, &block)
81
88
  end
82
- ruby2_keywords(:"with_#{slot_name}") if respond_to?(:ruby2_keywords, true)
89
+ ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
83
90
 
84
- define_method slot_name do |*args, &block|
91
+ define_method slot_name do
85
92
  get_slot(slot_name)
86
93
  end
87
- ruby2_keywords(slot_name.to_sym) if respond_to?(:ruby2_keywords, true)
88
94
 
89
95
  define_method "#{slot_name}?" do
90
96
  get_slot(slot_name).present?
91
97
  end
92
98
 
99
+ define_method "with_#{slot_name}_content" do |content|
100
+ send(setter_method_name) { content.to_s }
101
+
102
+ self
103
+ end
104
+
93
105
  register_slot(slot_name, collection: false, callable: callable)
94
106
  end
95
107
  end
@@ -140,18 +152,30 @@ module ViewComponent
140
152
  singular_name = ActiveSupport::Inflector.singularize(slot_name)
141
153
  validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
142
154
 
143
- define_method :"with_#{singular_name}" do |*args, &block|
155
+ setter_method_name = :"with_#{singular_name}"
156
+
157
+ define_method setter_method_name do |*args, &block|
144
158
  set_slot(slot_name, nil, *args, &block)
145
159
  end
146
- ruby2_keywords(:"with_#{singular_name}") if respond_to?(:ruby2_keywords, true)
160
+ ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
161
+
162
+ define_method "with_#{singular_name}_content" do |content|
163
+ send(setter_method_name) { content.to_s }
164
+
165
+ self
166
+ end
147
167
 
148
168
  define_method :"with_#{slot_name}" do |collection_args = nil, &block|
149
169
  collection_args.map do |args|
150
- set_slot(slot_name, nil, **args, &block)
170
+ if args.respond_to?(:to_hash)
171
+ set_slot(slot_name, nil, **args, &block)
172
+ else
173
+ set_slot(slot_name, nil, *args, &block)
174
+ end
151
175
  end
152
176
  end
153
177
 
154
- define_method slot_name do |collection_args = nil, &block|
178
+ define_method slot_name do
155
179
  get_slot(slot_name)
156
180
  end
157
181
 
@@ -206,10 +230,18 @@ module ViewComponent
206
230
  "#{slot_name}_#{poly_type}"
207
231
  end
208
232
 
209
- define_method("with_#{setter_name}") do |*args, &block|
233
+ setter_method_name = :"with_#{setter_name}"
234
+
235
+ define_method(setter_method_name) do |*args, &block|
210
236
  set_polymorphic_slot(slot_name, poly_type, *args, &block)
211
237
  end
212
- ruby2_keywords(:"with_#{setter_name}") if respond_to?(:ruby2_keywords, true)
238
+ ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
239
+
240
+ define_method "with_#{setter_name}_content" do |content|
241
+ send(setter_method_name) { content.to_s }
242
+
243
+ self
244
+ end
213
245
  end
214
246
 
215
247
  registered_slots[slot_name] = {
@@ -244,10 +276,7 @@ module ViewComponent
244
276
  define_method method_name, &callable
245
277
  slot[:renderable_function] = instance_method(method_name)
246
278
  else
247
- raise(
248
- ArgumentError,
249
- "invalid slot definition. Please pass a class, string, or callable (i.e. proc, lambda, etc)"
250
- )
279
+ raise(InvalidSlotDefinitionError)
251
280
  end
252
281
 
253
282
  slot
@@ -255,10 +284,7 @@ module ViewComponent
255
284
 
256
285
  def validate_plural_slot_name(slot_name)
257
286
  if RESERVED_NAMES[:plural].include?(slot_name.to_sym)
258
- raise ArgumentError.new(
259
- "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
260
- "To fix this issue, choose a different name."
261
- )
287
+ raise ReservedPluralSlotNameError.new(name, slot_name)
262
288
  end
263
289
 
264
290
  raise_if_slot_ends_with_question_mark(slot_name)
@@ -267,18 +293,11 @@ module ViewComponent
267
293
 
268
294
  def validate_singular_slot_name(slot_name)
269
295
  if slot_name.to_sym == :content
270
- raise ArgumentError.new(
271
- "#{self} declares a slot named content, which is a reserved word in ViewComponent.\n\n" \
272
- "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" \
273
- "To fix this issue, either use the `content` accessor directly or choose a different slot name."
274
- )
296
+ raise ContentSlotNameError.new(name)
275
297
  end
276
298
 
277
299
  if RESERVED_NAMES[:singular].include?(slot_name.to_sym)
278
- raise ArgumentError.new(
279
- "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
280
- "To fix this issue, choose a different name."
281
- )
300
+ raise ReservedSingularSlotNameError.new(name, slot_name)
282
301
  end
283
302
 
284
303
  raise_if_slot_ends_with_question_mark(slot_name)
@@ -288,22 +307,12 @@ module ViewComponent
288
307
  def raise_if_slot_registered(slot_name)
289
308
  if registered_slots.key?(slot_name)
290
309
  # TODO remove? This breaks overriding slots when slots are inherited
291
- raise ArgumentError.new(
292
- "#{self} declares the #{slot_name} slot multiple times.\n\n" \
293
- "To fix this issue, choose a different slot name."
294
- )
310
+ raise RedefinedSlotError.new(name, slot_name)
295
311
  end
296
312
  end
297
313
 
298
314
  def raise_if_slot_ends_with_question_mark(slot_name)
299
- if slot_name.to_s.ends_with?("?")
300
- raise ArgumentError.new(
301
- "#{self} declares a slot named #{slot_name}, which ends with a question mark.\n\n" \
302
- "This is not allowed because the ViewComponent framework already provides predicate " \
303
- "methods ending in `?`.\n\n" \
304
- "To fix this issue, choose a different name."
305
- )
306
- end
315
+ raise SlotPredicateNameError.new(name, slot_name) if slot_name.to_s.ends_with?("?")
307
316
  end
308
317
  end
309
318
 
@@ -384,7 +393,7 @@ module ViewComponent
384
393
  slot_definition = self.class.registered_slots[slot_name]
385
394
 
386
395
  if !slot_definition[:collection] && (defined?(@__vc_set_slots) && @__vc_set_slots[slot_name])
387
- raise ArgumentError, "content for slot '#{slot_name}' has already been provided"
396
+ raise ContentAlreadySetForPolymorphicSlotError.new(slot_name)
388
397
  end
389
398
 
390
399
  poly_def = slot_definition[:renderable_hash][poly_type]
@@ -97,7 +97,7 @@ module ViewComponent
97
97
  # Capybara assertions to be used. All arguments are forwarded to the block.
98
98
  #
99
99
  # ```ruby
100
- # render_in_view_context(arg1, arg2:) do |arg1, arg2:|
100
+ # render_in_view_context(arg1, arg2: nil) do |arg1, arg2:|
101
101
  # render(MyComponent.new(arg1, arg2))
102
102
  # end
103
103
  #
@@ -81,6 +81,8 @@ module ViewComponent
81
81
  end
82
82
 
83
83
  def translate(key = nil, **options)
84
+ raise ViewComponent::TranslateCalledBeforeRenderError if view_context.nil?
85
+
84
86
  return super unless i18n_backend
85
87
  return key.map { |k| translate(k, **options) } if key.is_a?(Array)
86
88
 
@@ -5,7 +5,7 @@ module ViewComponent
5
5
  MAJOR = 3
6
6
  MINOR = 0
7
7
  PATCH = 0
8
- PRE = "rc5"
8
+ PRE = "rc6"
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
  end
@@ -3,14 +3,9 @@
3
3
  module ViewComponent
4
4
  module WithContentHelper
5
5
  def with_content(value)
6
- if value.nil?
7
- raise ArgumentError.new(
8
- "No content provided to `#with_content` for #{self}.\n\n" \
9
- "To fix this issue, pass a value."
10
- )
11
- else
12
- @__vc_content_set_by_with_content = value
13
- end
6
+ raise NilWithContentError if value.nil?
7
+
8
+ @__vc_content_set_by_with_content = value
14
9
 
15
10
  self
16
11
  end
@@ -16,12 +16,10 @@ module ViewComponent
16
16
  autoload :InlineTemplate
17
17
  autoload :Instrumentation
18
18
  autoload :Preview
19
- autoload :PreviewTemplateError
20
19
  autoload :TestHelpers
21
20
  autoload :SystemTestHelpers
22
21
  autoload :TestCase
23
22
  autoload :SystemTestCase
24
- autoload :TemplateError
25
23
  autoload :Translatable
26
24
  end
27
25
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.rc5
4
+ version: 3.0.0.rc6
5
5
  platform: ruby
6
6
  authors:
7
7
  - ViewComponent Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-16 00:00:00.000000000 Z
11
+ date: 2023-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -90,16 +90,16 @@ dependencies:
90
90
  name: better_html
91
91
  requirement: !ruby/object:Gem::Requirement
92
92
  requirements:
93
- - - "~>"
93
+ - - ">="
94
94
  - !ruby/object:Gem::Version
95
- version: '1'
95
+ version: '0'
96
96
  type: :development
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
99
99
  requirements:
100
- - - "~>"
100
+ - - ">="
101
101
  - !ruby/object:Gem::Version
102
- version: '1'
102
+ version: '0'
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: bundler
105
105
  requirement: !ruby/object:Gem::Requirement
@@ -118,16 +118,16 @@ dependencies:
118
118
  name: erb_lint
119
119
  requirement: !ruby/object:Gem::Requirement
120
120
  requirements:
121
- - - "~>"
121
+ - - ">="
122
122
  - !ruby/object:Gem::Version
123
- version: 0.0.37
123
+ version: '0'
124
124
  type: :development
125
125
  prerelease: false
126
126
  version_requirements: !ruby/object:Gem::Requirement
127
127
  requirements:
128
- - - "~>"
128
+ - - ">="
129
129
  - !ruby/object:Gem::Version
130
- version: 0.0.37
130
+ version: '0'
131
131
  - !ruby/object:Gem::Dependency
132
132
  name: haml
133
133
  requirement: !ruby/object:Gem::Requirement
@@ -212,6 +212,20 @@ dependencies:
212
212
  - - "~>"
213
213
  - !ruby/object:Gem::Version
214
214
  version: '13.0'
215
+ - !ruby/object:Gem::Dependency
216
+ name: rubocop-md
217
+ requirement: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - "~>"
220
+ - !ruby/object:Gem::Version
221
+ version: '1'
222
+ type: :development
223
+ prerelease: false
224
+ version_requirements: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - "~>"
227
+ - !ruby/object:Gem::Version
228
+ version: '1'
215
229
  - !ruby/object:Gem::Dependency
216
230
  name: standard
217
231
  requirement: !ruby/object:Gem::Requirement
@@ -363,10 +377,10 @@ files:
363
377
  - lib/view_component/docs_builder_component.html.erb
364
378
  - lib/view_component/docs_builder_component.rb
365
379
  - lib/view_component/engine.rb
380
+ - lib/view_component/errors.rb
366
381
  - lib/view_component/inline_template.rb
367
382
  - lib/view_component/instrumentation.rb
368
383
  - lib/view_component/preview.rb
369
- - lib/view_component/preview_template_error.rb
370
384
  - lib/view_component/rails/tasks/view_component.rake
371
385
  - lib/view_component/render_component_helper.rb
372
386
  - lib/view_component/render_component_to_string_helper.rb
@@ -378,7 +392,6 @@ files:
378
392
  - lib/view_component/slotable.rb
379
393
  - lib/view_component/system_test_case.rb
380
394
  - lib/view_component/system_test_helpers.rb
381
- - lib/view_component/template_error.rb
382
395
  - lib/view_component/test_case.rb
383
396
  - lib/view_component/test_helpers.rb
384
397
  - lib/view_component/translatable.rb
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ViewComponent
4
- class PreviewTemplateError < StandardError
5
- end
6
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ViewComponent
4
- class TemplateError < StandardError
5
- def initialize(errors)
6
- super(errors.join(", "))
7
- end
8
- end
9
- end