view_component 4.12.0 → 4.13.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 +46 -0
- data/lib/view_component/active_job_serializer.rb +23 -0
- data/lib/view_component/base.rb +9 -7
- data/lib/view_component/collection.rb +4 -3
- data/lib/view_component/compiler.rb +9 -9
- data/lib/view_component/engine.rb +7 -0
- data/lib/view_component/errors.rb +22 -22
- data/lib/view_component/instrumentation.rb +1 -1
- data/lib/view_component/request_details.rb +3 -1
- data/lib/view_component/serializable/proxy.rb +110 -0
- data/lib/view_component/serializable.rb +27 -0
- data/lib/view_component/template.rb +33 -32
- data/lib/view_component/version.rb +2 -4
- data/lib/view_component.rb +1 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aaf81704d1bb151178b288ef446a657e2d8ac0158768ffee8db3c3e2c530aa29
|
|
4
|
+
data.tar.gz: 4e2192552574c18ff0dfc4613d797204abe98cd26b6343712c7edb7a4cba65bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c06fd1d045b7c78a32cc66d1db448df80b1c2543d6cf9e44ab5bfa4e92c5cfe3d6151f80b24b345168e62dde6b8b400dfb7c28006716494b9e33366376f8cb9d
|
|
7
|
+
data.tar.gz: ddf8a61600fb66a0e2ae8ce4095e1d9398355ff2c5b19a16e79d8adeaff3da896dc4ecd948c4a13eaac8683df811fb0b578147486e7bfc609773f41532964466
|
data/docs/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,52 @@ nav_order: 6
|
|
|
10
10
|
|
|
11
11
|
## main
|
|
12
12
|
|
|
13
|
+
## 4.13.0
|
|
14
|
+
|
|
15
|
+
* Add support for Turbo-streaming ViewComponents.
|
|
16
|
+
|
|
17
|
+
*Ben Sheldon*, *Joel Hawksley*
|
|
18
|
+
|
|
19
|
+
* Reduce allocations and avoid redundant compiler work when rendering components and collections.
|
|
20
|
+
|
|
21
|
+
*Joel Hawksley*
|
|
22
|
+
|
|
23
|
+
* Stabilize rendering allocation tests with explicit warmups and exact expectations by Rails and Ruby.
|
|
24
|
+
|
|
25
|
+
*Joel Hawksley*
|
|
26
|
+
|
|
27
|
+
* Replace the custom memory allocation test helper with `minitest-memory`, preserving Ruby-version-specific allocation thresholds while improving failure diagnostics.
|
|
28
|
+
|
|
29
|
+
*Joel Hawksley*
|
|
30
|
+
|
|
31
|
+
* Add zizmor security analysis for GitHub Actions workflows to CI.
|
|
32
|
+
|
|
33
|
+
*Joel Hawksley*
|
|
34
|
+
|
|
35
|
+
* Remove the `$PROGRAM_NAME` version-printing line from `version.rb` so the file no longer reads a global variable (unshareable across Ractors). The version is still available via `ViewComponent::VERSION::STRING`.
|
|
36
|
+
|
|
37
|
+
*Joel Hawksley*
|
|
38
|
+
|
|
39
|
+
* Fix intermittent template compilation failures where line-number offsets and annotation stripping were decided when a template object was created instead of when it was compiled, so later changes to coverage or annotation settings produced off-by-one backtraces or blank output.
|
|
40
|
+
|
|
41
|
+
*Joel Hawksley*
|
|
42
|
+
|
|
43
|
+
* Freeze `ViewComponent::VERSION::STRING` so the version constant is immutable and Ractor-shareable.
|
|
44
|
+
|
|
45
|
+
*Joel Hawksley*
|
|
46
|
+
|
|
47
|
+
* Add [audition](https://github.com/yaroslav/audition) Ractor-readiness checks to CI. Applied safe `.freeze` auto-fixes to string constants in `ViewComponent::Errors` and baselined existing findings so the gate fails only on new Ractor-isolation violations.
|
|
48
|
+
|
|
49
|
+
*Joel Hawksley*
|
|
50
|
+
|
|
51
|
+
* Update link to GOV.UK Components library in resources list to govuk-components.x-govuk.org
|
|
52
|
+
|
|
53
|
+
*Peter Yates*
|
|
54
|
+
|
|
55
|
+
* Fix `NoMethodError: undefined method 'template_handler_extensions'` when gathering sidecar templates on Rails main. Action View removed the `ActionView::Template.template_handler_extensions` method in newer versions; the compiler now reads the registered extensions through `ActionView::Template::Handlers.extensions`, which is the supported read-path API across all supported Rails versions (7.1+).
|
|
56
|
+
|
|
57
|
+
*Luiz Kowalski*
|
|
58
|
+
|
|
13
59
|
## 4.12.0
|
|
14
60
|
|
|
15
61
|
* Fix stale render context on reused component instances. A `ViewComponent::Base` instance memoized its controller, helpers, request, view context, lookup context, view flow, and requested format details on first render via `||=`. Rendering the same instance a second time (intentionally or via aliasing) reused that stale context, which could leak data across requests, sessions, or users. `#render_in` now resets these ivars on every call so each render derives its context from the current view.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job/serializers"
|
|
4
|
+
|
|
5
|
+
module ViewComponent
|
|
6
|
+
class ActiveJobSerializer < ActiveJob::Serializers::ObjectSerializer
|
|
7
|
+
def klass
|
|
8
|
+
ViewComponent::Serializable::Proxy
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def serialize?(argument)
|
|
12
|
+
argument.is_a?(ViewComponent::Serializable::Proxy)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def serialize(proxy)
|
|
16
|
+
super(proxy.serialize)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def deserialize(hash)
|
|
20
|
+
ViewComponent::Serializable::Proxy.deserialize(hash)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/view_component/base.rb
CHANGED
|
@@ -103,8 +103,8 @@ module ViewComponent
|
|
|
103
103
|
# Returns HTML that has been escaped by the respective template handler.
|
|
104
104
|
#
|
|
105
105
|
# @return [String]
|
|
106
|
-
def render_in(view_context,
|
|
107
|
-
self.class.__vc_compile(raise_errors: true)
|
|
106
|
+
def render_in(view_context, **, &block)
|
|
107
|
+
self.class.__vc_compile(raise_errors: true) unless self.class.__vc_compiled?
|
|
108
108
|
|
|
109
109
|
__vc_reset_render_state!
|
|
110
110
|
|
|
@@ -469,12 +469,14 @@ module ViewComponent
|
|
|
469
469
|
# `@__vc_content_set_by_with_content`) is intentionally preserved because it
|
|
470
470
|
# is populated by callers _before_ `render_in` runs (e.g. via `with_*`
|
|
471
471
|
# slot setters or `with_content`).
|
|
472
|
+
RENDER_STATE_IVARS = %i[
|
|
473
|
+
@__vc_controller
|
|
474
|
+
@__vc_helpers
|
|
475
|
+
@__vc_request
|
|
476
|
+
].freeze
|
|
477
|
+
|
|
472
478
|
def __vc_reset_render_state!
|
|
473
|
-
|
|
474
|
-
@__vc_controller
|
|
475
|
-
@__vc_helpers
|
|
476
|
-
@__vc_request
|
|
477
|
-
].each do |ivar|
|
|
479
|
+
RENDER_STATE_IVARS.each do |ivar|
|
|
478
480
|
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
|
|
479
481
|
end
|
|
480
482
|
end
|
|
@@ -13,7 +13,7 @@ module ViewComponent
|
|
|
13
13
|
delegate :size, to: :@collection
|
|
14
14
|
|
|
15
15
|
def render_in(view_context, **_, &block)
|
|
16
|
-
rendered = components.map do |component|
|
|
16
|
+
rendered = components.map! do |component|
|
|
17
17
|
component.render_in(view_context, &block)
|
|
18
18
|
end
|
|
19
19
|
safe_join(rendered, rendered_spacer(view_context))
|
|
@@ -63,11 +63,12 @@ module ViewComponent
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def component_options(item, iterator)
|
|
66
|
-
item_options =
|
|
66
|
+
item_options = @options.dup
|
|
67
|
+
item_options[component.__vc_collection_parameter] = item
|
|
67
68
|
item_options[component.__vc_collection_counter_parameter] = iterator.index if component.__vc_counter_argument_present?
|
|
68
69
|
item_options[component.__vc_collection_iteration_parameter] = iterator.dup if component.__vc_iteration_argument_present?
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
item_options
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
# Render the spacer through a fresh `dup` so a collection rendered multiple
|
|
@@ -115,11 +115,11 @@ module ViewComponent
|
|
|
115
115
|
.tally
|
|
116
116
|
.select { |_, count| count > 1 }
|
|
117
117
|
.each do |tally|
|
|
118
|
-
|
|
118
|
+
variant, this_format = tally.first
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
variant_string = " for variant `#{variant}`" if variant.present?
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
errors << "More than one #{this_format.upcase} template found#{variant_string} for #{@component}. "
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
default_template_types = @templates.each_with_object(Set.new) do |template, memo|
|
|
@@ -177,7 +177,7 @@ module ViewComponent
|
|
|
177
177
|
else
|
|
178
178
|
path_parser = ActionView::Resolver::PathParser.new
|
|
179
179
|
templates = @component.sidecar_files(
|
|
180
|
-
ActionView::Template.
|
|
180
|
+
ActionView::Template::Handlers.extensions
|
|
181
181
|
).map do |path|
|
|
182
182
|
details = path_parser.parse(path).details
|
|
183
183
|
Template::File.new(component: @component, path: path, details: details)
|
|
@@ -190,11 +190,11 @@ module ViewComponent
|
|
|
190
190
|
).flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) }
|
|
191
191
|
.uniq
|
|
192
192
|
.each do |method_name|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
templates << Template::InlineCall.new(
|
|
194
|
+
component: @component,
|
|
195
|
+
method_name: method_name,
|
|
196
|
+
defined_on_self: component_instance_methods_on_self.include?(method_name)
|
|
197
|
+
)
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
templates
|
|
@@ -77,6 +77,13 @@ module ViewComponent
|
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
initializer "view_component.serializable" do
|
|
81
|
+
ActiveSupport.on_load(:active_job) do
|
|
82
|
+
require "view_component/active_job_serializer"
|
|
83
|
+
ActiveJob::Serializers.add_serializers(ViewComponent::ActiveJobSerializer)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
80
87
|
initializer "view_component.eager_load_actions" do
|
|
81
88
|
ActiveSupport.on_load(:after_initialize) do
|
|
82
89
|
if Rails.application.config.eager_load
|
|
@@ -9,7 +9,7 @@ module ViewComponent
|
|
|
9
9
|
MESSAGE =
|
|
10
10
|
"It looks like a block was provided after calling `with_content` on COMPONENT, " \
|
|
11
11
|
"which means that ViewComponent doesn't know which content to use.\n\n" \
|
|
12
|
-
"To fix this issue, use either `with_content` or a block."
|
|
12
|
+
"To fix this issue, use either `with_content` or a block.".freeze
|
|
13
13
|
|
|
14
14
|
def initialize(klass_name)
|
|
15
15
|
super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
|
|
@@ -25,13 +25,13 @@ module ViewComponent
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
class MultipleInlineTemplatesError < BaseError
|
|
28
|
-
MESSAGE = "Inline templates can only be defined once per-component."
|
|
28
|
+
MESSAGE = "Inline templates can only be defined once per-component.".freeze
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
class MissingPreviewTemplateError < StandardError
|
|
32
32
|
MESSAGE =
|
|
33
33
|
"A preview template for example EXAMPLE doesn't exist.\n\n" \
|
|
34
|
-
"To fix this issue, create a template for the example."
|
|
34
|
+
"To fix this issue, create a template for the example.".freeze
|
|
35
35
|
|
|
36
36
|
def initialize(example)
|
|
37
37
|
super(MESSAGE.gsub("EXAMPLE", example))
|
|
@@ -41,7 +41,7 @@ module ViewComponent
|
|
|
41
41
|
class MissingTemplateError < StandardError
|
|
42
42
|
MESSAGE =
|
|
43
43
|
"No templates for COMPONENT match the request DETAIL.\n\n" \
|
|
44
|
-
"To fix this issue, provide a suitable template."
|
|
44
|
+
"To fix this issue, provide a suitable template.".freeze
|
|
45
45
|
|
|
46
46
|
def initialize(component, request_detail)
|
|
47
47
|
detail = {
|
|
@@ -58,7 +58,7 @@ module ViewComponent
|
|
|
58
58
|
MESSAGE =
|
|
59
59
|
"It looks like a block was provided after calling `with_content` on COMPONENT, " \
|
|
60
60
|
"which means that ViewComponent doesn't know which content to use.\n\n" \
|
|
61
|
-
"To fix this issue, use either `with_content` or a block."
|
|
61
|
+
"To fix this issue, use either `with_content` or a block.".freeze
|
|
62
62
|
|
|
63
63
|
def initialize(klass_name)
|
|
64
64
|
super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
|
|
@@ -70,7 +70,7 @@ module ViewComponent
|
|
|
70
70
|
"The initializer for COMPONENT doesn't accept the parameter `PARAMETER`, " \
|
|
71
71
|
"which is required to render it as a collection.\n\n" \
|
|
72
72
|
"To fix this issue, update the initializer to accept `PARAMETER`.\n\n" \
|
|
73
|
-
"See [the collections docs](https://viewcomponent.org/guide/collections.html) for more information on rendering collections."
|
|
73
|
+
"See [the collections docs](https://viewcomponent.org/guide/collections.html) for more information on rendering collections.".freeze
|
|
74
74
|
|
|
75
75
|
def initialize(klass_name, parameter)
|
|
76
76
|
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
|
|
@@ -80,7 +80,7 @@ module ViewComponent
|
|
|
80
80
|
class ReservedParameterError < StandardError
|
|
81
81
|
MESSAGE =
|
|
82
82
|
"COMPONENT initializer can't accept the parameter `PARAMETER`, as it will override a " \
|
|
83
|
-
"public ViewComponent method. To fix this issue, rename the parameter."
|
|
83
|
+
"public ViewComponent method. To fix this issue, rename the parameter.".freeze
|
|
84
84
|
|
|
85
85
|
def initialize(klass_name, parameter)
|
|
86
86
|
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
|
|
@@ -90,14 +90,14 @@ module ViewComponent
|
|
|
90
90
|
class InvalidCollectionArgumentError < BaseError
|
|
91
91
|
MESSAGE =
|
|
92
92
|
"The value of the first argument passed to `with_collection` isn't a valid collection. " \
|
|
93
|
-
"Make sure it responds to `to_ary`."
|
|
93
|
+
"Make sure it responds to `to_ary`.".freeze
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
class ContentSlotNameError < StandardError
|
|
97
97
|
MESSAGE =
|
|
98
98
|
"COMPONENT declares a slot named content, which is a reserved word in ViewComponent.\n\n" \
|
|
99
99
|
"Content passed to a ViewComponent as a block is captured and assigned to the `content` accessor without having to create an explicit slot.\n\n" \
|
|
100
|
-
"To fix this issue, either use the `content` accessor directly or choose a different slot name."
|
|
100
|
+
"To fix this issue, either use the `content` accessor directly or choose a different slot name.".freeze
|
|
101
101
|
|
|
102
102
|
def initialize(klass_name)
|
|
103
103
|
super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
|
|
@@ -107,7 +107,7 @@ module ViewComponent
|
|
|
107
107
|
class InvalidSlotDefinitionError < BaseError
|
|
108
108
|
MESSAGE =
|
|
109
109
|
"Invalid slot definition. Please pass a class, " \
|
|
110
|
-
"string, or callable (that is proc, lambda, etc)"
|
|
110
|
+
"string, or callable (that is proc, lambda, etc)".freeze
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
class InvalidSlotNameError < StandardError
|
|
@@ -118,7 +118,7 @@ module ViewComponent
|
|
|
118
118
|
"COMPONENT declares a slot named SLOT_NAME, which ends with a question mark.\n\n" \
|
|
119
119
|
"This isn't allowed because the ViewComponent framework already provides predicate " \
|
|
120
120
|
"methods ending in `?`.\n\n" \
|
|
121
|
-
"To fix this issue, choose a different name."
|
|
121
|
+
"To fix this issue, choose a different name.".freeze
|
|
122
122
|
|
|
123
123
|
def initialize(klass_name, slot_name)
|
|
124
124
|
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
|
|
@@ -128,7 +128,7 @@ module ViewComponent
|
|
|
128
128
|
class RedefinedSlotError < StandardError
|
|
129
129
|
MESSAGE =
|
|
130
130
|
"COMPONENT declares the SLOT_NAME slot multiple times.\n\n" \
|
|
131
|
-
"To fix this issue, choose a different slot name."
|
|
131
|
+
"To fix this issue, choose a different slot name.".freeze
|
|
132
132
|
|
|
133
133
|
def initialize(klass_name, slot_name)
|
|
134
134
|
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
|
|
@@ -138,7 +138,7 @@ module ViewComponent
|
|
|
138
138
|
class ReservedSingularSlotNameError < InvalidSlotNameError
|
|
139
139
|
MESSAGE =
|
|
140
140
|
"COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \
|
|
141
|
-
"To fix this issue, choose a different name."
|
|
141
|
+
"To fix this issue, choose a different name.".freeze
|
|
142
142
|
|
|
143
143
|
def initialize(klass_name, slot_name)
|
|
144
144
|
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
|
|
@@ -148,7 +148,7 @@ module ViewComponent
|
|
|
148
148
|
class ReservedPluralSlotNameError < InvalidSlotNameError
|
|
149
149
|
MESSAGE =
|
|
150
150
|
"COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \
|
|
151
|
-
"To fix this issue, choose a different name."
|
|
151
|
+
"To fix this issue, choose a different name.".freeze
|
|
152
152
|
|
|
153
153
|
def initialize(klass_name, slot_name)
|
|
154
154
|
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
|
|
@@ -158,7 +158,7 @@ module ViewComponent
|
|
|
158
158
|
class UncountableSlotNameError < InvalidSlotNameError
|
|
159
159
|
MESSAGE =
|
|
160
160
|
"COMPONENT declares a slot named SLOT_NAME, which is an uncountable word\n\n" \
|
|
161
|
-
"To fix this issue, choose a different name."
|
|
161
|
+
"To fix this issue, choose a different name.".freeze
|
|
162
162
|
|
|
163
163
|
def initialize(klass_name, slot_name)
|
|
164
164
|
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
|
|
@@ -166,7 +166,7 @@ module ViewComponent
|
|
|
166
166
|
end
|
|
167
167
|
|
|
168
168
|
class ContentAlreadySetForPolymorphicSlotError < StandardError
|
|
169
|
-
MESSAGE = "Content for slot SLOT_NAME has already been provided."
|
|
169
|
+
MESSAGE = "Content for slot SLOT_NAME has already been provided.".freeze
|
|
170
170
|
|
|
171
171
|
def initialize(slot_name)
|
|
172
172
|
super(MESSAGE.gsub("SLOT_NAME", slot_name.to_s))
|
|
@@ -176,7 +176,7 @@ module ViewComponent
|
|
|
176
176
|
class NilWithContentError < BaseError
|
|
177
177
|
MESSAGE =
|
|
178
178
|
"No content provided to `#with_content` for #{self}.\n\n" \
|
|
179
|
-
"To fix this issue, pass a value."
|
|
179
|
+
"To fix this issue, pass a value.".freeze
|
|
180
180
|
end
|
|
181
181
|
|
|
182
182
|
class TranslateCalledBeforeRenderError < BaseError
|
|
@@ -185,7 +185,7 @@ module ViewComponent
|
|
|
185
185
|
"on the view context that only exists once a ViewComponent is passed to " \
|
|
186
186
|
"the Rails render pipeline.\n\n" \
|
|
187
187
|
"It's sometimes possible to fix this issue by moving code dependent on " \
|
|
188
|
-
"`#translate` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
|
|
188
|
+
"`#translate` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void).".freeze
|
|
189
189
|
end
|
|
190
190
|
|
|
191
191
|
class HelpersCalledBeforeRenderError < BaseError
|
|
@@ -194,7 +194,7 @@ module ViewComponent
|
|
|
194
194
|
"on the view context that only exists once a ViewComponent is passed to " \
|
|
195
195
|
"the Rails render pipeline.\n\n" \
|
|
196
196
|
"It's sometimes possible to fix this issue by moving code dependent on " \
|
|
197
|
-
"`#helpers` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
|
|
197
|
+
"`#helpers` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void).".freeze
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
class ControllerCalledBeforeRenderError < BaseError
|
|
@@ -203,17 +203,17 @@ module ViewComponent
|
|
|
203
203
|
"on the view context that only exists once a ViewComponent is passed to " \
|
|
204
204
|
"the Rails render pipeline.\n\n" \
|
|
205
205
|
"It's sometimes possible to fix this issue by moving code dependent on " \
|
|
206
|
-
"`#controller` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
|
|
206
|
+
"`#controller` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void).".freeze
|
|
207
207
|
end
|
|
208
208
|
|
|
209
209
|
class SystemTestControllerNefariousPathError < BaseError
|
|
210
|
-
MESSAGE = "ViewComponent SystemTest controller attempted to load a file outside of the expected directory."
|
|
210
|
+
MESSAGE = "ViewComponent SystemTest controller attempted to load a file outside of the expected directory.".freeze
|
|
211
211
|
end
|
|
212
212
|
|
|
213
213
|
class AlreadyDefinedPolymorphicSlotSetterError < StandardError
|
|
214
214
|
MESSAGE =
|
|
215
215
|
"A method called 'SETTER_METHOD_NAME' already exists and would be overwritten by the 'SETTER_NAME' polymorphic " \
|
|
216
|
-
"slot setter.\n\nPlease choose a different setter name."
|
|
216
|
+
"slot setter.\n\nPlease choose a different setter name.".freeze
|
|
217
217
|
|
|
218
218
|
def initialize(setter_method_name, setter_name)
|
|
219
219
|
super(MESSAGE.gsub("SETTER_METHOD_NAME", setter_method_name.to_s).gsub("SETTER_NAME", setter_name.to_s))
|
|
@@ -8,7 +8,7 @@ module ViewComponent # :nodoc:
|
|
|
8
8
|
mod.prepend(self) unless self <= ViewComponent::Instrumentation
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def render_in(
|
|
11
|
+
def render_in(...)
|
|
12
12
|
return super if !Rails.application.config.view_component.instrumentation_enabled.present?
|
|
13
13
|
|
|
14
14
|
payload = {
|
|
@@ -11,6 +11,8 @@ module ViewComponent
|
|
|
11
11
|
# We're exposing it to the compiler via `refine` so that ViewComponent
|
|
12
12
|
# can match Rails' template picking logic.
|
|
13
13
|
module RequestDetails
|
|
14
|
+
EMPTY_DETAILS = {}.freeze
|
|
15
|
+
|
|
14
16
|
refine ActionView::LookupContext do
|
|
15
17
|
# Return an abstraction for matching and sorting available templates
|
|
16
18
|
# based on the current lookup context details.
|
|
@@ -18,7 +20,7 @@ module ViewComponent
|
|
|
18
20
|
# @return ActionView::TemplateDetails::Requested
|
|
19
21
|
# @see ActionView::LookupContext#detail_args_for
|
|
20
22
|
# @see ActionView::FileSystemResolver#_find_all
|
|
21
|
-
def vc_requested_details(user_details =
|
|
23
|
+
def vc_requested_details(user_details = EMPTY_DETAILS)
|
|
22
24
|
# The hash `user_details` would normally be the standard arguments that
|
|
23
25
|
# `render` accepts, but there's currently no mechanism for users to
|
|
24
26
|
# provide these when calling render on a ViewComponent.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Serializable
|
|
5
|
+
# A proxy that wraps a component class and its initialization arguments, deferring
|
|
6
|
+
# component instantiation until render time. This allows slot calls and other
|
|
7
|
+
# post-initialize configuration to be captured and replayed, and enables the
|
|
8
|
+
# proxy itself (rather than a live component instance) to be serialized for
|
|
9
|
+
# background jobs (e.g. ActiveJob / Turbo Streams).
|
|
10
|
+
class Proxy
|
|
11
|
+
# Rebuilds a Proxy from a serialized hash produced by +serialize+
|
|
12
|
+
def self.deserialize(hash)
|
|
13
|
+
klass = hash["component_class"].safe_constantize
|
|
14
|
+
raise ArgumentError, "Cannot deserialize unknown component: #{hash["component_class"]}" unless klass
|
|
15
|
+
|
|
16
|
+
args = ActiveJob::Arguments.deserialize(hash["initialize_args"] || [])
|
|
17
|
+
proxy = new(klass, *args)
|
|
18
|
+
|
|
19
|
+
Array(hash["slot_calls"]).each do |call|
|
|
20
|
+
method_name = call["method"].to_sym
|
|
21
|
+
slot_args = ActiveJob::Arguments.deserialize(call["args"])
|
|
22
|
+
proxy.public_send(method_name, *slot_args)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
proxy
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :component_class, :initialize_args, :slot_calls
|
|
29
|
+
|
|
30
|
+
def initialize(component_class, *args)
|
|
31
|
+
if component_class.name.nil?
|
|
32
|
+
raise UnserializableError, "Cannot serialize anonymous component class #{component_class.inspect}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@component_class = component_class
|
|
36
|
+
@initialize_args = args
|
|
37
|
+
@slot_calls = []
|
|
38
|
+
end
|
|
39
|
+
ruby2_keywords :initialize
|
|
40
|
+
|
|
41
|
+
# Implements the Rails renderable interface
|
|
42
|
+
def render_in(view_context, &block)
|
|
43
|
+
if block
|
|
44
|
+
raise UnserializableError, "Cannot serialize render_in with a block"
|
|
45
|
+
end
|
|
46
|
+
build_component.render_in(view_context)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def method_missing(method_name, *args, &block)
|
|
50
|
+
if slot_method?(method_name)
|
|
51
|
+
capture_slot_call(method_name, args, &block)
|
|
52
|
+
else
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
ruby2_keywords :method_missing
|
|
57
|
+
|
|
58
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
59
|
+
slot_method?(method_name) || super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns a hash that can be rebuilt into a Proxy instance using +deserialize+
|
|
63
|
+
def serialize
|
|
64
|
+
serialized_slot_calls = @slot_calls.map do |call|
|
|
65
|
+
{
|
|
66
|
+
"method" => call[:method].to_s,
|
|
67
|
+
"args" => ActiveJob::Arguments.serialize(call[:args])
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
"component_class" => @component_class.name,
|
|
73
|
+
"initialize_args" => ActiveJob::Arguments.serialize(@initialize_args),
|
|
74
|
+
"slot_calls" => serialized_slot_calls
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if defined?(Rails::VERSION) && Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
|
|
79
|
+
# Rails expects us to define `format` on all renderables,
|
|
80
|
+
# but we do not know the `format` of a ViewComponent until runtime.
|
|
81
|
+
def format
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def slot_method?(method_name)
|
|
89
|
+
method_name.to_s.start_with?("with_") && @component_class.method_defined?(method_name)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def capture_slot_call(method_name, args, &block)
|
|
93
|
+
if block
|
|
94
|
+
raise UnserializableError, "Cannot serialize slot call '#{method_name}' with a block"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@slot_calls << {method: method_name, args: args}
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def build_component
|
|
102
|
+
@component_class.new(*@initialize_args).tap do |instance|
|
|
103
|
+
@slot_calls.each do |call|
|
|
104
|
+
instance.public_send(call[:method], *call[:args])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
require "view_component/serializable/proxy"
|
|
5
|
+
|
|
6
|
+
module ViewComponent
|
|
7
|
+
module Serializable
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
class UnserializableError < ArgumentError; end
|
|
11
|
+
|
|
12
|
+
class_methods do
|
|
13
|
+
# Returns a Proxy that captures this class and its initialization arguments,
|
|
14
|
+
# deferring instantiation until render time. The proxy is renderable and
|
|
15
|
+
# serializable for ActiveJob (e.g. Turbo Streams). Slot calls made on the
|
|
16
|
+
# proxy are captured and replayed at render time. Blocks are not supported.
|
|
17
|
+
#
|
|
18
|
+
# MyComponent.render_later("title", size: :large).with_item(label: "One")
|
|
19
|
+
def render_later(*args, &block)
|
|
20
|
+
raise UnserializableError, "Cannot serialize render_later with a block" if block
|
|
21
|
+
|
|
22
|
+
ViewComponent::Serializable::Proxy.new(self, *args)
|
|
23
|
+
end
|
|
24
|
+
ruby2_keywords :render_later
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -5,7 +5,7 @@ module ViewComponent
|
|
|
5
5
|
DEFAULT_FORMAT = :html
|
|
6
6
|
private_constant :DEFAULT_FORMAT
|
|
7
7
|
|
|
8
|
-
DataWithSource = Struct.new(:format, :identifier, :short_identifier, :type
|
|
8
|
+
DataWithSource = Struct.new(:format, :identifier, :short_identifier, :type)
|
|
9
9
|
|
|
10
10
|
attr_reader :details, :path
|
|
11
11
|
|
|
@@ -28,33 +28,7 @@ module ViewComponent
|
|
|
28
28
|
details = ActionView::TemplateDetails.new(details.locale, details.handler, DEFAULT_FORMAT, details.variant)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# Rails 8.1 added a newline to compiled ERB output (rails/rails#53731).
|
|
34
|
-
# Use -1 to compensate for correct line numbers in stack traces.
|
|
35
|
-
# However, negative line numbers cause segfaults when Ruby's coverage
|
|
36
|
-
# is enabled (bugs.ruby-lang.org/issues/19363). In that case, strip the
|
|
37
|
-
# annotation line from compiled source instead.
|
|
38
|
-
lineno =
|
|
39
|
-
if Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 && details.handler == :erb
|
|
40
|
-
if coverage_running?
|
|
41
|
-
# Can't use negative lineno with coverage (causes segfault on Linux).
|
|
42
|
-
# Strip annotation line if enabled to preserve correct line numbers.
|
|
43
|
-
@strip_annotation_line = ActionView::Base.annotate_rendered_view_with_filenames
|
|
44
|
-
0
|
|
45
|
-
else
|
|
46
|
-
-1
|
|
47
|
-
end
|
|
48
|
-
else
|
|
49
|
-
0
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
super(
|
|
53
|
-
component: component,
|
|
54
|
-
details: details,
|
|
55
|
-
path: path,
|
|
56
|
-
lineno: lineno
|
|
57
|
-
)
|
|
31
|
+
super
|
|
58
32
|
end
|
|
59
33
|
|
|
60
34
|
def type
|
|
@@ -68,11 +42,36 @@ module ViewComponent
|
|
|
68
42
|
|
|
69
43
|
private
|
|
70
44
|
|
|
45
|
+
# Rails 8.1 added a newline to compiled ERB output (rails/rails#53731).
|
|
46
|
+
# Use -1 to compensate for correct line numbers in stack traces.
|
|
47
|
+
# However, negative line numbers cause segfaults when Ruby's coverage
|
|
48
|
+
# is enabled (bugs.ruby-lang.org/issues/19363). In that case, strip the
|
|
49
|
+
# annotation line from compiled source instead.
|
|
50
|
+
#
|
|
51
|
+
# Computed at compile time rather than at initialization because both
|
|
52
|
+
# coverage and annotation settings can change between the two.
|
|
53
|
+
def lineno
|
|
54
|
+
return 0 unless erb_newline_compensation_needed?
|
|
55
|
+
|
|
56
|
+
# Can't use negative lineno with coverage (causes segfault on Linux).
|
|
57
|
+
coverage_running? ? 0 : -1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Strip the annotation line to maintain correct line numbers when coverage
|
|
61
|
+
# is running (avoids segfault from negative lineno)
|
|
62
|
+
def strip_annotation_line?
|
|
63
|
+
erb_newline_compensation_needed? &&
|
|
64
|
+
coverage_running? &&
|
|
65
|
+
ActionView::Base.annotate_rendered_view_with_filenames
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def erb_newline_compensation_needed?
|
|
69
|
+
Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 && details.handler == :erb
|
|
70
|
+
end
|
|
71
|
+
|
|
71
72
|
def compiled_source
|
|
72
73
|
result = super
|
|
73
|
-
|
|
74
|
-
# is running (avoids segfault from negative lineno)
|
|
75
|
-
result = result.partition(";").last if @strip_annotation_line
|
|
74
|
+
result = result.partition(";").last if strip_annotation_line?
|
|
76
75
|
result
|
|
77
76
|
end
|
|
78
77
|
end
|
|
@@ -147,7 +146,7 @@ module ViewComponent
|
|
|
147
146
|
@component.silence_redefinition_of_method(call_method_name)
|
|
148
147
|
|
|
149
148
|
# rubocop:disable Style/EvalWithLocation
|
|
150
|
-
@component.class_eval <<~RUBY, @path,
|
|
149
|
+
@component.class_eval <<~RUBY, @path, lineno
|
|
151
150
|
def #{call_method_name}
|
|
152
151
|
#{compiled_source}
|
|
153
152
|
end
|
|
@@ -195,6 +194,8 @@ module ViewComponent
|
|
|
195
194
|
|
|
196
195
|
private
|
|
197
196
|
|
|
197
|
+
attr_reader :lineno
|
|
198
|
+
|
|
198
199
|
def compiled_source
|
|
199
200
|
handler = details.handler_class
|
|
200
201
|
this_source = source
|
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
module ViewComponent
|
|
4
4
|
module VERSION
|
|
5
5
|
MAJOR = 4
|
|
6
|
-
MINOR =
|
|
6
|
+
MINOR = 13
|
|
7
7
|
PATCH = 0
|
|
8
8
|
PRE = nil
|
|
9
9
|
|
|
10
|
-
STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
|
|
10
|
+
STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".").freeze
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
|
-
|
|
14
|
-
puts ViewComponent::VERSION::STRING if __FILE__ == $PROGRAM_NAME
|
data/lib/view_component.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: view_component
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ViewComponent Team
|
|
@@ -89,6 +89,7 @@ files:
|
|
|
89
89
|
- lib/generators/view_component/test_unit/templates/component_test.rb.tt
|
|
90
90
|
- lib/generators/view_component/test_unit/test_unit_generator.rb
|
|
91
91
|
- lib/view_component.rb
|
|
92
|
+
- lib/view_component/active_job_serializer.rb
|
|
92
93
|
- lib/view_component/base.rb
|
|
93
94
|
- lib/view_component/collection.rb
|
|
94
95
|
- lib/view_component/compile_cache.rb
|
|
@@ -102,6 +103,8 @@ files:
|
|
|
102
103
|
- lib/view_component/instrumentation.rb
|
|
103
104
|
- lib/view_component/preview.rb
|
|
104
105
|
- lib/view_component/request_details.rb
|
|
106
|
+
- lib/view_component/serializable.rb
|
|
107
|
+
- lib/view_component/serializable/proxy.rb
|
|
105
108
|
- lib/view_component/slot.rb
|
|
106
109
|
- lib/view_component/slotable.rb
|
|
107
110
|
- lib/view_component/system_spec_helpers.rb
|
|
@@ -135,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
135
138
|
- !ruby/object:Gem::Version
|
|
136
139
|
version: '0'
|
|
137
140
|
requirements: []
|
|
138
|
-
rubygems_version: 4.0.
|
|
141
|
+
rubygems_version: 4.0.16
|
|
139
142
|
specification_version: 4
|
|
140
143
|
summary: A framework for building reusable, testable & encapsulated view components
|
|
141
144
|
in Ruby on Rails.
|