view_component 3.14.0 → 3.15.0
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/docs/CHANGELOG.md +42 -0
- data/lib/rails/generators/component/templates/component.rb.tt +2 -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/test_unit/templates/component_test.rb.tt +1 -1
- data/lib/view_component/base.rb +13 -41
- data/lib/view_component/collection.rb +6 -1
- data/lib/view_component/compiler.rb +163 -243
- data/lib/view_component/config.rb +18 -0
- data/lib/view_component/engine.rb +15 -11
- data/lib/view_component/errors.rb +1 -1
- data/lib/view_component/instrumentation.rb +1 -1
- data/lib/view_component/slotable.rb +26 -8
- data/lib/view_component/template.rb +123 -0
- data/lib/view_component/test_helpers.rb +25 -2
- data/lib/view_component/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25e2163752aa77abea2905cb28191358f69e1f8f473bbfa05db10d89d3fe4543
|
4
|
+
data.tar.gz: 4641471ff3338d4132c409d28a9f4ce5d77677525be98737550288b8b22f3693
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f780668314e90a1f3584e77785d6f514c30996fa6f94dc994c670d96c75df681f368c50a3cb1615cfa0e86224a5f7524afd03e323f4c0a9ed32394c6cd04022a
|
7
|
+
data.tar.gz: 89fa4c33d4fd03f1f63ac1e0dac274379d37cc11ee9fdb250d12ebb377c6aadfeae1d87edc69c1fcf65e800401b4f195d129f4ef20d937c62245064b4ac927be
|
data/docs/CHANGELOG.md
CHANGED
@@ -10,6 +10,48 @@ nav_order: 5
|
|
10
10
|
|
11
11
|
## main
|
12
12
|
|
13
|
+
## 3.15.0
|
14
|
+
|
15
|
+
* Add basic internal testing for memory allocations.
|
16
|
+
|
17
|
+
*Joel Hawksley*
|
18
|
+
|
19
|
+
* Add support for request formats.
|
20
|
+
|
21
|
+
*Joel Hawksley*
|
22
|
+
|
23
|
+
* Add `rendered_json` test helper.
|
24
|
+
|
25
|
+
*Joel Hawksley*
|
26
|
+
|
27
|
+
* Add `with_format` test helper.
|
28
|
+
|
29
|
+
*Joel Hawksley*
|
30
|
+
|
31
|
+
* Warn if using Ruby < 3.2 or Rails < 7.1, which won't be supported by ViewComponent v4, to be released no earlier than April 1, 2025.
|
32
|
+
|
33
|
+
*Joel Hawksley*
|
34
|
+
|
35
|
+
* Add Kicksite to list of companies using ViewComponent.
|
36
|
+
|
37
|
+
*Adil Lari*
|
38
|
+
|
39
|
+
* Allow overridden slot methods to use `super`.
|
40
|
+
|
41
|
+
*Andrew Schwartz*
|
42
|
+
|
43
|
+
* Add Rails engine support to generators.
|
44
|
+
|
45
|
+
*Tomasz Kowalewski*
|
46
|
+
|
47
|
+
* Register stats directories with `Rails::CodeStatistics.register_directory` to support `rails stats` in Rails 8.
|
48
|
+
|
49
|
+
*Petrik de Heus*
|
50
|
+
|
51
|
+
* Fixed type declaration for `ViewComponent::TestHelpers.with_controller_class` parameter.
|
52
|
+
|
53
|
+
*Graham Rogers*
|
54
|
+
|
13
55
|
## 3.14.0
|
14
56
|
|
15
57
|
* Defer to built-in caching for language environment setup, rather than manually using `actions/cache` in CI.
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
<% module_namespacing do -%>
|
3
4
|
class <%= class_name %>Component < <%= parent_class %>
|
4
5
|
<%- if initialize_signature -%>
|
5
6
|
def initialize(<%= initialize_signature %>)
|
@@ -11,5 +12,5 @@ class <%= class_name %>Component < <%= parent_class %>
|
|
11
12
|
content_tag :h1, "Hello world!"<%= ", data: { controller: \"#{stimulus_controller}\" }" if options["stimulus"] %>
|
12
13
|
end
|
13
14
|
<%- end -%>
|
14
|
-
|
15
15
|
end
|
16
|
+
<% end -%>
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require "rails_helper"
|
4
4
|
|
5
|
-
RSpec.describe <%= class_name %>Component, type: :component do
|
5
|
+
RSpec.describe <%= namespaced? ? "#{namespace.name}::" : '' %><%= class_name %>Component, type: :component do
|
6
6
|
pending "add some examples to (or delete) #{__FILE__}"
|
7
7
|
|
8
8
|
# it "renders something useful" do
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require "test_helper"
|
4
4
|
|
5
|
-
class <%= class_name %>ComponentTest < ViewComponent::TestCase
|
5
|
+
class <%= namespaced? ? "#{namespace.name}::" : '' %><%= class_name %>ComponentTest < ViewComponent::TestCase
|
6
6
|
def test_component_renders_something_useful
|
7
7
|
# assert_equal(
|
8
8
|
# %(<span>Hello, components!</span>),
|
data/lib/view_component/base.rb
CHANGED
@@ -11,6 +11,7 @@ require "view_component/inline_template"
|
|
11
11
|
require "view_component/preview"
|
12
12
|
require "view_component/slotable"
|
13
13
|
require "view_component/slotable_default"
|
14
|
+
require "view_component/template"
|
14
15
|
require "view_component/translatable"
|
15
16
|
require "view_component/with_content_helper"
|
16
17
|
require "view_component/use_helpers"
|
@@ -35,6 +36,7 @@ module ViewComponent
|
|
35
36
|
include ViewComponent::WithContentHelper
|
36
37
|
|
37
38
|
RESERVED_PARAMETER = :content
|
39
|
+
VC_INTERNAL_DEFAULT_FORMAT = :html
|
38
40
|
|
39
41
|
# For CSRF authenticity tokens in forms
|
40
42
|
delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
|
@@ -106,9 +108,16 @@ module ViewComponent
|
|
106
108
|
before_render
|
107
109
|
|
108
110
|
if render?
|
109
|
-
|
110
|
-
|
111
|
+
rendered_template =
|
112
|
+
if compiler.renders_template_for?(@__vc_variant, request&.format&.to_sym)
|
113
|
+
render_template_for(@__vc_variant, request&.format&.to_sym)
|
114
|
+
else
|
115
|
+
maybe_escape_html(render_template_for(@__vc_variant, request&.format&.to_sym)) do
|
116
|
+
Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. The output will be automatically escaped, but you may want to investigate.")
|
117
|
+
end
|
118
|
+
end.to_s
|
111
119
|
|
120
|
+
# Avoid allocating new string when output_preamble and output_postamble are blank
|
112
121
|
if output_preamble.blank? && output_postamble.blank?
|
113
122
|
rendered_template
|
114
123
|
else
|
@@ -330,16 +339,6 @@ module ViewComponent
|
|
330
339
|
end
|
331
340
|
end
|
332
341
|
|
333
|
-
def safe_render_template_for(variant)
|
334
|
-
if compiler.renders_template_for_variant?(variant)
|
335
|
-
render_template_for(variant)
|
336
|
-
else
|
337
|
-
maybe_escape_html(render_template_for(variant)) do
|
338
|
-
Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. The output will be automatically escaped, but you may want to investigate.")
|
339
|
-
end
|
340
|
-
end
|
341
|
-
end
|
342
|
-
|
343
342
|
def safe_output_preamble
|
344
343
|
maybe_escape_html(output_preamble) do
|
345
344
|
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.")
|
@@ -500,13 +499,6 @@ module ViewComponent
|
|
500
499
|
Collection.new(self, collection, **args)
|
501
500
|
end
|
502
501
|
|
503
|
-
# Provide identifier for ActionView template annotations
|
504
|
-
#
|
505
|
-
# @private
|
506
|
-
def short_identifier
|
507
|
-
@short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
|
508
|
-
end
|
509
|
-
|
510
502
|
# @private
|
511
503
|
def inherited(child)
|
512
504
|
# Compile so child will inherit compiled `call_*` template methods that
|
@@ -519,12 +511,12 @@ module ViewComponent
|
|
519
511
|
# meaning it will not be called for any children and thus not compile their templates.
|
520
512
|
if !child.instance_methods(false).include?(:render_template_for) && !child.compiled?
|
521
513
|
child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
522
|
-
def render_template_for(variant = nil)
|
514
|
+
def render_template_for(variant = nil, format = nil)
|
523
515
|
# Force compilation here so the compiler always redefines render_template_for.
|
524
516
|
# This is mostly a safeguard to prevent infinite recursion.
|
525
517
|
self.class.compile(raise_errors: true, force: true)
|
526
518
|
# .compile replaces this method; call the new one
|
527
|
-
render_template_for(variant)
|
519
|
+
render_template_for(variant, format)
|
528
520
|
end
|
529
521
|
RUBY
|
530
522
|
end
|
@@ -572,10 +564,6 @@ module ViewComponent
|
|
572
564
|
compile unless compiled?
|
573
565
|
end
|
574
566
|
|
575
|
-
# Compile templates to instance methods, assuming they haven't been compiled already.
|
576
|
-
#
|
577
|
-
# Do as much work as possible in this step, as doing so reduces the amount
|
578
|
-
# of work done each time a component is rendered.
|
579
567
|
# @private
|
580
568
|
def compile(raise_errors: false, force: false)
|
581
569
|
compiler.compile(raise_errors: raise_errors, force: force)
|
@@ -586,22 +574,6 @@ module ViewComponent
|
|
586
574
|
@__vc_compiler ||= Compiler.new(self)
|
587
575
|
end
|
588
576
|
|
589
|
-
# we'll eventually want to update this to support other types
|
590
|
-
# @private
|
591
|
-
def type
|
592
|
-
"text/html"
|
593
|
-
end
|
594
|
-
|
595
|
-
# @private
|
596
|
-
def format
|
597
|
-
:html
|
598
|
-
end
|
599
|
-
|
600
|
-
# @private
|
601
|
-
def identifier
|
602
|
-
source_location
|
603
|
-
end
|
604
|
-
|
605
577
|
# Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
|
606
578
|
#
|
607
579
|
# ```ruby
|
@@ -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
|
@@ -41,6 +40,12 @@ 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
51
|
def initialize(component, object, **options)
|
@@ -4,308 +4,228 @@ require "concurrent-ruby"
|
|
4
4
|
|
5
5
|
module ViewComponent
|
6
6
|
class Compiler
|
7
|
-
# Compiler mode. Can be either:
|
8
|
-
# *
|
7
|
+
# Compiler development mode. Can be either:
|
8
|
+
# * true (a blocking mode which ensures thread safety when redefining the `call` method for components,
|
9
9
|
# default in Rails development and test mode)
|
10
|
-
# *
|
11
|
-
|
12
|
-
PRODUCTION_MODE = :production
|
10
|
+
# * false(a non-blocking mode, default in Rails production mode)
|
11
|
+
class_attribute :development_mode, default: false
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
def initialize(component_class)
|
17
|
-
@component_class = component_class
|
13
|
+
def initialize(component)
|
14
|
+
@component = component
|
18
15
|
@redefinition_lock = Mutex.new
|
19
|
-
@
|
16
|
+
@rendered_templates = Set.new
|
20
17
|
end
|
21
18
|
|
22
19
|
def compiled?
|
23
|
-
CompileCache.compiled?(
|
24
|
-
end
|
25
|
-
|
26
|
-
def development?
|
27
|
-
self.class.mode == DEVELOPMENT_MODE
|
20
|
+
CompileCache.compiled?(@component)
|
28
21
|
end
|
29
22
|
|
30
23
|
def compile(raise_errors: false, force: false)
|
31
24
|
return if compiled? && !force
|
32
|
-
return if
|
33
|
-
|
34
|
-
component_class.superclass.compile(raise_errors: raise_errors) if should_compile_superclass?
|
35
|
-
|
36
|
-
if template_errors.present?
|
37
|
-
raise TemplateError.new(template_errors) if raise_errors
|
25
|
+
return if @component == ViewComponent::Base
|
38
26
|
|
39
|
-
|
40
|
-
end
|
27
|
+
gather_templates
|
41
28
|
|
42
|
-
if
|
43
|
-
|
44
|
-
component_class.validate_collection_parameter!
|
29
|
+
if self.class.development_mode && @templates.any?(&:requires_compiled_superclass?)
|
30
|
+
@component.superclass.compile(raise_errors: raise_errors)
|
45
31
|
end
|
46
32
|
|
47
|
-
if
|
48
|
-
template = component_class.inline_template
|
49
|
-
|
50
|
-
redefinition_lock.synchronize do
|
51
|
-
component_class.silence_redefinition_of_method("call")
|
52
|
-
# rubocop:disable Style/EvalWithLocation
|
53
|
-
component_class.class_eval <<-RUBY, template.path, template.lineno
|
54
|
-
def call
|
55
|
-
#{compiled_inline_template(template)}
|
56
|
-
end
|
57
|
-
RUBY
|
58
|
-
# rubocop:enable Style/EvalWithLocation
|
59
|
-
|
60
|
-
component_class.define_method(:"_call_#{safe_class_name}", component_class.instance_method(:call))
|
33
|
+
return if gather_template_errors(raise_errors).any?
|
61
34
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
_call_#{safe_class_name}
|
66
|
-
end
|
67
|
-
RUBY
|
68
|
-
end
|
69
|
-
else
|
70
|
-
templates.each do |template|
|
71
|
-
method_name = call_method_name(template[:variant])
|
72
|
-
@variants_rendering_templates << template[:variant]
|
73
|
-
|
74
|
-
redefinition_lock.synchronize do
|
75
|
-
component_class.silence_redefinition_of_method(method_name)
|
76
|
-
# rubocop:disable Style/EvalWithLocation
|
77
|
-
component_class.class_eval <<-RUBY, template[:path], 0
|
78
|
-
def #{method_name}
|
79
|
-
#{compiled_template(template[:path])}
|
80
|
-
end
|
81
|
-
RUBY
|
82
|
-
# rubocop:enable Style/EvalWithLocation
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
define_render_template_for
|
35
|
+
if raise_errors
|
36
|
+
@component.validate_initialization_parameters!
|
37
|
+
@component.validate_collection_parameter!
|
87
38
|
end
|
88
39
|
|
89
|
-
|
90
|
-
config[:default_method] = component_class.instance_methods.find { |method_name| method_name == :"default_#{slot_name}" }
|
40
|
+
define_render_template_for
|
91
41
|
|
92
|
-
|
93
|
-
|
42
|
+
@component.register_default_slots
|
43
|
+
@component.build_i18n_backend
|
94
44
|
|
95
|
-
|
96
|
-
|
97
|
-
CompileCache.register(component_class)
|
45
|
+
CompileCache.register(@component)
|
98
46
|
end
|
99
47
|
|
100
|
-
def
|
101
|
-
@
|
48
|
+
def renders_template_for?(variant, format)
|
49
|
+
@rendered_templates.include?([variant, format])
|
102
50
|
end
|
103
51
|
|
104
52
|
private
|
105
53
|
|
106
|
-
attr_reader :
|
54
|
+
attr_reader :templates
|
107
55
|
|
108
56
|
def define_render_template_for
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
end.join("\n")
|
115
|
-
|
116
|
-
component_class.define_method(:"_call_#{safe_class_name}", component_class.instance_method(:call))
|
57
|
+
@templates.each do |template|
|
58
|
+
@redefinition_lock.synchronize do
|
59
|
+
template.compile_to_component
|
60
|
+
end
|
61
|
+
end
|
117
62
|
|
118
|
-
|
119
|
-
if
|
120
|
-
|
121
|
-
|
63
|
+
method_body =
|
64
|
+
if @templates.one?
|
65
|
+
@templates.first.safe_method_name
|
66
|
+
elsif (template = @templates.find(&:inline?))
|
67
|
+
template.safe_method_name
|
122
68
|
else
|
123
|
-
|
69
|
+
branches = []
|
70
|
+
|
71
|
+
@templates.each do |template|
|
72
|
+
conditional =
|
73
|
+
if template.inline_call?
|
74
|
+
"variant&.to_sym == #{template.variant.inspect}"
|
75
|
+
else
|
76
|
+
[
|
77
|
+
template.default_format? ? "(format == #{ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT.inspect} || format.nil?)" : "format == #{template.format.inspect}",
|
78
|
+
template.variant.nil? ? "variant.nil?" : "variant&.to_sym == #{template.variant.inspect}"
|
79
|
+
].join(" && ")
|
80
|
+
end
|
81
|
+
|
82
|
+
branches << [conditional, template.safe_method_name]
|
83
|
+
end
|
84
|
+
|
85
|
+
out = branches.each_with_object(+"") do |(conditional, branch_body), memo|
|
86
|
+
memo << "#{(!memo.present?) ? "if" : "elsif"} #{conditional}\n #{branch_body}\n"
|
87
|
+
end
|
88
|
+
out << "else\n #{templates.find { _1.variant.nil? && _1.default_format? }.safe_method_name}\nend"
|
124
89
|
end
|
125
|
-
RUBY
|
126
90
|
|
127
|
-
redefinition_lock.synchronize do
|
128
|
-
|
129
|
-
|
130
|
-
def render_template_for(variant = nil)
|
131
|
-
#{
|
91
|
+
@redefinition_lock.synchronize do
|
92
|
+
@component.silence_redefinition_of_method(:render_template_for)
|
93
|
+
@component.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
94
|
+
def render_template_for(variant = nil, format = nil)
|
95
|
+
#{method_body}
|
132
96
|
end
|
133
97
|
RUBY
|
134
98
|
end
|
135
99
|
end
|
136
100
|
|
137
|
-
def
|
138
|
-
|
139
|
-
end
|
101
|
+
def gather_template_errors(raise_errors)
|
102
|
+
errors = []
|
140
103
|
|
141
|
-
|
142
|
-
@__vc_template_errors ||=
|
143
|
-
begin
|
144
|
-
errors = []
|
104
|
+
errors << "Couldn't find a template file or inline render method for #{@component}." if @templates.empty?
|
145
105
|
|
146
|
-
|
147
|
-
|
148
|
-
|
106
|
+
# We currently allow components to have both an inline call method and a template for a variant, with the
|
107
|
+
# inline call method overriding the template. We should aim to change this in v4 to instead
|
108
|
+
# raise an error.
|
109
|
+
@templates.reject(&:inline_call?)
|
110
|
+
.map { |template| [template.variant, template.format] }
|
111
|
+
.tally
|
112
|
+
.select { |_, count| count > 1 }
|
113
|
+
.each do |tally|
|
114
|
+
variant, this_format = tally.first
|
149
115
|
|
150
|
-
|
151
|
-
errors <<
|
152
|
-
"More than one template found for #{component_class}. " \
|
153
|
-
"There can only be one default template file per component."
|
154
|
-
end
|
116
|
+
variant_string = " for variant `#{variant}`" if variant.present?
|
155
117
|
|
156
|
-
|
157
|
-
|
158
|
-
.group_by { |template| template[:variant] }
|
159
|
-
.map { |variant, grouped| variant if grouped.length > 1 }
|
160
|
-
.compact
|
161
|
-
.sort
|
162
|
-
|
163
|
-
unless invalid_variants.empty?
|
164
|
-
errors <<
|
165
|
-
"More than one template found for #{"variant".pluralize(invalid_variants.count)} " \
|
166
|
-
"#{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. " \
|
167
|
-
"There can only be one template file per variant."
|
168
|
-
end
|
118
|
+
errors << "More than one #{this_format.upcase} template found#{variant_string} for #{@component}. "
|
119
|
+
end
|
169
120
|
|
170
|
-
|
171
|
-
|
172
|
-
"Template file and inline render method found for #{component_class}. " \
|
173
|
-
"There can only be a template file or inline render method per component."
|
174
|
-
end
|
121
|
+
default_template_types = @templates.each_with_object(Set.new) do |template, memo|
|
122
|
+
next if template.variant
|
175
123
|
|
176
|
-
|
177
|
-
|
124
|
+
memo << :template_file if !template.inline_call?
|
125
|
+
memo << :inline_render if template.inline_call? && template.defined_on_self?
|
178
126
|
|
179
|
-
|
180
|
-
|
127
|
+
memo
|
128
|
+
end
|
181
129
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
"There can only be a template file or inline render method per variant."
|
188
|
-
end
|
130
|
+
if default_template_types.length > 1
|
131
|
+
errors <<
|
132
|
+
"Template file and inline render method found for #{@component}. " \
|
133
|
+
"There can only be a template file or inline render method per component."
|
134
|
+
end
|
189
135
|
|
190
|
-
|
191
|
-
|
136
|
+
# If a template has inline calls, they can conflict with template files the component may use
|
137
|
+
# to render. This attempts to catch and raise that issue before run time. For example,
|
138
|
+
# `def render_mobile` would conflict with a sidecar template of `component.html+mobile.erb`
|
139
|
+
duplicate_template_file_and_inline_call_variants =
|
140
|
+
@templates.reject(&:inline_call?).map(&:variant) &
|
141
|
+
@templates.select { _1.inline_call? && _1.defined_on_self? }.map(&:variant)
|
142
|
+
|
143
|
+
unless duplicate_template_file_and_inline_call_variants.empty?
|
144
|
+
count = duplicate_template_file_and_inline_call_variants.count
|
145
|
+
|
146
|
+
errors <<
|
147
|
+
"Template #{"file".pluralize(count)} and inline render #{"method".pluralize(count)} " \
|
148
|
+
"found for #{"variant".pluralize(count)} " \
|
149
|
+
"#{duplicate_template_file_and_inline_call_variants.map { |v| "'#{v}'" }.to_sentence} " \
|
150
|
+
"in #{@component}. There can only be a template file or inline render method per variant."
|
151
|
+
end
|
192
152
|
|
193
|
-
|
194
|
-
|
195
|
-
|
153
|
+
@templates.select(&:variant).each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |template, memo|
|
154
|
+
memo[template.normalized_variant_name] << template.variant
|
155
|
+
memo
|
156
|
+
end.each do |_, variant_names|
|
157
|
+
next unless variant_names.length > 1
|
196
158
|
|
197
|
-
|
198
|
-
|
199
|
-
"Colliding templates #{colliding_variants.sort.map { |v| "'#{v}'" }.to_sentence} " \
|
200
|
-
"found in #{component_class}."
|
201
|
-
end
|
159
|
+
errors << "Colliding templates #{variant_names.sort.map { |v| "'#{v}'" }.to_sentence} found in #{@component}."
|
160
|
+
end
|
202
161
|
|
203
|
-
|
204
|
-
|
162
|
+
raise TemplateError.new(errors) if errors.any? && raise_errors
|
163
|
+
|
164
|
+
errors
|
205
165
|
end
|
206
166
|
|
207
|
-
def
|
167
|
+
def gather_templates
|
208
168
|
@templates ||=
|
209
169
|
begin
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
170
|
+
templates = @component.sidecar_files(
|
171
|
+
ActionView::Template.template_handler_extensions
|
172
|
+
).map do |path|
|
173
|
+
# Extract format and variant from template filename
|
174
|
+
this_format, variant =
|
175
|
+
File
|
176
|
+
.basename(path) # "variants_component.html+mini.watch.erb"
|
177
|
+
.split(".")[1..-2] # ["html+mini", "watch"]
|
178
|
+
.join(".") # "html+mini.watch"
|
179
|
+
.split("+") # ["html", "mini.watch"]
|
180
|
+
.map(&:to_sym) # [:html, :"mini.watch"]
|
181
|
+
|
182
|
+
out = Template.new(
|
183
|
+
component: @component,
|
184
|
+
type: :file,
|
215
185
|
path: path,
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
def inline_calls
|
224
|
-
@inline_calls ||=
|
225
|
-
begin
|
226
|
-
# Fetch only ViewComponent ancestor classes to limit the scope of
|
227
|
-
# finding inline calls
|
228
|
-
view_component_ancestors =
|
229
|
-
(
|
230
|
-
component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } -
|
231
|
-
component_class.included_modules
|
186
|
+
lineno: 0,
|
187
|
+
extension: path.split(".").last,
|
188
|
+
this_format: this_format,
|
189
|
+
variant: variant
|
232
190
|
)
|
233
191
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
def inline_calls_defined_on_self
|
239
|
-
@inline_calls_defined_on_self ||= component_class.instance_methods(false).grep(/^call(_|$)/)
|
240
|
-
end
|
241
|
-
|
242
|
-
def variants
|
243
|
-
@__vc_variants = (
|
244
|
-
templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
|
245
|
-
).compact.uniq
|
246
|
-
end
|
247
|
-
|
248
|
-
def variants_from_inline_calls(calls)
|
249
|
-
calls.reject { |call| call == :call }.map do |variant_call|
|
250
|
-
variant_call.to_s.sub("call_", "").to_sym
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
def compiled_inline_template(template)
|
255
|
-
handler = ActionView::Template.handler_for_extension(template.language)
|
256
|
-
template = template.source.dup
|
192
|
+
# TODO: We should consider inlining the HTML output safety logic into the compiled render_template_for
|
193
|
+
# instead of this indirect approach
|
194
|
+
@rendered_templates << [out.variant, out.this_format]
|
257
195
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
def compiled_template(file_path)
|
262
|
-
handler = ActionView::Template.handler_for_extension(File.extname(file_path).delete("."))
|
263
|
-
template = File.read(file_path)
|
264
|
-
|
265
|
-
compile_template(template, handler)
|
266
|
-
end
|
267
|
-
|
268
|
-
def compile_template(template, handler)
|
269
|
-
template.rstrip! if component_class.strip_trailing_whitespace?
|
270
|
-
|
271
|
-
if handler.method(:call).parameters.length > 1
|
272
|
-
handler.call(component_class, template)
|
273
|
-
# :nocov:
|
274
|
-
else
|
275
|
-
handler.call(
|
276
|
-
OpenStruct.new(
|
277
|
-
source: template,
|
278
|
-
identifier: component_class.identifier,
|
279
|
-
type: component_class.type
|
280
|
-
)
|
281
|
-
)
|
282
|
-
end
|
283
|
-
# :nocov:
|
284
|
-
end
|
285
|
-
|
286
|
-
def call_method_name(variant)
|
287
|
-
if variant.present? && variants.include?(variant)
|
288
|
-
"call_#{normalized_variant_name(variant)}"
|
289
|
-
else
|
290
|
-
"call"
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
def normalized_variant_name(variant)
|
295
|
-
variant.to_s.gsub("-", "__").gsub(".", "___")
|
296
|
-
end
|
196
|
+
out
|
197
|
+
end
|
297
198
|
|
298
|
-
|
299
|
-
|
300
|
-
|
199
|
+
component_instance_methods_on_self = @component.instance_methods(false)
|
200
|
+
|
201
|
+
(
|
202
|
+
@component.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - @component.included_modules
|
203
|
+
).flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) }
|
204
|
+
.uniq
|
205
|
+
.each do |method_name|
|
206
|
+
templates << Template.new(
|
207
|
+
component: @component,
|
208
|
+
type: :inline_call,
|
209
|
+
this_format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT,
|
210
|
+
variant: method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil,
|
211
|
+
method_name: method_name,
|
212
|
+
defined_on_self: component_instance_methods_on_self.include?(method_name)
|
213
|
+
)
|
214
|
+
end
|
301
215
|
|
302
|
-
|
303
|
-
|
304
|
-
|
216
|
+
if @component.inline_template.present?
|
217
|
+
templates << Template.new(
|
218
|
+
component: @component,
|
219
|
+
type: :inline,
|
220
|
+
path: @component.inline_template.path,
|
221
|
+
lineno: @component.inline_template.lineno,
|
222
|
+
source: @component.inline_template.source.dup,
|
223
|
+
extension: @component.inline_template.language
|
224
|
+
)
|
225
|
+
end
|
305
226
|
|
306
|
-
|
307
|
-
|
308
|
-
component_class.private_instance_methods(false).include?(:call)
|
227
|
+
templates
|
228
|
+
end
|
309
229
|
end
|
310
230
|
end
|
311
231
|
end
|
@@ -168,11 +168,29 @@ module ViewComponent
|
|
168
168
|
# Defaults to `false`.
|
169
169
|
|
170
170
|
def default_preview_paths
|
171
|
+
(default_rails_preview_paths + default_rails_engines_preview_paths).uniq
|
172
|
+
end
|
173
|
+
|
174
|
+
def default_rails_preview_paths
|
171
175
|
return [] unless defined?(Rails.root) && Dir.exist?("#{Rails.root}/test/components/previews")
|
172
176
|
|
173
177
|
["#{Rails.root}/test/components/previews"]
|
174
178
|
end
|
175
179
|
|
180
|
+
def default_rails_engines_preview_paths
|
181
|
+
return [] unless defined?(Rails::Engine)
|
182
|
+
|
183
|
+
registered_rails_engines_with_previews.map do |descendant|
|
184
|
+
"#{descendant.root}/test/components/previews"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def registered_rails_engines_with_previews
|
189
|
+
Rails::Engine.descendants.select do |descendant|
|
190
|
+
defined?(descendant.root) && Dir.exist?("#{descendant.root}/test/components/previews")
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
176
194
|
def default_generate_options
|
177
195
|
options = ActiveSupport::OrderedOptions.new(false)
|
178
196
|
options.preview_path = ""
|
@@ -8,8 +8,16 @@ module ViewComponent
|
|
8
8
|
class Engine < Rails::Engine # :nodoc:
|
9
9
|
config.view_component = ViewComponent::Config.current
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
if Rails.version.to_f < 8.0
|
12
|
+
rake_tasks do
|
13
|
+
load "view_component/rails/tasks/view_component.rake"
|
14
|
+
end
|
15
|
+
else
|
16
|
+
initializer "view_component.stats_directories" do |app|
|
17
|
+
require "rails/code_statistics"
|
18
|
+
dir = ViewComponent::Base.view_component_path
|
19
|
+
Rails::CodeStatistics.register_directory("ViewComponents", dir)
|
20
|
+
end
|
13
21
|
end
|
14
22
|
|
15
23
|
initializer "view_component.set_configs" do |app|
|
@@ -128,11 +136,7 @@ module ViewComponent
|
|
128
136
|
end
|
129
137
|
|
130
138
|
initializer "compiler mode" do |_app|
|
131
|
-
ViewComponent::Compiler.
|
132
|
-
ViewComponent::Compiler::DEVELOPMENT_MODE
|
133
|
-
else
|
134
|
-
ViewComponent::Compiler::PRODUCTION_MODE
|
135
|
-
end
|
139
|
+
ViewComponent::Compiler.development_mode = (Rails.env.development? || Rails.env.test?)
|
136
140
|
end
|
137
141
|
|
138
142
|
config.after_initialize do |app|
|
@@ -165,12 +169,12 @@ module ViewComponent
|
|
165
169
|
end
|
166
170
|
|
167
171
|
# :nocov:
|
168
|
-
if RUBY_VERSION < "3.
|
169
|
-
ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 3.
|
172
|
+
if RUBY_VERSION < "3.2.0"
|
173
|
+
ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 3.2.0", "ViewComponent v4 will remove support for Ruby versions < 3.2.0 no earlier than April 1, 2025")
|
170
174
|
end
|
171
175
|
|
172
|
-
if Rails.version.to_f <
|
173
|
-
ViewComponent::Deprecation.deprecation_warning("Support for Rails versions <
|
176
|
+
if Rails.version.to_f < 7.1
|
177
|
+
ViewComponent::Deprecation.deprecation_warning("Support for Rails versions < 7.1", "ViewComponent v4 will remove support for Rails versions < 7.1 no earlier than April 1, 2025")
|
174
178
|
end
|
175
179
|
# :nocov:
|
176
180
|
|
@@ -89,11 +89,11 @@ module ViewComponent
|
|
89
89
|
end
|
90
90
|
ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
|
91
91
|
|
92
|
-
define_method slot_name do
|
92
|
+
self::GeneratedSlotMethods.define_method slot_name do
|
93
93
|
get_slot(slot_name)
|
94
94
|
end
|
95
95
|
|
96
|
-
define_method :"#{slot_name}?" do
|
96
|
+
self::GeneratedSlotMethods.define_method :"#{slot_name}?" do
|
97
97
|
get_slot(slot_name).present?
|
98
98
|
end
|
99
99
|
|
@@ -176,11 +176,11 @@ module ViewComponent
|
|
176
176
|
end
|
177
177
|
end
|
178
178
|
|
179
|
-
define_method slot_name do
|
179
|
+
self::GeneratedSlotMethods.define_method slot_name do
|
180
180
|
get_slot(slot_name)
|
181
181
|
end
|
182
182
|
|
183
|
-
define_method :"#{slot_name}?" do
|
183
|
+
self::GeneratedSlotMethods.define_method :"#{slot_name}?" do
|
184
184
|
get_slot(slot_name).present?
|
185
185
|
end
|
186
186
|
|
@@ -199,19 +199,28 @@ module ViewComponent
|
|
199
199
|
end
|
200
200
|
end
|
201
201
|
|
202
|
-
# Clone slot configuration into child class
|
203
|
-
# see #test_slots_pollution
|
204
202
|
def inherited(child)
|
203
|
+
# Clone slot configuration into child class
|
204
|
+
# see #test_slots_pollution
|
205
205
|
child.registered_slots = registered_slots.clone
|
206
|
+
|
207
|
+
# Add a module for slot methods, allowing them to be overriden by the component class
|
208
|
+
# see #test_slot_name_can_be_overriden
|
209
|
+
unless child.const_defined?(:GeneratedSlotMethods, false)
|
210
|
+
generated_slot_methods = Module.new
|
211
|
+
child.const_set(:GeneratedSlotMethods, generated_slot_methods)
|
212
|
+
child.include generated_slot_methods
|
213
|
+
end
|
214
|
+
|
206
215
|
super
|
207
216
|
end
|
208
217
|
|
209
218
|
def register_polymorphic_slot(slot_name, types, collection:)
|
210
|
-
define_method(slot_name) do
|
219
|
+
self::GeneratedSlotMethods.define_method(slot_name) do
|
211
220
|
get_slot(slot_name)
|
212
221
|
end
|
213
222
|
|
214
|
-
define_method(:"#{slot_name}?") do
|
223
|
+
self::GeneratedSlotMethods.define_method(:"#{slot_name}?") do
|
215
224
|
get_slot(slot_name).present?
|
216
225
|
end
|
217
226
|
|
@@ -259,6 +268,15 @@ module ViewComponent
|
|
259
268
|
}
|
260
269
|
end
|
261
270
|
|
271
|
+
# Called by the compiler, as instance methods are not defined when slots are first registered
|
272
|
+
def register_default_slots
|
273
|
+
registered_slots.each do |slot_name, config|
|
274
|
+
config[:default_method] = instance_methods.find { |method_name| method_name == :"default_#{slot_name}" }
|
275
|
+
|
276
|
+
registered_slots[slot_name] = config
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
262
280
|
private
|
263
281
|
|
264
282
|
def register_slot(slot_name, **kwargs)
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
class Template
|
5
|
+
attr_reader :variant, :this_format, :type
|
6
|
+
|
7
|
+
def initialize(
|
8
|
+
component:,
|
9
|
+
type:,
|
10
|
+
this_format: nil,
|
11
|
+
variant: nil,
|
12
|
+
lineno: nil,
|
13
|
+
path: nil,
|
14
|
+
extension: nil,
|
15
|
+
source: nil,
|
16
|
+
method_name: nil,
|
17
|
+
defined_on_self: true
|
18
|
+
)
|
19
|
+
@component = component
|
20
|
+
@type = type
|
21
|
+
@this_format = this_format
|
22
|
+
@variant = variant&.to_sym
|
23
|
+
@lineno = lineno
|
24
|
+
@path = path
|
25
|
+
@extension = extension
|
26
|
+
@source = source
|
27
|
+
@method_name = method_name
|
28
|
+
@defined_on_self = defined_on_self
|
29
|
+
|
30
|
+
@source_originally_nil = @source.nil?
|
31
|
+
|
32
|
+
@call_method_name =
|
33
|
+
if @method_name
|
34
|
+
@method_name
|
35
|
+
else
|
36
|
+
out = +"call"
|
37
|
+
out << "_#{normalized_variant_name}" if @variant.present?
|
38
|
+
out << "_#{@this_format}" if @this_format.present? && @this_format != ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT
|
39
|
+
out
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def compile_to_component
|
44
|
+
if !inline_call?
|
45
|
+
@component.silence_redefinition_of_method(@call_method_name)
|
46
|
+
|
47
|
+
# rubocop:disable Style/EvalWithLocation
|
48
|
+
@component.class_eval <<-RUBY, @path, @lineno
|
49
|
+
def #{@call_method_name}
|
50
|
+
#{compiled_source}
|
51
|
+
end
|
52
|
+
RUBY
|
53
|
+
# rubocop:enable Style/EvalWithLocation
|
54
|
+
end
|
55
|
+
|
56
|
+
@component.define_method(safe_method_name, @component.instance_method(@call_method_name))
|
57
|
+
end
|
58
|
+
|
59
|
+
def requires_compiled_superclass?
|
60
|
+
inline_call? && !defined_on_self?
|
61
|
+
end
|
62
|
+
|
63
|
+
def inline_call?
|
64
|
+
@type == :inline_call
|
65
|
+
end
|
66
|
+
|
67
|
+
def inline?
|
68
|
+
@type == :inline
|
69
|
+
end
|
70
|
+
|
71
|
+
def default_format?
|
72
|
+
@this_format == ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT
|
73
|
+
end
|
74
|
+
|
75
|
+
def format
|
76
|
+
@this_format
|
77
|
+
end
|
78
|
+
|
79
|
+
def safe_method_name
|
80
|
+
"_#{@call_method_name}_#{@component.name.underscore.gsub("/", "__")}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def normalized_variant_name
|
84
|
+
@variant.to_s.gsub("-", "__").gsub(".", "___")
|
85
|
+
end
|
86
|
+
|
87
|
+
def defined_on_self?
|
88
|
+
@defined_on_self
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def source
|
94
|
+
if @source_originally_nil
|
95
|
+
# Load file each time we look up #source in case the file has been modified
|
96
|
+
File.read(@path)
|
97
|
+
else
|
98
|
+
@source
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def compiled_source
|
103
|
+
handler = ActionView::Template.handler_for_extension(@extension)
|
104
|
+
this_source = source
|
105
|
+
this_source.rstrip! if @component.strip_trailing_whitespace?
|
106
|
+
|
107
|
+
short_identifier = defined?(Rails.root) ? @path.sub("#{Rails.root}/", "") : @path
|
108
|
+
type = ActionView::Template::Types[@this_format]
|
109
|
+
|
110
|
+
if handler.method(:call).parameters.length > 1
|
111
|
+
handler.call(
|
112
|
+
OpenStruct.new(format: @this_format, identifier: @path, short_identifier: short_identifier, type: type),
|
113
|
+
this_source
|
114
|
+
)
|
115
|
+
# :nocov:
|
116
|
+
# TODO: Remove in v4
|
117
|
+
else
|
118
|
+
handler.call(OpenStruct.new(source: this_source, identifier: @path, type: type))
|
119
|
+
end
|
120
|
+
# :nocov:
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -63,6 +63,16 @@ module ViewComponent
|
|
63
63
|
Nokogiri::HTML.fragment(@rendered_content)
|
64
64
|
end
|
65
65
|
|
66
|
+
# `JSON.parse`-d component output.
|
67
|
+
#
|
68
|
+
# ```ruby
|
69
|
+
# render_inline(MyJsonComponent.new)
|
70
|
+
# assert_equal(rendered_json["hello"], "world")
|
71
|
+
# ```
|
72
|
+
def rendered_json
|
73
|
+
JSON.parse(rendered_content)
|
74
|
+
end
|
75
|
+
|
66
76
|
# Render a preview inline. Internally sets `page` to be a `Capybara::Node::Simple`,
|
67
77
|
# allowing for Capybara assertions to be used:
|
68
78
|
#
|
@@ -145,7 +155,7 @@ module ViewComponent
|
|
145
155
|
# end
|
146
156
|
# ```
|
147
157
|
#
|
148
|
-
# @param klass [ActionController::Base] The controller to be used.
|
158
|
+
# @param klass [Class<ActionController::Base>] The controller to be used.
|
149
159
|
def with_controller_class(klass)
|
150
160
|
old_controller = defined?(@vc_test_controller) && @vc_test_controller
|
151
161
|
|
@@ -155,6 +165,19 @@ module ViewComponent
|
|
155
165
|
@vc_test_controller = old_controller
|
156
166
|
end
|
157
167
|
|
168
|
+
# Set format of the current request
|
169
|
+
#
|
170
|
+
# ```ruby
|
171
|
+
# with_format(:json) do
|
172
|
+
# render_inline(MyComponent.new)
|
173
|
+
# end
|
174
|
+
# ```
|
175
|
+
#
|
176
|
+
# @param format [Symbol] The format to be set for the provided block.
|
177
|
+
def with_format(format)
|
178
|
+
with_request_url("/", format: format) { yield }
|
179
|
+
end
|
180
|
+
|
158
181
|
# Set the URL of the current request (such as when using request-dependent path helpers):
|
159
182
|
#
|
160
183
|
# ```ruby
|
@@ -182,7 +205,7 @@ module ViewComponent
|
|
182
205
|
# @param full_path [String] The path to set for the current request.
|
183
206
|
# @param host [String] The host to set for the current request.
|
184
207
|
# @param method [String] The request method to set for the current request.
|
185
|
-
def with_request_url(full_path, host: nil, method: nil, format:
|
208
|
+
def with_request_url(full_path, host: nil, method: nil, format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT)
|
186
209
|
old_request_host = vc_test_request.host
|
187
210
|
old_request_method = vc_test_request.request_method
|
188
211
|
old_request_path_info = vc_test_request.path_info
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: view_component
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ViewComponent Team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -58,6 +58,20 @@ dependencies:
|
|
58
58
|
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '1.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: allocation_stats
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.1.5
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 0.1.5
|
61
75
|
- !ruby/object:Gem::Dependency
|
62
76
|
name: appraisal
|
63
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -547,6 +561,7 @@ files:
|
|
547
561
|
- lib/view_component/slotable_default.rb
|
548
562
|
- lib/view_component/system_test_case.rb
|
549
563
|
- lib/view_component/system_test_helpers.rb
|
564
|
+
- lib/view_component/template.rb
|
550
565
|
- lib/view_component/test_case.rb
|
551
566
|
- lib/view_component/test_helpers.rb
|
552
567
|
- lib/view_component/translatable.rb
|