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.
- 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 +299 -5
- 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/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 +52 -59
- data/lib/view_component/collection.rb +18 -3
- data/lib/view_component/compiler.rb +164 -240
- data/lib/view_component/config.rb +26 -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/slotable.rb +28 -14
- 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
|
@@ -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.
|
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.
|
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
|
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
|
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
|