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.
- 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 +351 -1
- 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 +55 -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 +31 -2
- data/lib/view_component/use_helpers.rb +32 -10
- data/lib/view_component/version.rb +2 -2
- metadata +252 -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,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
|
-
|
|
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
|
-
|
|
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
|
|
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 :
|
|
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
|
|
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(
|
|
461
|
-
filename = File.basename(
|
|
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 - [
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|