view_component 2.34.0 → 2.35.0

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.

@@ -5,27 +5,17 @@ require "rails/generators/erb/component_generator"
5
5
  module Haml
6
6
  module Generators
7
7
  class ComponentGenerator < Erb::Generators::ComponentGenerator
8
+ include ViewComponent::AbstractGenerator
9
+
8
10
  source_root File.expand_path("templates", __dir__)
9
11
  class_option :sidecar, type: :boolean, default: false
10
12
 
11
- def copy_view_file
12
- if !options["inline"]
13
- template "component.html.haml", destination
14
- end
15
- end
16
-
17
- private
18
-
19
- def destination
20
- if options["sidecar"]
21
- File.join("app/components", class_path, "#{file_name}_component", "#{file_name}_component.html.haml")
22
- else
23
- File.join("app/components", class_path, "#{file_name}_component.html.haml")
24
- end
13
+ def engine_name
14
+ "haml"
25
15
  end
26
16
 
27
- def file_name
28
- @_file_name ||= super.sub(/_component\z/i, "")
17
+ def copy_view_file
18
+ super
29
19
  end
30
20
  end
31
21
  end
@@ -5,27 +5,17 @@ require "rails/generators/erb/component_generator"
5
5
  module Slim
6
6
  module Generators
7
7
  class ComponentGenerator < Erb::Generators::ComponentGenerator
8
+ include ViewComponent::AbstractGenerator
9
+
8
10
  source_root File.expand_path("templates", __dir__)
9
11
  class_option :sidecar, type: :boolean, default: false
10
12
 
11
- def copy_view_file
12
- if !options["inline"]
13
- template "component.html.slim", destination
14
- end
15
- end
16
-
17
- private
18
-
19
- def destination
20
- if options["sidecar"]
21
- File.join("app/components", class_path, "#{file_name}_component", "#{file_name}_component.html.slim")
22
- else
23
- File.join("app/components", class_path, "#{file_name}_component.html.slim")
24
- end
13
+ def engine_name
14
+ "slim"
25
15
  end
26
16
 
27
- def file_name
28
- @_file_name ||= super.sub(/_component\z/i, "")
17
+ def copy_view_file
18
+ super
29
19
  end
30
20
  end
31
21
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "action_view"
3
4
  require "active_support/dependencies/autoload"
4
5
 
@@ -4,6 +4,7 @@ require "action_view"
4
4
  require "active_support/configurable"
5
5
  require "view_component/collection"
6
6
  require "view_component/compile_cache"
7
+ require "view_component/content_areas"
7
8
  require "view_component/previewable"
8
9
  require "view_component/slotable"
9
10
  require "view_component/slotable_v2"
@@ -12,6 +13,7 @@ require "view_component/with_content_helper"
12
13
  module ViewComponent
13
14
  class Base < ActionView::Base
14
15
  include ActiveSupport::Configurable
16
+ include ViewComponent::ContentAreas
15
17
  include ViewComponent::Previewable
16
18
  include ViewComponent::SlotableV2
17
19
  include ViewComponent::WithContentHelper
@@ -76,22 +78,24 @@ module ViewComponent
76
78
  @virtual_path ||= virtual_path
77
79
 
78
80
  # For template variants (+phone, +desktop, etc.)
79
- @variant ||= @lookup_context.variants.first
81
+ @__vc_variant ||= @lookup_context.variants.first
80
82
 
81
83
  # For caching, such as #cache_if
82
84
  @current_template = nil unless defined?(@current_template)
83
85
  old_current_template = @current_template
84
86
  @current_template = self
85
87
 
86
- raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.") if block && defined?(@_content_set_by_with_content)
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.")
90
+ end
87
91
 
88
- @_content_evaluated = false
89
- @_render_in_block = block
92
+ @__vc_content_evaluated = false
93
+ @__vc_render_in_block = block
90
94
 
91
95
  before_render
92
96
 
93
97
  if render?
94
- render_template_for(@variant).to_s + _output_postamble
98
+ render_template_for(@__vc_variant).to_s + _output_postamble
95
99
  else
96
100
  ""
97
101
  end
@@ -115,7 +119,7 @@ module ViewComponent
115
119
 
116
120
  # Called after rendering the component.
117
121
  #
118
- # @deprecated Use `before_render` instead. Will be removed in v3.0.0.
122
+ # @deprecated Use `#before_render` instead. Will be removed in v3.0.0.
119
123
  # @return [void]
120
124
  def before_render_check
121
125
  # noop
@@ -151,7 +155,8 @@ module ViewComponent
151
155
  # @return [ActionController::Base]
152
156
  def controller
153
157
  raise ViewContextCalledBeforeRenderError, "`controller` can only be called at render time." if view_context.nil?
154
- @controller ||= view_context.controller
158
+
159
+ @__vc_controller ||= view_context.controller
155
160
  end
156
161
 
157
162
  # A proxy through which to access helpers. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
@@ -159,7 +164,8 @@ module ViewComponent
159
164
  # @return [ActionView::Base]
160
165
  def helpers
161
166
  raise ViewContextCalledBeforeRenderError, "`helpers` can only be called at render time." if view_context.nil?
162
- @helpers ||= controller.view_context
167
+
168
+ @__vc_helpers ||= controller.view_context
163
169
  end
164
170
 
165
171
  # Exposes .virtual_path as an instance method
@@ -180,33 +186,17 @@ module ViewComponent
180
186
  # @private
181
187
  def format
182
188
  # Ruby 2.6 throws a warning without checking `defined?`, 2.7 does not
183
- if defined?(@variant)
184
- @variant
189
+ if defined?(@__vc_variant)
190
+ @__vc_variant
185
191
  end
186
192
  end
187
193
 
188
- # Assign the provided content to the content area accessor
189
- #
190
- # @private
191
- def with(area, content = nil, &block)
192
- unless content_areas.include?(area)
193
- raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
194
- end
195
-
196
- if block_given?
197
- content = view_context.capture(&block)
198
- end
199
-
200
- instance_variable_set("@#{area}".to_sym, content)
201
- nil
202
- end
203
-
204
194
  # Use the provided variant instead of the one determined by the current request.
205
195
  #
206
196
  # @param variant [Symbol] The variant to be used by the component.
207
197
  # @return [self]
208
198
  def with_variant(variant)
209
- @variant = variant
199
+ @__vc_variant = variant
210
200
 
211
201
  self
212
202
  end
@@ -223,31 +213,54 @@ module ViewComponent
223
213
  attr_reader :view_context
224
214
 
225
215
  def content
226
- return @_content if defined?(@_content)
227
- @_content_evaluated = true
228
-
229
- @_content = if @view_context && @_render_in_block
230
- view_context.capture(self, &@_render_in_block)
231
- elsif defined?(@_content_set_by_with_content)
232
- @_content_set_by_with_content
233
- end
216
+ @__vc_content_evaluated = true
217
+ return @__vc_content if defined?(@__vc_content)
218
+
219
+ @__vc_content =
220
+ if @view_context && @__vc_render_in_block
221
+ view_context.capture(self, &@__vc_render_in_block)
222
+ elsif defined?(@__vc_content_set_by_with_content)
223
+ @__vc_content_set_by_with_content
224
+ end
234
225
  end
235
226
 
236
227
  def content_evaluated?
237
- @_content_evaluated
228
+ @__vc_content_evaluated
238
229
  end
239
230
 
240
- # The controller used for testing components.
241
- # Defaults to ApplicationController, but can be configured
242
- # on a per-test basis using `with_controller_class`.
243
- # This should be set early in the initialization process and should be a string.
231
+ # Set the controller used for testing components:
232
+ #
233
+ # config.view_component.test_controller = "MyTestController"
234
+ #
235
+ # Defaults to ApplicationController. Can also be configured on a per-test
236
+ # basis using `with_controller_class`.
237
+ #
244
238
  mattr_accessor :test_controller
245
239
  @@test_controller = "ApplicationController"
246
240
 
247
- # Configure if render monkey patches should be included or not in Rails <6.1.
241
+ # Set if render monkey patches should be included or not in Rails <6.1:
242
+ #
243
+ # config.view_component.render_monkey_patch_enabled = false
244
+ #
248
245
  mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
249
246
 
247
+ # Enable or disable source code previews in component previews:
248
+ #
249
+ # config.view_component.show_previews_source = true
250
+ #
251
+ # Defaults to `false`.
252
+ #
253
+ mattr_accessor :show_previews_source, instance_writer: false, default: false
254
+
255
+ # Path for component files
256
+ #
257
+ # config.view_component.view_component_path = "app/my_components"
258
+ #
259
+ # Defaults to "app/components".
260
+ mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
261
+
250
262
  class << self
263
+ # @private
251
264
  attr_accessor :source_location, :virtual_path
252
265
 
253
266
  # EXPERIMENTAL: This API is experimental and may be removed at any time.
@@ -257,6 +270,7 @@ module ViewComponent
257
270
  # strings starting without the "dot", example: `["erb", "haml"]`.
258
271
  #
259
272
  # For example, one might collect sidecar CSS files that need to be compiled.
273
+ # @private TODO: add documentation
260
274
  def _sidecar_files(extensions)
261
275
  return [] unless source_location
262
276
 
@@ -277,11 +291,12 @@ module ViewComponent
277
291
  # end
278
292
  #
279
293
  # Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
280
- nested_component_files = if name.include?("::") && component_name != filename
281
- Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
282
- else
283
- []
284
- end
294
+ nested_component_files =
295
+ if name.include?("::") && component_name != filename
296
+ Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
297
+ else
298
+ []
299
+ end
285
300
 
286
301
  # view files in the same directory as the component
287
302
  sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]
@@ -291,16 +306,24 @@ module ViewComponent
291
306
  (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files).uniq
292
307
  end
293
308
 
294
- # Render a component collection.
309
+ # Render a component for each element in a collection ([documentation](/guide/collections)):
310
+ #
311
+ # render(ProductsComponent.with_collection(@products, foo: :bar))
312
+ #
313
+ # @param collection [Enumerable] A list of items to pass the ViewComponent one at a time.
314
+ # @param args [Arguments] Arguments to pass to the ViewComponent every time.
295
315
  def with_collection(collection, **args)
296
316
  Collection.new(self, collection, **args)
297
317
  end
298
318
 
299
319
  # Provide identifier for ActionView template annotations
320
+ #
321
+ # @private
300
322
  def short_identifier
301
323
  @short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
302
324
  end
303
325
 
326
+ # @private
304
327
  def inherited(child)
305
328
  # Compile so child will inherit compiled `call_*` template methods that
306
329
  # `compile` defines
@@ -318,11 +341,14 @@ module ViewComponent
318
341
  child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].absolute_path
319
342
 
320
343
  # Removes the first part of the path and the extension.
321
- child.virtual_path = child.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
344
+ child.virtual_path = child.source_location.gsub(
345
+ %r{(.*#{Regexp.quote(ViewComponent::Base.view_component_path)})|(\.rb)}, ""
346
+ )
322
347
 
323
348
  super
324
349
  end
325
350
 
351
+ # @private
326
352
  def compiled?
327
353
  compiler.compiled?
328
354
  end
@@ -331,50 +357,39 @@ module ViewComponent
331
357
  #
332
358
  # Do as much work as possible in this step, as doing so reduces the amount
333
359
  # of work done each time a component is rendered.
360
+ # @private
334
361
  def compile(raise_errors: false)
335
362
  compiler.compile(raise_errors: raise_errors)
336
363
  end
337
364
 
365
+ # @private
338
366
  def compiler
339
- @_compiler ||= Compiler.new(self)
367
+ @__vc_compiler ||= Compiler.new(self)
340
368
  end
341
369
 
342
370
  # we'll eventually want to update this to support other types
371
+ # @private
343
372
  def type
344
373
  "text/html"
345
374
  end
346
375
 
376
+ # @private
347
377
  def format
348
378
  :html
349
379
  end
350
380
 
381
+ # @private
351
382
  def identifier
352
383
  source_location
353
384
  end
354
385
 
355
- def with_content_areas(*areas)
356
- ActiveSupport::Deprecation.warn(
357
- "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
358
- "Use slots (https://viewcomponent.org/guide/slots.html) instead."
359
- )
360
-
361
- if areas.include?(:content)
362
- raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
363
- end
364
-
365
- areas.each do |area|
366
- define_method area.to_sym do
367
- content unless content_evaluated? # ensure content is loaded so content_areas will be defined
368
- instance_variable_get(:"@#{area}") if instance_variable_defined?(:"@#{area}")
369
- end
370
- end
371
-
372
- self.content_areas = areas
373
- end
374
-
375
- # Support overriding collection parameter name
376
- def with_collection_parameter(param)
377
- @provided_collection_parameter = param
386
+ # Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
387
+ #
388
+ # with_collection_parameter :item
389
+ #
390
+ # @param parameter [Symbol] The parameter name used when rendering elements of a collection.
391
+ def with_collection_parameter(parameter)
392
+ @provided_collection_parameter = parameter
378
393
  end
379
394
 
380
395
  # Ensure the component initializer accepts the
@@ -382,6 +397,7 @@ module ViewComponent
382
397
  # validate that the default parameter name
383
398
  # is accepted, as support for collection
384
399
  # rendering is optional.
400
+ # @private TODO: add documentation
385
401
  def validate_collection_parameter!(validate_default: false)
386
402
  parameter = validate_default ? collection_parameter : provided_collection_parameter
387
403
 
@@ -406,6 +422,7 @@ module ViewComponent
406
422
  # Ensure the component initializer does not define
407
423
  # invalid parameters that could override the framework's
408
424
  # methods.
425
+ # @private TODO: add documentation
409
426
  def validate_initialization_parameters!
410
427
  return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
411
428
 
@@ -416,6 +433,7 @@ module ViewComponent
416
433
  )
417
434
  end
418
435
 
436
+ # @private
419
437
  def collection_parameter
420
438
  if provided_collection_parameter
421
439
  provided_collection_parameter
@@ -424,18 +442,22 @@ module ViewComponent
424
442
  end
425
443
  end
426
444
 
445
+ # @private
427
446
  def collection_counter_parameter
428
447
  "#{collection_parameter}_counter".to_sym
429
448
  end
430
449
 
450
+ # @private
431
451
  def counter_argument_present?
432
452
  initialize_parameter_names.include?(collection_counter_parameter)
433
453
  end
434
454
 
455
+ # @private
435
456
  def collection_iteration_parameter
436
457
  "#{collection_parameter}_iteration".to_sym
437
458
  end
438
459
 
460
+ # @private
439
461
  def iteration_argument_present?
440
462
  initialize_parameter_names.include?(collection_iteration_parameter)
441
463
  end
@@ -5,6 +5,7 @@ require "action_view/renderer/collection_renderer" if Rails.version.to_f >= 6.1
5
5
  module ViewComponent
6
6
  class Collection
7
7
  attr_reader :component
8
+
8
9
  delegate :format, to: :component
9
10
 
10
11
  def render_in(view_context, &block)
@@ -16,7 +17,7 @@ module ViewComponent
16
17
  content = component.new(**component_options(item, iterator)).render_in(view_context, &block)
17
18
  iterator.iterate!
18
19
  content
19
- end.join.html_safe
20
+ end.join.html_safe # rubocop:disable Rails/OutputSafety
20
21
  end
21
22
 
22
23
  private
@@ -7,6 +7,7 @@ module ViewComponent
7
7
  mattr_accessor :cache, instance_reader: false, instance_accessor: false do
8
8
  Set.new
9
9
  end
10
+
10
11
  module_function
11
12
 
12
13
  def register(klass)