view_component 3.0.0.rc5 → 3.0.0.rc6

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.

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