view_component 3.12.1 → 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 (35) 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 +299 -5
  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/templates/component_spec.rb.tt +1 -1
  12. data/lib/rails/generators/stimulus/component_generator.rb +8 -3
  13. data/lib/rails/generators/stimulus/templates/component_controller.ts.tt +9 -0
  14. data/lib/rails/generators/test_unit/templates/component_test.rb.tt +1 -1
  15. data/lib/view_component/base.rb +52 -59
  16. data/lib/view_component/collection.rb +18 -3
  17. data/lib/view_component/compiler.rb +164 -240
  18. data/lib/view_component/config.rb +26 -2
  19. data/lib/view_component/configurable.rb +17 -0
  20. data/lib/view_component/engine.rb +21 -11
  21. data/lib/view_component/errors.rb +7 -5
  22. data/lib/view_component/instrumentation.rb +1 -1
  23. data/lib/view_component/preview.rb +1 -1
  24. data/lib/view_component/rails/tasks/view_component.rake +8 -2
  25. data/lib/view_component/slotable.rb +28 -14
  26. data/lib/view_component/slotable_default.rb +20 -0
  27. data/lib/view_component/template.rb +134 -0
  28. data/lib/view_component/test_helpers.rb +29 -2
  29. data/lib/view_component/use_helpers.rb +32 -10
  30. data/lib/view_component/version.rb +2 -2
  31. metadata +112 -19
  32. data/lib/rails/generators/component/USAGE +0 -13
  33. data/lib/view_component/docs_builder_component.html.erb +0 -22
  34. data/lib/view_component/docs_builder_component.rb +0 -96
  35. 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,6 +25,11 @@ 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
@@ -34,6 +41,7 @@ module ViewComponent
34
41
  include ViewComponent::WithContentHelper
35
42
 
36
43
  RESERVED_PARAMETER = :content
44
+ VC_INTERNAL_DEFAULT_FORMAT = :html
37
45
 
38
46
  # For CSRF authenticity tokens in forms
39
47
  delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
@@ -105,9 +113,9 @@ module ViewComponent
105
113
  before_render
106
114
 
107
115
  if render?
108
- # Avoid allocating new string when output_preamble and output_postamble are blank
109
- 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
110
117
 
118
+ # Avoid allocating new string when output_preamble and output_postamble are blank
111
119
  if output_preamble.blank? && output_postamble.blank?
112
120
  rendered_template
113
121
  else
@@ -188,6 +196,8 @@ module ViewComponent
188
196
  true
189
197
  end
190
198
 
199
+ # Override the ActionView::Base initializer so that components
200
+ # do not need to define their own initializers.
191
201
  # @private
192
202
  def initialize(*)
193
203
  end
@@ -244,7 +254,7 @@ module ViewComponent
244
254
  raise e, <<~MESSAGE.chomp if view_context && e.is_a?(NameError) && helpers.respond_to?(method_name)
245
255
  #{e.message}
246
256
 
247
- 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}`?
248
258
  MESSAGE
249
259
 
250
260
  raise
@@ -276,7 +286,14 @@ module ViewComponent
276
286
  #
277
287
  # @return [ActionDispatch::Request]
278
288
  def request
279
- @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)
280
297
  end
281
298
 
282
299
  # The content passed to the component instance as a block.
@@ -318,7 +335,7 @@ module ViewComponent
318
335
  end
319
336
 
320
337
  def maybe_escape_html(text)
321
- return text if request && !request.format.html?
338
+ return text if __vc_request && !__vc_request.format.html?
322
339
  return text if text.blank?
323
340
 
324
341
  if text.html_safe?
@@ -329,16 +346,6 @@ module ViewComponent
329
346
  end
330
347
  end
331
348
 
332
- def safe_render_template_for(variant)
333
- if compiler.renders_template_for_variant?(variant)
334
- render_template_for(variant)
335
- else
336
- maybe_escape_html(render_template_for(variant)) do
337
- Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. The output will be automatically escaped, but you may want to investigate.")
338
- end
339
- end
340
- end
341
-
342
349
  def safe_output_preamble
343
350
  maybe_escape_html(output_preamble) do
344
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.")
@@ -351,10 +358,6 @@ module ViewComponent
351
358
  end
352
359
  end
353
360
 
354
- def compiler
355
- @compiler ||= self.class.compiler
356
- end
357
-
358
361
  # Set the controller used for testing components:
359
362
  #
360
363
  # ```ruby
@@ -412,6 +415,14 @@ module ViewComponent
412
415
  # config.view_component.generate.stimulus_controller = true
413
416
  # ```
414
417
  #
418
+ # #### `#typescript`
419
+ #
420
+ # Generate TypeScript files instead of JavaScript files:
421
+ #
422
+ # ```ruby
423
+ # config.view_component.generate.typescript = true
424
+ # ```
425
+ #
415
426
  # #### #locale
416
427
  #
417
428
  # Always generate translations file alongside the component:
@@ -442,8 +453,16 @@ module ViewComponent
442
453
  # Defaults to `false`.
443
454
 
444
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
+
445
464
  # @private
446
- attr_accessor :source_location, :virtual_path
465
+ attr_accessor :virtual_path
447
466
 
448
467
  # Find sidecar files for the given extensions.
449
468
  #
@@ -453,13 +472,13 @@ module ViewComponent
453
472
  # For example, one might collect sidecar CSS files that need to be compiled.
454
473
  # @param extensions [Array<String>] Extensions of which to return matching sidecar files.
455
474
  def sidecar_files(extensions)
456
- return [] unless source_location
475
+ return [] unless identifier
457
476
 
458
477
  extensions = extensions.join(",")
459
478
 
460
479
  # view files in a directory named like the component
461
- directory = File.dirname(source_location)
462
- filename = File.basename(source_location, ".rb")
480
+ directory = File.dirname(identifier)
481
+ filename = File.basename(identifier, ".rb")
463
482
  component_name = name.demodulize.underscore
464
483
 
465
484
  # Add support for nested components defined in the same file.
@@ -484,7 +503,7 @@ module ViewComponent
484
503
 
485
504
  sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
486
505
 
487
- (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files).uniq
506
+ (sidecar_files - [identifier] + sidecar_directory_files + nested_component_files).uniq
488
507
  end
489
508
 
490
509
  # Render a component for each element in a collection ([documentation](/guide/collections)):
@@ -494,16 +513,10 @@ module ViewComponent
494
513
  # ```
495
514
  #
496
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.
497
517
  # @param args [Arguments] Arguments to pass to the ViewComponent every time.
498
- def with_collection(collection, **args)
499
- Collection.new(self, collection, **args)
500
- end
501
-
502
- # Provide identifier for ActionView template annotations
503
- #
504
- # @private
505
- def short_identifier
506
- @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)
507
520
  end
508
521
 
509
522
  # @private
@@ -518,12 +531,12 @@ module ViewComponent
518
531
  # meaning it will not be called for any children and thus not compile their templates.
519
532
  if !child.instance_methods(false).include?(:render_template_for) && !child.compiled?
520
533
  child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
521
- def render_template_for(variant = nil)
534
+ def render_template_for(variant = nil, format = nil)
522
535
  # Force compilation here so the compiler always redefines render_template_for.
523
536
  # This is mostly a safeguard to prevent infinite recursion.
524
537
  self.class.compile(raise_errors: true, force: true)
525
538
  # .compile replaces this method; call the new one
526
- render_template_for(variant)
539
+ render_template_for(variant, format)
527
540
  end
528
541
  RUBY
529
542
  end
@@ -539,11 +552,11 @@ module ViewComponent
539
552
  # has been re-defined by the consuming application, likely in ApplicationComponent.
540
553
  # We use `base_label` method here instead of `label` to avoid cases where the method
541
554
  # owner is included in a prefix like `ApplicationComponent.inherited`.
542
- child.source_location = caller_locations(1, 10).reject { |l| l.base_label == "inherited" }[0].path
555
+ child.identifier = caller_locations(1, 10).reject { |l| l.base_label == "inherited" }[0].path
543
556
 
544
557
  # If Rails application is loaded, removes the first part of the path and the extension.
545
558
  if defined?(Rails) && Rails.application
546
- child.virtual_path = child.source_location.gsub(
559
+ child.virtual_path = child.identifier.gsub(
547
560
  /(.*#{Regexp.quote(ViewComponent::Base.config.view_component_path)})|(\.rb)/, ""
548
561
  )
549
562
  end
@@ -571,10 +584,6 @@ module ViewComponent
571
584
  compile unless compiled?
572
585
  end
573
586
 
574
- # Compile templates to instance methods, assuming they haven't been compiled already.
575
- #
576
- # Do as much work as possible in this step, as doing so reduces the amount
577
- # of work done each time a component is rendered.
578
587
  # @private
579
588
  def compile(raise_errors: false, force: false)
580
589
  compiler.compile(raise_errors: raise_errors, force: force)
@@ -585,22 +594,6 @@ module ViewComponent
585
594
  @__vc_compiler ||= Compiler.new(self)
586
595
  end
587
596
 
588
- # we'll eventually want to update this to support other types
589
- # @private
590
- def type
591
- "text/html"
592
- end
593
-
594
- # @private
595
- def format
596
- :html
597
- end
598
-
599
- # @private
600
- def identifier
601
- source_location
602
- end
603
-
604
597
  # Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
605
598
  #
606
599
  # ```ruby
@@ -638,7 +631,7 @@ module ViewComponent
638
631
  # validate that the default parameter name
639
632
  # is accepted, as support for collection
640
633
  # rendering is optional.
641
- # @private TODO: add documentation
634
+ # @private
642
635
  def validate_collection_parameter!(validate_default: false)
643
636
  parameter = validate_default ? collection_parameter : provided_collection_parameter
644
637
 
@@ -658,7 +651,7 @@ module ViewComponent
658
651
  # Ensure the component initializer doesn't define
659
652
  # invalid parameters that could override the framework's
660
653
  # methods.
661
- # @private TODO: add documentation
654
+ # @private
662
655
  def validate_initialization_parameters!
663
656
  return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
664
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