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.
@@ -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, keyword_init: true)
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
- @strip_annotation_line = false
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
- # Strip the annotation line to maintain correct line numbers when coverage
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, @lineno
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 = 12
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
@@ -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.12.0
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.10
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.