view_component 3.11.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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/view_component/preview_actions.rb +8 -1
- data/app/helpers/preview_helper.rb +1 -1
- data/app/views/view_components/_preview_source.html.erb +1 -1
- data/docs/CHANGELOG.md +322 -2
- data/lib/rails/generators/abstract_generator.rb +9 -1
- data/lib/rails/generators/component/component_generator.rb +2 -1
- data/lib/rails/generators/component/templates/component.rb.tt +3 -2
- data/lib/rails/generators/erb/component_generator.rb +1 -1
- data/lib/rails/generators/preview/templates/component_preview.rb.tt +2 -0
- data/lib/rails/generators/rspec/component_generator.rb +15 -3
- data/lib/rails/generators/rspec/templates/component_spec.rb.tt +1 -1
- data/lib/rails/generators/stimulus/component_generator.rb +8 -3
- data/lib/rails/generators/stimulus/templates/component_controller.ts.tt +9 -0
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +1 -1
- data/lib/view_component/base.rb +54 -59
- data/lib/view_component/collection.rb +18 -3
- data/lib/view_component/compiler.rb +164 -240
- data/lib/view_component/config.rb +39 -2
- data/lib/view_component/configurable.rb +17 -0
- data/lib/view_component/engine.rb +21 -11
- data/lib/view_component/errors.rb +7 -5
- data/lib/view_component/instrumentation.rb +1 -1
- data/lib/view_component/preview.rb +1 -1
- data/lib/view_component/rails/tasks/view_component.rake +8 -2
- data/lib/view_component/slot.rb +11 -1
- data/lib/view_component/slotable.rb +29 -15
- data/lib/view_component/slotable_default.rb +20 -0
- data/lib/view_component/template.rb +134 -0
- data/lib/view_component/test_helpers.rb +29 -2
- data/lib/view_component/use_helpers.rb +32 -10
- data/lib/view_component/version.rb +2 -2
- metadata +112 -19
- data/lib/rails/generators/component/USAGE +0 -13
- data/lib/view_component/docs_builder_component.html.erb +0 -22
- data/lib/view_component/docs_builder_component.rb +0 -96
- data/lib/yard/mattr_accessor_handler.rb +0 -19
data/lib/view_component/base.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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 :
|
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
|
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(
|
462
|
-
filename = File.basename(
|
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 - [
|
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
|
@@ -537,11 +550,13 @@ module ViewComponent
|
|
537
550
|
# Derive the source location of the component Ruby file from the call stack.
|
538
551
|
# We need to ignore `inherited` frames here as they indicate that `inherited`
|
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
|
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
|
541
556
|
|
542
557
|
# If Rails application is loaded, removes the first part of the path and the extension.
|
543
558
|
if defined?(Rails) && Rails.application
|
544
|
-
child.virtual_path = child.
|
559
|
+
child.virtual_path = child.identifier.gsub(
|
545
560
|
/(.*#{Regexp.quote(ViewComponent::Base.config.view_component_path)})|(\.rb)/, ""
|
546
561
|
)
|
547
562
|
end
|
@@ -569,10 +584,6 @@ module ViewComponent
|
|
569
584
|
compile unless compiled?
|
570
585
|
end
|
571
586
|
|
572
|
-
# Compile templates to instance methods, assuming they haven't been compiled already.
|
573
|
-
#
|
574
|
-
# Do as much work as possible in this step, as doing so reduces the amount
|
575
|
-
# of work done each time a component is rendered.
|
576
587
|
# @private
|
577
588
|
def compile(raise_errors: false, force: false)
|
578
589
|
compiler.compile(raise_errors: raise_errors, force: force)
|
@@ -583,22 +594,6 @@ module ViewComponent
|
|
583
594
|
@__vc_compiler ||= Compiler.new(self)
|
584
595
|
end
|
585
596
|
|
586
|
-
# we'll eventually want to update this to support other types
|
587
|
-
# @private
|
588
|
-
def type
|
589
|
-
"text/html"
|
590
|
-
end
|
591
|
-
|
592
|
-
# @private
|
593
|
-
def format
|
594
|
-
:html
|
595
|
-
end
|
596
|
-
|
597
|
-
# @private
|
598
|
-
def identifier
|
599
|
-
source_location
|
600
|
-
end
|
601
|
-
|
602
597
|
# Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
|
603
598
|
#
|
604
599
|
# ```ruby
|
@@ -636,7 +631,7 @@ module ViewComponent
|
|
636
631
|
# validate that the default parameter name
|
637
632
|
# is accepted, as support for collection
|
638
633
|
# rendering is optional.
|
639
|
-
# @private
|
634
|
+
# @private
|
640
635
|
def validate_collection_parameter!(validate_default: false)
|
641
636
|
parameter = validate_default ? collection_parameter : provided_collection_parameter
|
642
637
|
|
@@ -656,7 +651,7 @@ module ViewComponent
|
|
656
651
|
# Ensure the component initializer doesn't define
|
657
652
|
# invalid parameters that could override the framework's
|
658
653
|
# methods.
|
659
|
-
# @private
|
654
|
+
# @private
|
660
655
|
def validate_initialization_parameters!
|
661
656
|
return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
|
662
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
|