view_component 2.35.0 → 2.39.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aecf7b0c1b767aefa852ac1c72ad9a8f9f579289136c15743e24124e240e3f36
4
- data.tar.gz: 9fd3b0e4a7d2411626e4853c4d8bf59d0de2ebf1c09830aa3a8dde78222bcf04
3
+ metadata.gz: e5f28b38a232a62dbe80033fe0ac5a7b0ee717f933afc76a1e991396ff51a8d6
4
+ data.tar.gz: ed6610e4fc417a876aeac464439cc627c5ce6e242a65faa0d7db001e2249035a
5
5
  SHA512:
6
- metadata.gz: 7a6954671a016ccc031bc4a1076640ddc6442a6a4de3f6366b8ef485a3e7cb8977afdca103660a67e5c81d510b9539d7a1d804cf27c750cad2c75945e5a33324
7
- data.tar.gz: b0ce60068522b0f9fa45e8325d79bfde1534d81edcf690c43709f34f5196f48b1c1e70e0303a49d4b9063c6b366fdd2c1e60890e983af3bfa98a678b8529a4c6
6
+ metadata.gz: d7e130233a0699c54a1a9664c9a893afc6df8e45795d397ce6592e2edbd719c61ed6ef9fb373b3dc8d8285bc3a2d712c013579303138fb466806e646c2480465
7
+ data.tar.gz: 25001a6bff37068aceccfb029d5829cd759f4662f7f0b8eb4ab76a42bf0a31ba55f1ceb97480c4a285dc3e9d9da62e746194f623f6b443c647fdb17bb21b3963
@@ -56,7 +56,7 @@ class ViewComponentsController < Rails::ApplicationController # :nodoc:
56
56
  if preview
57
57
  @preview = ViewComponent::Preview.find(preview)
58
58
  else
59
- raise AbstractController::ActionNotFound, "Component preview '#{params[:path]}' not found"
59
+ raise AbstractController::ActionNotFound, "Component preview '#{params[:path]}' not found."
60
60
  end
61
61
  end
62
62
 
data/docs/CHANGELOG.md CHANGED
@@ -7,6 +7,95 @@ title: Changelog
7
7
 
8
8
  ## main
9
9
 
10
+ ## 2.39.0
11
+
12
+ * Clarify documentation of `with_variant` as an override of Action Pack.
13
+
14
+ *Blake Williams*, *Cameron Dutro*, *Joel Hawksley*
15
+
16
+ * Update docs page to be called Javascript and CSS, rename Building ViewComponents to Guide.
17
+
18
+ *Joel Hawksley*
19
+
20
+ * Deprecate `Base#with_variant`.
21
+
22
+ *Cameron Dutro*
23
+
24
+ * Error out in the CI if docs/api.md has to be regenerated.
25
+
26
+ *Dany Marcoux*
27
+
28
+ ## 2.38.0
29
+
30
+ * Add `--stimulus` flag to the component generator. Generates a Stimulus controller alongside the component.
31
+ * Add config option `config.view_component.generate_stimulus_controller` to always generate a Stimulus controller.
32
+
33
+ *Sebastien Auriault*
34
+
35
+ ## 2.37.0
36
+
37
+ * Clarify slots example in docs to reduce naming confusion.
38
+
39
+ *Joel Hawksley*, *Blake Williams*
40
+
41
+ * Fix error in documentation for `render_many` passthrough slots.
42
+
43
+ *Ollie Nye*
44
+
45
+ * Add test case for conflict with internal `@variant` variable.
46
+
47
+ *David Backeus*
48
+
49
+ * Document decision to not change naming convention recommendation to remove `-Component` suffix.
50
+
51
+ *Joel Hawksley*
52
+
53
+ * Fix typo in documentation.
54
+
55
+ *Ryo.gift*
56
+
57
+ * Add inline template example to benchmark script.
58
+
59
+ *Andrew Tait*
60
+
61
+ * Fix benchmark scripts.
62
+
63
+ *Andrew Tait*
64
+
65
+ * Run benchmarks in CI.
66
+
67
+ *Joel Hawksley*
68
+
69
+ ## 2.36.0
70
+
71
+ * Add `slot_type` helper method.
72
+
73
+ *Jon Palmer*
74
+
75
+ * Add test case for rendering a ViewComponent with slots in a controller.
76
+
77
+ *Simon Fish*
78
+
79
+ * Add example ViewComponent to documentation landing page.
80
+
81
+ *Joel Hawksley*
82
+
83
+ * Set maximum line length to 120.
84
+
85
+ *Joel Hawksley*
86
+
87
+ * Setting a collection slot with the plural setter (`component.items(array)` for `renders_many :items`) returns the array of slots.
88
+
89
+ *Jon Palmer*
90
+
91
+ * Update error messages to be more descriptive and helpful.
92
+
93
+ *Joel Hawksley*
94
+
95
+ * Raise an error if the slot name for renders_many is :contents
96
+
97
+ *Simon Fish*
98
+
10
99
  ## 2.35.0
11
100
 
12
101
  * Only load assets for Preview source highlighting if previews are enabled.
@@ -11,13 +11,21 @@ module ViewComponent
11
11
  private
12
12
 
13
13
  def destination
14
+ File.join(destination_directory, "#{destination_file_name}.html.#{engine_name}")
15
+ end
16
+
17
+ def destination_directory
14
18
  if options["sidecar"]
15
- File.join(component_path, class_path, "#{file_name}_component", "#{file_name}_component.html.#{engine_name}")
19
+ File.join(component_path, class_path, destination_file_name)
16
20
  else
17
- File.join(component_path, class_path, "#{file_name}_component.html.#{engine_name}")
21
+ File.join(component_path, class_path)
18
22
  end
19
23
  end
20
24
 
25
+ def destination_file_name
26
+ "#{file_name}_component"
27
+ end
28
+
21
29
  def file_name
22
30
  @_file_name ||= super.sub(/_component\z/i, "")
23
31
  end
@@ -25,5 +33,14 @@ module ViewComponent
25
33
  def component_path
26
34
  ViewComponent::Base.view_component_path
27
35
  end
36
+
37
+ def stimulus_controller
38
+ if options["stimulus"]
39
+ File.join(destination_directory, destination_file_name).
40
+ sub("#{component_path}/", "").
41
+ gsub("_", "-").
42
+ gsub("/", "--")
43
+ end
44
+ end
28
45
  end
29
46
  end
@@ -12,6 +12,8 @@ module Rails
12
12
  argument :attributes, type: :array, default: [], banner: "attribute"
13
13
  check_class_collision suffix: "Component"
14
14
  class_option :inline, type: :boolean, default: false
15
+ class_option :stimulus, type: :boolean, default: ViewComponent::Base.generate_stimulus_controller
16
+ class_option :sidecar, type: :boolean, default: false
15
17
 
16
18
  def create_component_file
17
19
  template "component.rb", File.join(component_path, class_path, "#{file_name}_component.rb")
@@ -21,6 +23,8 @@ module Rails
21
23
 
22
24
  hook_for :preview, type: :boolean
23
25
 
26
+ hook_for :stimulus, type: :boolean
27
+
24
28
  hook_for :template_engine do |instance, template_engine|
25
29
  instance.invoke template_engine, [instance.name]
26
30
  end
@@ -8,7 +8,7 @@ class <%= class_name %>Component < <%= parent_class %>
8
8
  <%- end -%>
9
9
  <%- if initialize_call_method_for_inline? -%>
10
10
  def call
11
- content_tag :h1, "Hello world!"
11
+ content_tag :h1, "Hello world!"<%= ", data: { controller: \"#{stimulus_controller}\" }" if options["stimulus"] %>
12
12
  end
13
13
  <%- end -%>
14
14
 
@@ -11,6 +11,7 @@ module Erb
11
11
  source_root File.expand_path("templates", __dir__)
12
12
  class_option :sidecar, type: :boolean, default: false
13
13
  class_option :inline, type: :boolean, default: false
14
+ class_option :stimulus, type: :boolean, default: false
14
15
 
15
16
  def engine_name
16
17
  "erb"
@@ -19,6 +20,14 @@ module Erb
19
20
  def copy_view_file
20
21
  super
21
22
  end
23
+
24
+ private
25
+
26
+ def data_attributes
27
+ if options["stimulus"]
28
+ " data-controller=\"#{stimulus_controller}\""
29
+ end
30
+ end
22
31
  end
23
32
  end
24
33
  end
@@ -1 +1 @@
1
- <div>Add <%= class_name %> template here</div>
1
+ <div<%= data_attributes %>>Add <%= class_name %> template here</div>
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stimulus
4
+ module Generators
5
+ class ComponentGenerator < ::Rails::Generators::NamedBase
6
+ include ViewComponent::AbstractGenerator
7
+
8
+ source_root File.expand_path("templates", __dir__)
9
+ class_option :sidecar, type: :boolean, default: false
10
+
11
+ def create_stimulus_controller
12
+ template "component_controller.js", destination
13
+ end
14
+
15
+ private
16
+
17
+ def destination
18
+ if options["sidecar"]
19
+ File.join(component_path, class_path, "#{file_name}_component", "#{file_name}_component_controller.js")
20
+ else
21
+ File.join(component_path, class_path, "#{file_name}_component_controller.js")
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ import { Controller } from "stimulus";
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ console.log("Hello, Stimulus!", this.element);
6
+ }
7
+ }
@@ -86,7 +86,11 @@ module ViewComponent
86
86
  @current_template = self
87
87
 
88
88
  if block && defined?(@__vc_content_set_by_with_content)
89
- raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.")
89
+ raise ArgumentError.new(
90
+ "It looks like a block was provided after calling `with_content` on #{self.class.name}, " \
91
+ "which means that ViewComponent doesn't know which content to use.\n\n" \
92
+ "To fix this issue, use either `with_content` or a block."
93
+ )
90
94
  end
91
95
 
92
96
  @__vc_content_evaluated = false
@@ -110,7 +114,8 @@ module ViewComponent
110
114
  ""
111
115
  end
112
116
 
113
- # Called before rendering the component. Override to perform operations that depend on having access to the view context, such as helpers.
117
+ # Called before rendering the component. Override to perform operations that
118
+ # depend on having access to the view context, such as helpers.
114
119
  #
115
120
  # @return [void]
116
121
  def before_render
@@ -150,20 +155,40 @@ module ViewComponent
150
155
  end
151
156
  end
152
157
 
153
- # The current controller. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
158
+ # The current controller. Use sparingly as doing so introduces coupling
159
+ # that inhibits encapsulation & reuse, often making testing difficult.
154
160
  #
155
161
  # @return [ActionController::Base]
156
162
  def controller
157
- raise ViewContextCalledBeforeRenderError, "`controller` can only be called at render time." if view_context.nil?
163
+ if view_context.nil?
164
+ raise(
165
+ ViewContextCalledBeforeRenderError,
166
+ "`#controller` cannot be used during initialization, as it depends " \
167
+ "on the view context that only exists once a ViewComponent is passed to " \
168
+ "the Rails render pipeline.\n\n" \
169
+ "It's sometimes possible to fix this issue by moving code dependent on " \
170
+ "`#controller` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
171
+ )
172
+ end
158
173
 
159
174
  @__vc_controller ||= view_context.controller
160
175
  end
161
176
 
162
- # A proxy through which to access helpers. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
177
+ # A proxy through which to access helpers. Use sparingly as doing so introduces
178
+ # coupling that inhibits encapsulation & reuse, often making testing difficult.
163
179
  #
164
180
  # @return [ActionView::Base]
165
181
  def helpers
166
- raise ViewContextCalledBeforeRenderError, "`helpers` can only be called at render time." if view_context.nil?
182
+ if view_context.nil?
183
+ raise(
184
+ ViewContextCalledBeforeRenderError,
185
+ "`#helpers` cannot be used during initialization, as it depends " \
186
+ "on the view context that only exists once a ViewComponent is passed to " \
187
+ "the Rails render pipeline.\n\n" \
188
+ "It's sometimes possible to fix this issue by moving code dependent on " \
189
+ "`#helpers` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
190
+ )
191
+ end
167
192
 
168
193
  @__vc_helpers ||= controller.view_context
169
194
  end
@@ -193,15 +218,21 @@ module ViewComponent
193
218
 
194
219
  # Use the provided variant instead of the one determined by the current request.
195
220
  #
221
+ # @deprecated Will be removed in v3.0.0.
196
222
  # @param variant [Symbol] The variant to be used by the component.
197
223
  # @return [self]
198
224
  def with_variant(variant)
225
+ ActiveSupport::Deprecation.warn(
226
+ "`with_variant` is deprecated and will be removed in ViewComponent v3.0.0."
227
+ )
228
+
199
229
  @__vc_variant = variant
200
230
 
201
231
  self
202
232
  end
203
233
 
204
- # The current request. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
234
+ # The current request. Use sparingly as doing so introduces coupling that
235
+ # inhibits encapsulation & reuse, often making testing difficult.
205
236
  #
206
237
  # @return [ActionDispatch::Request]
207
238
  def request
@@ -252,6 +283,14 @@ module ViewComponent
252
283
  #
253
284
  mattr_accessor :show_previews_source, instance_writer: false, default: false
254
285
 
286
+ # Always generate a Stimulus controller alongside the component:
287
+ #
288
+ # config.view_component.generate_stimulus_controller = true
289
+ #
290
+ # Defaults to `false`.
291
+ #
292
+ mattr_accessor :generate_stimulus_controller, instance_writer: false, default: false
293
+
255
294
  # Path for component files
256
295
  #
257
296
  # config.view_component.view_component_path = "app/my_components"
@@ -409,13 +448,18 @@ module ViewComponent
409
448
  # the component.
410
449
  if initialize_parameters.empty?
411
450
  raise ArgumentError.new(
412
- "#{self} initializer is empty or invalid."
451
+ "The #{self} initializer is empty or invalid." \
452
+ "It must accept the parameter `#{parameter}` to render it as a collection.\n\n" \
453
+ "To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
454
+ "See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
413
455
  )
414
456
  end
415
457
 
416
458
  raise ArgumentError.new(
417
- "#{self} initializer must accept " \
418
- "`#{parameter}` collection parameter."
459
+ "The initializer for #{self} does not accept the parameter `#{parameter}`, " \
460
+ "which is required in order to render it as a collection.\n\n" \
461
+ "To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
462
+ "See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
419
463
  )
420
464
  end
421
465
 
@@ -427,9 +471,8 @@ module ViewComponent
427
471
  return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
428
472
 
429
473
  raise ViewComponent::ComponentError.new(
430
- "#{self} initializer cannot contain " \
431
- "`#{RESERVED_PARAMETER}` since it will override a " \
432
- "public ViewComponent method."
474
+ "#{self} initializer cannot accept the parameter `#{RESERVED_PARAMETER}`, as it will override a " \
475
+ "public ViewComponent method. To fix this issue, rename the parameter."
433
476
  )
434
477
  end
435
478
 
@@ -32,7 +32,10 @@ module ViewComponent
32
32
  if object.respond_to?(:to_ary)
33
33
  object.to_ary
34
34
  else
35
- raise ArgumentError.new("The value of the argument isn't a valid collection. Make sure it responds to to_ary: #{object.inspect}")
35
+ raise ArgumentError.new(
36
+ "The value of the first argument passed to `with_collection` isn't a valid collection. " \
37
+ "Make sure it responds to `to_ary`."
38
+ )
36
39
  end
37
40
  end
38
41
 
@@ -16,7 +16,10 @@ module ViewComponent
16
16
  subclass_instance_methods = component_class.instance_methods(false)
17
17
 
18
18
  if subclass_instance_methods.include?(:with_content) && raise_errors
19
- raise ViewComponent::ComponentError.new("#{component_class} implements a reserved method, `with_content`.")
19
+ raise ViewComponent::ComponentError.new(
20
+ "#{component_class} implements a reserved method, `#with_content`.\n\n" \
21
+ "To fix this issue, change the name of the method."
22
+ )
20
23
  end
21
24
 
22
25
  if template_errors.present?
@@ -27,7 +30,8 @@ module ViewComponent
27
30
 
28
31
  if subclass_instance_methods.include?(:before_render_check)
29
32
  ActiveSupport::Deprecation.warn(
30
- "`before_render_check` will be removed in v3.0.0. Use `#before_render` instead."
33
+ "`#before_render_check` will be removed in v3.0.0.\n\n" \
34
+ "To fix this issue, use `#before_render` instead."
31
35
  )
32
36
  end
33
37
 
@@ -40,7 +44,10 @@ module ViewComponent
40
44
  # Remove existing compiled template methods,
41
45
  # as Ruby warns when redefining a method.
42
46
  method_name = call_method_name(template[:variant])
43
- component_class.send(:undef_method, method_name.to_sym) if component_class.instance_methods.include?(method_name.to_sym)
47
+
48
+ if component_class.instance_methods.include?(method_name.to_sym)
49
+ component_class.send(:undef_method, method_name.to_sym)
50
+ end
44
51
 
45
52
  component_class.class_eval <<-RUBY, template[:path], -1
46
53
  def #{method_name}
@@ -62,7 +69,9 @@ module ViewComponent
62
69
  attr_reader :component_class
63
70
 
64
71
  def define_render_template_for
65
- component_class.send(:undef_method, :render_template_for) if component_class.instance_methods.include?(:render_template_for)
72
+ if component_class.instance_methods.include?(:render_template_for)
73
+ component_class.send(:undef_method, :render_template_for)
74
+ end
66
75
 
67
76
  variant_elsifs = variants.compact.uniq.map do |variant|
68
77
  "elsif variant.to_sym == :#{variant}\n #{call_method_name(variant)}"
@@ -90,7 +99,9 @@ module ViewComponent
90
99
  end
91
100
 
92
101
  if templates.count { |template| template[:variant].nil? } > 1
93
- errors << "More than one template found for #{component_class}. There can only be one default template file per component."
102
+ errors <<
103
+ "More than one template found for #{component_class}. " \
104
+ "There can only be one default template file per component."
94
105
  end
95
106
 
96
107
  invalid_variants =
@@ -101,11 +112,16 @@ module ViewComponent
101
112
  sort
102
113
 
103
114
  unless invalid_variants.empty?
104
- errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be one template file per variant."
115
+ errors <<
116
+ "More than one template found for #{'variant'.pluralize(invalid_variants.count)} " \
117
+ "#{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. " \
118
+ "There can only be one template file per variant."
105
119
  end
106
120
 
107
121
  if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
108
- errors << "Template file and inline render method found for #{component_class}. There can only be a template file or inline render method per component."
122
+ errors <<
123
+ "Template file and inline render method found for #{component_class}. " \
124
+ "There can only be a template file or inline render method per component."
109
125
  end
110
126
 
111
127
  duplicate_template_file_and_inline_variant_calls =
@@ -114,7 +130,12 @@ module ViewComponent
114
130
  unless duplicate_template_file_and_inline_variant_calls.empty?
115
131
  count = duplicate_template_file_and_inline_variant_calls.count
116
132
 
117
- errors << "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} found for #{'variant'.pluralize(count)} #{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be a template file or inline render method per variant."
133
+ errors <<
134
+ "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} " \
135
+ "found for #{'variant'.pluralize(count)} " \
136
+ "#{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} " \
137
+ "in #{component_class}. " \
138
+ "There can only be a template file or inline render method per variant."
118
139
  end
119
140
 
120
141
  errors
@@ -143,7 +164,10 @@ module ViewComponent
143
164
  # Fetch only ViewComponent ancestor classes to limit the scope of
144
165
  # finding inline calls
145
166
  view_component_ancestors =
146
- component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - component_class.included_modules
167
+ (
168
+ component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } -
169
+ component_class.included_modules
170
+ )
147
171
 
148
172
  view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
149
173
  end
@@ -172,7 +196,13 @@ module ViewComponent
172
196
  if handler.method(:call).parameters.length > 1
173
197
  handler.call(component_class, template)
174
198
  else
175
- handler.call(OpenStruct.new(source: template, identifier: component_class.identifier, type: component_class.type))
199
+ handler.call(
200
+ OpenStruct.new(
201
+ source: template,
202
+ identifier: component_class.identifier,
203
+ type: component_class.type
204
+ )
205
+ )
176
206
  end
177
207
  end
178
208
 
@@ -14,7 +14,11 @@ module ViewComponent
14
14
  # @private
15
15
  def with(area, content = nil, &block)
16
16
  unless content_areas.include?(area)
17
- raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
17
+ raise ArgumentError.new(
18
+ "Unknown content_area '#{area}' for #{self} - expected one of '#{content_areas}'.\n\n" \
19
+ "To fix this issue, add `with_content_area :#{area}` to #{self} or reference " \
20
+ "a valid content area."
21
+ )
18
22
  end
19
23
 
20
24
  if block_given?
@@ -28,12 +32,15 @@ module ViewComponent
28
32
  class_methods do
29
33
  def with_content_areas(*areas)
30
34
  ActiveSupport::Deprecation.warn(
31
- "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
35
+ "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n\n" \
32
36
  "Use slots (https://viewcomponent.org/guide/slots.html) instead."
33
37
  )
34
38
 
35
39
  if areas.include?(:content)
36
- raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
40
+ raise ArgumentError.new(
41
+ "#{self} defines a content area called :content, which is a reserved name. \n\n" \
42
+ "To fix this issue, use another name, such as `:body`."
43
+ )
37
44
  end
38
45
 
39
46
  areas.each do |area|
@@ -110,8 +110,19 @@ module ViewComponent
110
110
  app.routes.prepend do
111
111
  preview_controller = options.preview_controller.sub(/Controller$/, "").underscore
112
112
 
113
- get options.preview_route, to: "#{preview_controller}#index", as: :preview_view_components, internal: true
114
- get "#{options.preview_route}/*path", to: "#{preview_controller}#previews", as: :preview_view_component, internal: true
113
+ get(
114
+ options.preview_route,
115
+ to: "#{preview_controller}#index",
116
+ as: :preview_view_components,
117
+ internal: true
118
+ )
119
+
120
+ get(
121
+ "#{options.preview_route}/*path",
122
+ to: "#{preview_controller}#previews",
123
+ as: :preview_view_component,
124
+ internal: true
125
+ )
115
126
  end
116
127
  end
117
128
 
@@ -9,7 +9,11 @@ module ViewComponent # :nodoc:
9
9
  end
10
10
 
11
11
  def render_in(view_context, &block)
12
- ActiveSupport::Notifications.instrument("!render.view_component", name: self.class.name, identifier: self.class.identifier) do
12
+ ActiveSupport::Notifications.instrument(
13
+ "!render.view_component",
14
+ name: self.class.name,
15
+ identifier: self.class.identifier
16
+ ) do
13
17
  super(view_context, &block)
14
18
  end
15
19
  end
@@ -77,7 +77,11 @@ module ViewComponent # :nodoc:
77
77
  end
78
78
 
79
79
  if preview_path.nil?
80
- raise PreviewTemplateError, "preview template for example #{example} does not exist"
80
+ raise(
81
+ PreviewTemplateError,
82
+ "A preview template for example #{example} does not exist.\n\n" \
83
+ "To fix this issue, create a template for the example."
84
+ )
81
85
  end
82
86
 
83
87
  path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
@@ -30,7 +30,13 @@ module ViewComponent
30
30
 
31
31
  view_context = @parent.send(:view_context)
32
32
 
33
- raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.") if defined?(@__vc_content_block) && defined?(@__vc_content_set_by_with_content)
33
+ 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
+ )
39
+ end
34
40
 
35
41
  @content =
36
42
  if defined?(@__vc_component_instance)
@@ -65,7 +65,7 @@ module ViewComponent
65
65
  # <% end %>
66
66
  # <% end %>
67
67
  def renders_one(slot_name, callable = nil)
68
- validate_slot_name(slot_name)
68
+ validate_singular_slot_name(slot_name)
69
69
 
70
70
  define_method slot_name do |*args, **kwargs, &block|
71
71
  if args.empty? && kwargs.empty? && block.nil?
@@ -116,7 +116,7 @@ module ViewComponent
116
116
  # <% end %>
117
117
  # <% end %>
118
118
  def renders_many(slot_name, callable = nil)
119
- validate_slot_name(slot_name)
119
+ validate_plural_slot_name(slot_name)
120
120
 
121
121
  singular_name = ActiveSupport::Inflector.singularize(slot_name)
122
122
 
@@ -133,7 +133,7 @@ module ViewComponent
133
133
  if collection_args.nil? && block.nil?
134
134
  get_slot(slot_name)
135
135
  else
136
- collection_args.each do |args|
136
+ collection_args.map do |args|
137
137
  set_slot(slot_name, **args, &block)
138
138
  end
139
139
  end
@@ -142,6 +142,17 @@ module ViewComponent
142
142
  register_slot(slot_name, collection: true, callable: callable)
143
143
  end
144
144
 
145
+ def slot_type(slot_name)
146
+ registered_slot = registered_slots[slot_name]
147
+ if registered_slot
148
+ registered_slot[:collection] ? :collection : :single
149
+ else
150
+ plural_slot_name = ActiveSupport::Inflector.pluralize(slot_name).to_sym
151
+ plural_registered_slot = registered_slots[plural_slot_name]
152
+ plural_registered_slot&.fetch(:collection) ? :collection_item : nil
153
+ end
154
+ end
155
+
145
156
  # Clone slot configuration into child class
146
157
  # see #test_slots_pollution
147
158
  def inherited(child)
@@ -174,14 +185,35 @@ module ViewComponent
174
185
  self.registered_slots[slot_name] = slot
175
186
  end
176
187
 
177
- def validate_slot_name(slot_name)
188
+ def validate_plural_slot_name(slot_name)
189
+ if slot_name.to_sym == :contents
190
+ raise ArgumentError.new(
191
+ "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
192
+ "To fix this issue, choose a different name."
193
+ )
194
+ end
195
+
196
+ raise_if_slot_registered(slot_name)
197
+ end
198
+
199
+ def validate_singular_slot_name(slot_name)
178
200
  if slot_name.to_sym == :content
179
- raise ArgumentError.new("#{slot_name} is not a valid slot name.")
201
+ raise ArgumentError.new(
202
+ "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
203
+ "To fix this issue, choose a different name."
204
+ )
180
205
  end
181
206
 
207
+ raise_if_slot_registered(slot_name)
208
+ end
209
+
210
+ def raise_if_slot_registered(slot_name)
182
211
  if self.registered_slots.key?(slot_name)
183
212
  # TODO remove? This breaks overriding slots when slots are inherited
184
- raise ArgumentError.new("#{slot_name} slot declared multiple times")
213
+ raise ArgumentError.new(
214
+ "#{self} declares the #{slot_name} slot multiple times.\n\n" \
215
+ "To fix this issue, choose a different slot name."
216
+ )
185
217
  end
186
218
  end
187
219
  end
@@ -224,7 +256,8 @@ module ViewComponent
224
256
  slot.__vc_component_instance = slot_definition[:renderable].new(*args, **kwargs)
225
257
  # If class name as a string
226
258
  elsif slot_definition[:renderable_class_name]
227
- slot.__vc_component_instance = self.class.const_get(slot_definition[:renderable_class_name]).new(*args, **kwargs)
259
+ slot.__vc_component_instance =
260
+ self.class.const_get(slot_definition[:renderable_class_name]).new(*args, **kwargs)
228
261
  # If passed a lambda
229
262
  elsif slot_definition[:renderable_function]
230
263
  # Use `bind(self)` to ensure lambda is executed in the context of the
@@ -17,7 +17,13 @@ module ViewComponent
17
17
  # We don't have a test case for running an application without capybara installed.
18
18
  # It's probably fine to leave this without coverage.
19
19
  # :nocov:
20
- warn "WARNING in `ViewComponent::TestHelpers`: You must add `capybara` to your Gemfile to use Capybara assertions." if ENV["DEBUG"]
20
+ if ENV["DEBUG"]
21
+ warn(
22
+ "WARNING in `ViewComponent::TestHelpers`: You must add `capybara` " \
23
+ "to your Gemfile to use Capybara assertions."
24
+ )
25
+ end
26
+
21
27
  # :nocov:
22
28
  end
23
29
 
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 35
6
+ MINOR = 39
7
7
  PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
@@ -4,7 +4,10 @@ module ViewComponent
4
4
  module WithContentHelper
5
5
  def with_content(value)
6
6
  if value.nil?
7
- raise ArgumentError.new("No content provided.")
7
+ raise ArgumentError.new(
8
+ "No content provided to `#with_content` for #{self}.\n\n" \
9
+ "To fix this issue, pass a value."
10
+ )
8
11
  else
9
12
  @__vc_content_set_by_with_content = value
10
13
  end
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: 2.35.0
4
+ version: 2.39.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-02 00:00:00.000000000 Z
11
+ date: 2021-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -287,6 +287,8 @@ files:
287
287
  - lib/rails/generators/rspec/templates/component_spec.rb.tt
288
288
  - lib/rails/generators/slim/component_generator.rb
289
289
  - lib/rails/generators/slim/templates/component.html.slim.tt
290
+ - lib/rails/generators/stimulus/component_generator.rb
291
+ - lib/rails/generators/stimulus/templates/component_controller.js.tt
290
292
  - lib/rails/generators/test_unit/component_generator.rb
291
293
  - lib/rails/generators/test_unit/templates/component_test.rb.tt
292
294
  - lib/view_component.rb