view_component 3.10.0 → 3.23.2

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/view_component/preview_actions.rb +8 -1
  3. data/app/helpers/preview_helper.rb +1 -1
  4. data/app/views/view_components/_preview_source.html.erb +1 -1
  5. data/docs/CHANGELOG.md +351 -1
  6. data/lib/rails/generators/abstract_generator.rb +9 -1
  7. data/lib/rails/generators/component/component_generator.rb +2 -1
  8. data/lib/rails/generators/component/templates/component.rb.tt +3 -2
  9. data/lib/rails/generators/erb/component_generator.rb +1 -1
  10. data/lib/rails/generators/preview/templates/component_preview.rb.tt +2 -0
  11. data/lib/rails/generators/rspec/component_generator.rb +15 -3
  12. data/lib/rails/generators/rspec/templates/component_spec.rb.tt +1 -1
  13. data/lib/rails/generators/stimulus/component_generator.rb +8 -3
  14. data/lib/rails/generators/stimulus/templates/component_controller.ts.tt +9 -0
  15. data/lib/rails/generators/test_unit/templates/component_test.rb.tt +1 -1
  16. data/lib/view_component/base.rb +55 -59
  17. data/lib/view_component/collection.rb +18 -3
  18. data/lib/view_component/compiler.rb +164 -240
  19. data/lib/view_component/config.rb +39 -2
  20. data/lib/view_component/configurable.rb +17 -0
  21. data/lib/view_component/engine.rb +21 -11
  22. data/lib/view_component/errors.rb +7 -5
  23. data/lib/view_component/instrumentation.rb +1 -1
  24. data/lib/view_component/preview.rb +1 -1
  25. data/lib/view_component/rails/tasks/view_component.rake +8 -2
  26. data/lib/view_component/slot.rb +11 -1
  27. data/lib/view_component/slotable.rb +29 -15
  28. data/lib/view_component/slotable_default.rb +20 -0
  29. data/lib/view_component/template.rb +134 -0
  30. data/lib/view_component/test_helpers.rb +31 -2
  31. data/lib/view_component/use_helpers.rb +32 -10
  32. data/lib/view_component/version.rb +2 -2
  33. metadata +252 -19
  34. data/lib/rails/generators/component/USAGE +0 -13
  35. data/lib/view_component/docs_builder_component.html.erb +0 -22
  36. data/lib/view_component/docs_builder_component.rb +0 -96
  37. data/lib/yard/mattr_accessor_handler.rb +0 -19
@@ -10,6 +10,8 @@ require "view_component/errors"
10
10
  require "view_component/inline_template"
11
11
  require "view_component/preview"
12
12
  require "view_component/slotable"
13
+ require "view_component/slotable_default"
14
+ require "view_component/template"
13
15
  require "view_component/translatable"
14
16
  require "view_component/with_content_helper"
15
17
  require "view_component/use_helpers"
@@ -23,16 +25,23 @@ module ViewComponent
23
25
  #
24
26
  # @return [ActiveSupport::OrderedOptions]
25
27
  def config
28
+ module_parents.each do |module_parent|
29
+ next unless module_parent.respond_to?(:config)
30
+ module_parent_config = module_parent.config.try(:view_component)
31
+ return module_parent_config if module_parent_config
32
+ end
26
33
  ViewComponent::Config.current
27
34
  end
28
35
  end
29
36
 
30
37
  include ViewComponent::InlineTemplate
38
+ include ViewComponent::UseHelpers
31
39
  include ViewComponent::Slotable
32
40
  include ViewComponent::Translatable
33
41
  include ViewComponent::WithContentHelper
34
42
 
35
43
  RESERVED_PARAMETER = :content
44
+ VC_INTERNAL_DEFAULT_FORMAT = :html
36
45
 
37
46
  # For CSRF authenticity tokens in forms
38
47
  delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
@@ -104,9 +113,9 @@ module ViewComponent
104
113
  before_render
105
114
 
106
115
  if render?
107
- # Avoid allocating new string when output_preamble and output_postamble are blank
108
- rendered_template = safe_render_template_for(@__vc_variant).to_s
116
+ rendered_template = render_template_for(@__vc_variant, __vc_request&.format&.to_sym).to_s
109
117
 
118
+ # Avoid allocating new string when output_preamble and output_postamble are blank
110
119
  if output_preamble.blank? && output_postamble.blank?
111
120
  rendered_template
112
121
  else
@@ -187,6 +196,8 @@ module ViewComponent
187
196
  true
188
197
  end
189
198
 
199
+ # Override the ActionView::Base initializer so that components
200
+ # do not need to define their own initializers.
190
201
  # @private
191
202
  def initialize(*)
192
203
  end
@@ -243,7 +254,7 @@ module ViewComponent
243
254
  raise e, <<~MESSAGE.chomp if view_context && e.is_a?(NameError) && helpers.respond_to?(method_name)
244
255
  #{e.message}
245
256
 
246
- You may be trying to call a method provided as a view helper. Did you mean `helpers.#{method_name}'?
257
+ You may be trying to call a method provided as a view helper. Did you mean `helpers.#{method_name}`?
247
258
  MESSAGE
248
259
 
249
260
  raise
@@ -275,7 +286,14 @@ module ViewComponent
275
286
  #
276
287
  # @return [ActionDispatch::Request]
277
288
  def request
278
- @request ||= controller.request if controller.respond_to?(:request)
289
+ __vc_request
290
+ end
291
+
292
+ # Enables consumers to override request/@request
293
+ #
294
+ # @private
295
+ def __vc_request
296
+ @__vc_request ||= controller.request if controller.respond_to?(:request)
279
297
  end
280
298
 
281
299
  # The content passed to the component instance as a block.
@@ -317,7 +335,7 @@ module ViewComponent
317
335
  end
318
336
 
319
337
  def maybe_escape_html(text)
320
- return text if request && !request.format.html?
338
+ return text if __vc_request && !__vc_request.format.html?
321
339
  return text if text.blank?
322
340
 
323
341
  if text.html_safe?
@@ -328,16 +346,6 @@ module ViewComponent
328
346
  end
329
347
  end
330
348
 
331
- def safe_render_template_for(variant)
332
- if compiler.renders_template_for_variant?(variant)
333
- render_template_for(variant)
334
- else
335
- maybe_escape_html(render_template_for(variant)) do
336
- Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. The output will be automatically escaped, but you may want to investigate.")
337
- end
338
- end
339
- end
340
-
341
349
  def safe_output_preamble
342
350
  maybe_escape_html(output_preamble) do
343
351
  Kernel.warn("WARNING: The #{self.class} component was provided an HTML-unsafe preamble. The preamble will be automatically escaped, but you may want to investigate.")
@@ -350,10 +358,6 @@ module ViewComponent
350
358
  end
351
359
  end
352
360
 
353
- def compiler
354
- @compiler ||= self.class.compiler
355
- end
356
-
357
361
  # Set the controller used for testing components:
358
362
  #
359
363
  # ```ruby
@@ -411,6 +415,14 @@ module ViewComponent
411
415
  # config.view_component.generate.stimulus_controller = true
412
416
  # ```
413
417
  #
418
+ # #### `#typescript`
419
+ #
420
+ # Generate TypeScript files instead of JavaScript files:
421
+ #
422
+ # ```ruby
423
+ # config.view_component.generate.typescript = true
424
+ # ```
425
+ #
414
426
  # #### #locale
415
427
  #
416
428
  # Always generate translations file alongside the component:
@@ -441,8 +453,16 @@ module ViewComponent
441
453
  # Defaults to `false`.
442
454
 
443
455
  class << self
456
+ # The file path of the component Ruby file.
457
+ #
458
+ # @return [String]
459
+ attr_reader :identifier
460
+
461
+ # @private
462
+ attr_writer :identifier
463
+
444
464
  # @private
445
- attr_accessor :source_location, :virtual_path
465
+ attr_accessor :virtual_path
446
466
 
447
467
  # Find sidecar files for the given extensions.
448
468
  #
@@ -452,13 +472,13 @@ module ViewComponent
452
472
  # For example, one might collect sidecar CSS files that need to be compiled.
453
473
  # @param extensions [Array<String>] Extensions of which to return matching sidecar files.
454
474
  def sidecar_files(extensions)
455
- return [] unless source_location
475
+ return [] unless identifier
456
476
 
457
477
  extensions = extensions.join(",")
458
478
 
459
479
  # view files in a directory named like the component
460
- directory = File.dirname(source_location)
461
- filename = File.basename(source_location, ".rb")
480
+ directory = File.dirname(identifier)
481
+ filename = File.basename(identifier, ".rb")
462
482
  component_name = name.demodulize.underscore
463
483
 
464
484
  # Add support for nested components defined in the same file.
@@ -483,7 +503,7 @@ module ViewComponent
483
503
 
484
504
  sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
485
505
 
486
- (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files).uniq
506
+ (sidecar_files - [identifier] + sidecar_directory_files + nested_component_files).uniq
487
507
  end
488
508
 
489
509
  # Render a component for each element in a collection ([documentation](/guide/collections)):
@@ -493,16 +513,10 @@ module ViewComponent
493
513
  # ```
494
514
  #
495
515
  # @param collection [Enumerable] A list of items to pass the ViewComponent one at a time.
516
+ # @param spacer_component [ViewComponent::Base] Component instance to be rendered between items.
496
517
  # @param args [Arguments] Arguments to pass to the ViewComponent every time.
497
- def with_collection(collection, **args)
498
- Collection.new(self, collection, **args)
499
- end
500
-
501
- # Provide identifier for ActionView template annotations
502
- #
503
- # @private
504
- def short_identifier
505
- @short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
518
+ def with_collection(collection, spacer_component: nil, **args)
519
+ Collection.new(self, collection, spacer_component, **args)
506
520
  end
507
521
 
508
522
  # @private
@@ -517,12 +531,12 @@ module ViewComponent
517
531
  # meaning it will not be called for any children and thus not compile their templates.
518
532
  if !child.instance_methods(false).include?(:render_template_for) && !child.compiled?
519
533
  child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
520
- def render_template_for(variant = nil)
534
+ def render_template_for(variant = nil, format = nil)
521
535
  # Force compilation here so the compiler always redefines render_template_for.
522
536
  # This is mostly a safeguard to prevent infinite recursion.
523
537
  self.class.compile(raise_errors: true, force: true)
524
538
  # .compile replaces this method; call the new one
525
- render_template_for(variant)
539
+ render_template_for(variant, format)
526
540
  end
527
541
  RUBY
528
542
  end
@@ -536,11 +550,13 @@ module ViewComponent
536
550
  # Derive the source location of the component Ruby file from the call stack.
537
551
  # We need to ignore `inherited` frames here as they indicate that `inherited`
538
552
  # has been re-defined by the consuming application, likely in ApplicationComponent.
539
- child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].path
553
+ # We use `base_label` method here instead of `label` to avoid cases where the method
554
+ # owner is included in a prefix like `ApplicationComponent.inherited`.
555
+ child.identifier = caller_locations(1, 10).reject { |l| l.base_label == "inherited" }[0].path
540
556
 
541
557
  # If Rails application is loaded, removes the first part of the path and the extension.
542
558
  if defined?(Rails) && Rails.application
543
- child.virtual_path = child.source_location.gsub(
559
+ child.virtual_path = child.identifier.gsub(
544
560
  /(.*#{Regexp.quote(ViewComponent::Base.config.view_component_path)})|(\.rb)/, ""
545
561
  )
546
562
  end
@@ -568,10 +584,6 @@ module ViewComponent
568
584
  compile unless compiled?
569
585
  end
570
586
 
571
- # Compile templates to instance methods, assuming they haven't been compiled already.
572
- #
573
- # Do as much work as possible in this step, as doing so reduces the amount
574
- # of work done each time a component is rendered.
575
587
  # @private
576
588
  def compile(raise_errors: false, force: false)
577
589
  compiler.compile(raise_errors: raise_errors, force: force)
@@ -582,22 +594,6 @@ module ViewComponent
582
594
  @__vc_compiler ||= Compiler.new(self)
583
595
  end
584
596
 
585
- # we'll eventually want to update this to support other types
586
- # @private
587
- def type
588
- "text/html"
589
- end
590
-
591
- # @private
592
- def format
593
- :html
594
- end
595
-
596
- # @private
597
- def identifier
598
- source_location
599
- end
600
-
601
597
  # Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
602
598
  #
603
599
  # ```ruby
@@ -635,7 +631,7 @@ module ViewComponent
635
631
  # validate that the default parameter name
636
632
  # is accepted, as support for collection
637
633
  # rendering is optional.
638
- # @private TODO: add documentation
634
+ # @private
639
635
  def validate_collection_parameter!(validate_default: false)
640
636
  parameter = validate_default ? collection_parameter : provided_collection_parameter
641
637
 
@@ -655,7 +651,7 @@ module ViewComponent
655
651
  # Ensure the component initializer doesn't define
656
652
  # invalid parameters that could override the framework's
657
653
  # methods.
658
- # @private TODO: add documentation
654
+ # @private
659
655
  def validate_initialization_parameters!
660
656
  return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
661
657
 
@@ -7,7 +7,6 @@ module ViewComponent
7
7
  include Enumerable
8
8
  attr_reader :component
9
9
 
10
- delegate :format, to: :component
11
10
  delegate :size, to: :@collection
12
11
 
13
12
  attr_accessor :__vc_original_view_context
@@ -20,7 +19,7 @@ module ViewComponent
20
19
  components.map do |component|
21
20
  component.set_original_view_context(__vc_original_view_context)
22
21
  component.render_in(view_context, &block)
23
- end.join.html_safe
22
+ end.join(rendered_spacer(view_context)).html_safe
24
23
  end
25
24
 
26
25
  def components
@@ -41,11 +40,18 @@ module ViewComponent
41
40
  components.each(&block)
42
41
  end
43
42
 
43
+ # Rails expects us to define `format` on all renderables,
44
+ # but we do not know the `format` of a ViewComponent until runtime.
45
+ def format
46
+ nil
47
+ end
48
+
44
49
  private
45
50
 
46
- def initialize(component, object, **options)
51
+ def initialize(component, object, spacer_component, **options)
47
52
  @component = component
48
53
  @collection = collection_variable(object || [])
54
+ @spacer_component = spacer_component
49
55
  @options = options
50
56
  end
51
57
 
@@ -64,5 +70,14 @@ module ViewComponent
64
70
 
65
71
  @options.merge(item_options)
66
72
  end
73
+
74
+ def rendered_spacer(view_context)
75
+ if @spacer_component
76
+ @spacer_component.set_original_view_context(__vc_original_view_context)
77
+ @spacer_component.render_in(view_context)
78
+ else
79
+ ""
80
+ end
81
+ end
67
82
  end
68
83
  end