view_component 4.12.0 → 4.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/app/controllers/view_components_system_test_controller.rb +1 -0
- data/docs/CHANGELOG.md +86 -0
- data/lib/view_component/active_job_serializer.rb +23 -0
- data/lib/view_component/base.rb +25 -8
- data/lib/view_component/cache_digest/dependency_tracking.rb +46 -0
- data/lib/view_component/cache_digest/resolver.rb +178 -0
- data/lib/view_component/cache_digest.rb +247 -0
- data/lib/view_component/collection.rb +11 -5
- data/lib/view_component/compiler.rb +9 -9
- data/lib/view_component/engine.rb +7 -0
- data/lib/view_component/errors.rb +70 -22
- data/lib/view_component/experimentally_cacheable.rb +285 -0
- 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 +3 -0
- metadata +9 -2
|
@@ -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 = 15
|
|
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
|
@@ -8,13 +8,16 @@ module ViewComponent
|
|
|
8
8
|
extend ActiveSupport::Autoload
|
|
9
9
|
|
|
10
10
|
autoload :Base
|
|
11
|
+
autoload :CacheDigest
|
|
11
12
|
autoload :Compiler
|
|
12
13
|
autoload :CompileCache
|
|
13
14
|
autoload :Config
|
|
14
15
|
autoload :Deprecation
|
|
16
|
+
autoload :ExperimentallyCacheable
|
|
15
17
|
autoload :InlineTemplate
|
|
16
18
|
autoload :Instrumentation
|
|
17
19
|
autoload :Preview
|
|
20
|
+
autoload :Serializable
|
|
18
21
|
autoload :Translatable
|
|
19
22
|
|
|
20
23
|
if defined?(Rails.env) && Rails.env.test?
|
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.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ViewComponent Team
|
|
@@ -89,7 +89,11 @@ 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
|
|
94
|
+
- lib/view_component/cache_digest.rb
|
|
95
|
+
- lib/view_component/cache_digest/dependency_tracking.rb
|
|
96
|
+
- lib/view_component/cache_digest/resolver.rb
|
|
93
97
|
- lib/view_component/collection.rb
|
|
94
98
|
- lib/view_component/compile_cache.rb
|
|
95
99
|
- lib/view_component/compiler.rb
|
|
@@ -98,10 +102,13 @@ files:
|
|
|
98
102
|
- lib/view_component/deprecation.rb
|
|
99
103
|
- lib/view_component/engine.rb
|
|
100
104
|
- lib/view_component/errors.rb
|
|
105
|
+
- lib/view_component/experimentally_cacheable.rb
|
|
101
106
|
- lib/view_component/inline_template.rb
|
|
102
107
|
- lib/view_component/instrumentation.rb
|
|
103
108
|
- lib/view_component/preview.rb
|
|
104
109
|
- lib/view_component/request_details.rb
|
|
110
|
+
- lib/view_component/serializable.rb
|
|
111
|
+
- lib/view_component/serializable/proxy.rb
|
|
105
112
|
- lib/view_component/slot.rb
|
|
106
113
|
- lib/view_component/slotable.rb
|
|
107
114
|
- lib/view_component/system_spec_helpers.rb
|
|
@@ -135,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
135
142
|
- !ruby/object:Gem::Version
|
|
136
143
|
version: '0'
|
|
137
144
|
requirements: []
|
|
138
|
-
rubygems_version: 4.0.
|
|
145
|
+
rubygems_version: 4.0.16
|
|
139
146
|
specification_version: 4
|
|
140
147
|
summary: A framework for building reusable, testable & encapsulated view components
|
|
141
148
|
in Ruby on Rails.
|