view_component 3.7.0 → 3.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47e62a0d72c8f9b594303a04d917565ea3766f30ee1335154643e311f92b6128
4
- data.tar.gz: 412377cfaf624ddfb930d52dcf07a469666f57024d35712f71f752f454f507a5
3
+ metadata.gz: 7d167b54bcd09ae6e5a8ace98cdb2cfac7386a29029cd8b4b34d1e5aa00093fd
4
+ data.tar.gz: 5ea5073913c8c6026dbf5d5e6d04671bc28a42564a9788988dfe0ee18d03af9d
5
5
  SHA512:
6
- metadata.gz: f55188297a16933853b95796a14686a29c1ba14eab0cf50d965c28ad8a18664cdafe8ebb12c700c5877f533b6ddb112cbba8dd498735d4a1f66a9b37647f33bd
7
- data.tar.gz: 780c452ac9a0e33079ca50d49408c74a1c69f427836415c6c08bbdd82f0cb098a646859d26da8ecd93affe8724ab871ed08a210234a9fff71d68ba0ab7668fe8
6
+ metadata.gz: 2232e4237c3d851577dc10cb9054c03a7614113761e2f6b2b33e3005ad05e665dd1031d29c84f10fcc7bef1a72fa3529866d11082c4ddbc9dd395e415ae5b2b9
7
+ data.tar.gz: 67d9aa02848b03e68b5eb56c2bb37d27b39d757d53b4a30b466c9f7e73db29fe5771f2732f6447a135a3d1a120abc387350451b8d875923ce3dae6db06f5ce24
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PreviewHelper
4
+ # :nocov:
5
+ include ActionView::Helpers::AssetUrlHelper if Rails.version.to_f < 6.1
6
+ # :nocov:
7
+
4
8
  AVAILABLE_PRISM_LANGUAGES = %w[ruby erb haml]
5
9
  FALLBACK_LANGUAGE = "ruby"
6
10
 
@@ -10,6 +14,14 @@ module PreviewHelper
10
14
  render "preview_source"
11
15
  end
12
16
 
17
+ def prism_css_source_url
18
+ serve_static_preview_assets? ? asset_path("prism.css", skip_pipeline: true) : "https://cdn.jsdelivr.net/npm/prismjs@1.28.0/themes/prism.min.css"
19
+ end
20
+
21
+ def prism_js_source_url
22
+ serve_static_preview_assets? ? asset_path("prism.min.js", skip_pipeline: true) : "https://cdn.jsdelivr.net/npm/prismjs@1.28.0/prism.min.js"
23
+ end
24
+
13
25
  def find_template_data(lookup_context:, template_identifier:)
14
26
  template = lookup_context.find_template(template_identifier)
15
27
 
@@ -18,6 +30,7 @@ module PreviewHelper
18
30
  source: template.source,
19
31
  prism_language_name: prism_language_name_by_template(template: template)
20
32
  }
33
+ # :nocov:
21
34
  else
22
35
  # Fetch template source via finding it through preview paths
23
36
  # to accomodate source view when exclusively using templates
@@ -43,6 +56,7 @@ module PreviewHelper
43
56
  prism_language_name: prism_language_name
44
57
  }
45
58
  end
59
+ # :nocov:
46
60
  end
47
61
 
48
62
  private
@@ -55,6 +69,7 @@ module PreviewHelper
55
69
  language
56
70
  end
57
71
 
72
+ # :nocov:
58
73
  def prism_language_name_by_template_path(template_file_path:)
59
74
  language = template_file_path.gsub(".html", "").split(".").last
60
75
 
@@ -62,4 +77,9 @@ module PreviewHelper
62
77
 
63
78
  language
64
79
  end
80
+ # :nocov:
81
+
82
+ def serve_static_preview_assets?
83
+ ViewComponent::Base.config.show_previews && Rails.application.config.public_file_server.enabled
84
+ end
65
85
  end
@@ -1,4 +1,4 @@
1
- <link href="<%= asset_path('prism.css', skip_pipeline: true) %>" media="screen" rel="stylesheet" type="text/css">
1
+ <link href="<%= prism_css_source_url %>" media="screen" rel="stylesheet" type="text/css">
2
2
  <div class="view-component-source-example">
3
3
  <h2>Source:</h2>
4
4
  <pre class="source">
@@ -14,4 +14,4 @@
14
14
  <% end %>
15
15
  </pre>
16
16
  </div>
17
- <script type="text/javascript" src="<%= asset_path('prism.min.js', skip_pipeline: true) %>"></script>
17
+ <script type="text/javascript" src="<%= prism_js_source_url %>"></script>
data/docs/CHANGELOG.md CHANGED
@@ -10,6 +10,84 @@ nav_order: 5
10
10
 
11
11
  ## main
12
12
 
13
+ ## 3.9.0
14
+
15
+ * Don’t break `rails stats` if ViewComponent path is missing.
16
+
17
+ *Claudio Baccigalupo*
18
+
19
+ * Add deprecation warnings for EOL ruby and Rails versions and patches associated with them.
20
+
21
+ *Reegan Viljoen*
22
+
23
+ * Add support for Ruby 3.3.
24
+
25
+ *Reegan Viljoen*
26
+
27
+ * Allow translations to be inherited and overridden in subclasses.
28
+
29
+ *Elia Schito*
30
+
31
+ * Resolve console warnings when running test suite.
32
+
33
+ *Joel Hawksley*
34
+
35
+ * Fix spelling in a local variable.
36
+
37
+ *Olle Jonsson*
38
+
39
+ * Avoid duplicating rendered string when `output_postamble` is blank.
40
+
41
+ *Mitchell Henke*
42
+
43
+ * Ensure HTML output safety.
44
+
45
+ *Cameron Dutro*
46
+
47
+ ## 3.8.0
48
+
49
+ * Use correct value for the `config.action_dispatch.show_exceptions` config option for edge Rails.
50
+
51
+ *Cameron Dutro*
52
+
53
+ * Remove unsupported versions of Rails & Ruby from CI matrix.
54
+
55
+ *Reegan Viljoen*
56
+
57
+ * Raise error when uncountable slot names are used in `renders_many`
58
+
59
+ *Hugo Chantelauze*
60
+ *Reegan Viljoen*
61
+
62
+ * Replace usage of `String#ends_with?` with `String#end_with?` to reduce the dependency on ActiveSupport core extensions.
63
+
64
+ *halo*
65
+
66
+ * Don't add ActionDispatch::Static middleware unless `public_file_server.enabled`.
67
+
68
+ *Daniel Gonzalez*
69
+ *Reegan Viljoen*
70
+
71
+ * Resolve an issue where slots starting with `call` would cause a `NameError`
72
+
73
+ *Blake Williams*
74
+
75
+ * Add `use_helper` API.
76
+
77
+ *Reegan Viljoen*
78
+
79
+ * Fix bug where the `Rails` module wasn't being searched from the root namespace.
80
+
81
+ *Zenéixe*
82
+
83
+ * Fix bug where `#with_request_url`, set the incorrect `request.fullpath`.
84
+
85
+ *Nachiket Pusalkar*
86
+
87
+ * Allow setting method when using the `with_request_url` test helper.
88
+
89
+ *Andrew Duthie*
90
+
13
91
  ## 3.7.0
14
92
 
15
93
  * Support Rails 7.1 in CI.
@@ -181,6 +259,17 @@ This release makes the following breaking changes, many of which have long been
181
259
 
182
260
  *Joel Hawksley*
183
261
 
262
+ For example:
263
+
264
+ ```diff
265
+ <%= render BlogComponent.new do |component| %>
266
+ - <% component.header do %>
267
+ + <% component.with_header do %>
268
+ <%= link_to "My blog", root_path %>
269
+ <% end %>
270
+ <% end %>
271
+ ```
272
+
184
273
  * BREAKING: Remove deprecated SlotsV1 in favor of current SlotsV2.
185
274
 
186
275
  *Joel Hawksley*
@@ -34,11 +34,11 @@ module Locale
34
34
  end
35
35
 
36
36
  def destination(locale = nil)
37
- extention = ".#{locale}" if locale
37
+ extension = ".#{locale}" if locale
38
38
  if sidecar?
39
- File.join(component_path, class_path, "#{file_name}_component", "#{file_name}_component#{extention}.yml")
39
+ File.join(component_path, class_path, "#{file_name}_component", "#{file_name}_component#{extension}.yml")
40
40
  else
41
- File.join(component_path, class_path, "#{file_name}_component#{extention}.yml")
41
+ File.join(component_path, class_path, "#{file_name}_component#{extension}.yml")
42
42
  end
43
43
  end
44
44
  end
@@ -12,6 +12,7 @@ require "view_component/preview"
12
12
  require "view_component/slotable"
13
13
  require "view_component/translatable"
14
14
  require "view_component/with_content_helper"
15
+ require "view_component/use_helpers"
15
16
 
16
17
  module ViewComponent
17
18
  class Base < ActionView::Base
@@ -103,7 +104,12 @@ module ViewComponent
103
104
  before_render
104
105
 
105
106
  if render?
106
- render_template_for(@__vc_variant).to_s + output_postamble
107
+ # Avoid allocating new string when output_postamble is blank
108
+ if output_postamble.blank?
109
+ safe_render_template_for(@__vc_variant).to_s
110
+ else
111
+ safe_render_template_for(@__vc_variant).to_s + safe_output_postamble
112
+ end
107
113
  else
108
114
  ""
109
115
  end
@@ -154,7 +160,7 @@ module ViewComponent
154
160
  #
155
161
  # @return [String]
156
162
  def output_postamble
157
- ""
163
+ @@default_output_postamble ||= "".html_safe
158
164
  end
159
165
 
160
166
  # Called before rendering the component. Override to perform operations that
@@ -219,7 +225,8 @@ module ViewComponent
219
225
  @__vc_helpers ||= __vc_original_view_context || controller.view_context
220
226
  end
221
227
 
222
- if Rails.env.development? || Rails.env.test?
228
+ if ::Rails.env.development? || ::Rails.env.test?
229
+ # @private
223
230
  def method_missing(method_name, *args) # rubocop:disable Style/MissingRespondToMissing
224
231
  super
225
232
  rescue => e # rubocop:disable Style/RescueStandardError
@@ -300,6 +307,38 @@ module ViewComponent
300
307
  defined?(@__vc_content_evaluated) && @__vc_content_evaluated
301
308
  end
302
309
 
310
+ def maybe_escape_html(text)
311
+ return text if request && !request.format.html?
312
+ return text if text.nil? || text.empty?
313
+
314
+ if text.html_safe?
315
+ text
316
+ else
317
+ yield
318
+ html_escape(text)
319
+ end
320
+ end
321
+
322
+ def safe_render_template_for(variant)
323
+ if compiler.renders_template_for_variant?(variant)
324
+ render_template_for(variant)
325
+ else
326
+ maybe_escape_html(render_template_for(variant)) do
327
+ Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. The output will be automatically escaped, but you may want to investigate.")
328
+ end
329
+ end
330
+ end
331
+
332
+ def safe_output_postamble
333
+ maybe_escape_html(output_postamble) do
334
+ Kernel.warn("WARNING: The #{self.class} component was provided an HTML-unsafe postamble. The postamble will be automatically escaped, but you may want to investigate.")
335
+ end
336
+ end
337
+
338
+ def compiler
339
+ @compiler ||= self.class.compiler
340
+ end
341
+
303
342
  # Set the controller used for testing components:
304
343
  #
305
344
  # ```ruby
@@ -615,7 +654,7 @@ module ViewComponent
615
654
 
616
655
  # @private
617
656
  def collection_counter_parameter
618
- "#{collection_parameter}_counter".to_sym
657
+ :"#{collection_parameter}_counter"
619
658
  end
620
659
 
621
660
  # @private
@@ -625,7 +664,7 @@ module ViewComponent
625
664
 
626
665
  # @private
627
666
  def collection_iteration_parameter
628
- "#{collection_parameter}_iteration".to_sym
667
+ :"#{collection_parameter}_iteration"
629
668
  end
630
669
 
631
670
  # @private
@@ -16,6 +16,7 @@ module ViewComponent
16
16
  def initialize(component_class)
17
17
  @component_class = component_class
18
18
  @redefinition_lock = Mutex.new
19
+ @variants_rendering_templates = Set.new
19
20
  end
20
21
 
21
22
  def compiled?
@@ -56,7 +57,7 @@ module ViewComponent
56
57
  RUBY
57
58
  # rubocop:enable Style/EvalWithLocation
58
59
 
59
- component_class.define_method("_call_#{safe_class_name}", component_class.instance_method(:call))
60
+ component_class.define_method(:"_call_#{safe_class_name}", component_class.instance_method(:call))
60
61
 
61
62
  component_class.silence_redefinition_of_method("render_template_for")
62
63
  component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -68,6 +69,7 @@ module ViewComponent
68
69
  else
69
70
  templates.each do |template|
70
71
  method_name = call_method_name(template[:variant])
72
+ @variants_rendering_templates << template[:variant]
71
73
 
72
74
  redefinition_lock.synchronize do
73
75
  component_class.silence_redefinition_of_method(method_name)
@@ -89,6 +91,10 @@ module ViewComponent
89
91
  CompileCache.register(component_class)
90
92
  end
91
93
 
94
+ def renders_template_for_variant?(variant)
95
+ @variants_rendering_templates.include?(variant)
96
+ end
97
+
92
98
  private
93
99
 
94
100
  attr_reader :component_class, :redefinition_lock
@@ -101,7 +107,7 @@ module ViewComponent
101
107
  "elsif variant.to_sym == :'#{variant}'\n #{safe_name}"
102
108
  end.join("\n")
103
109
 
104
- component_class.define_method("_call_#{safe_class_name}", component_class.instance_method(:call))
110
+ component_class.define_method(:"_call_#{safe_class_name}", component_class.instance_method(:call))
105
111
 
106
112
  body = <<-RUBY
107
113
  if variant.nil?
@@ -219,12 +225,12 @@ module ViewComponent
219
225
  component_class.included_modules
220
226
  )
221
227
 
222
- view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
228
+ view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) }.uniq
223
229
  end
224
230
  end
225
231
 
226
232
  def inline_calls_defined_on_self
227
- @inline_calls_defined_on_self ||= component_class.instance_methods(false).grep(/^call/)
233
+ @inline_calls_defined_on_self ||= component_class.instance_methods(false).grep(/^call(_|$)/)
228
234
  end
229
235
 
230
236
  def variants
@@ -258,6 +264,7 @@ module ViewComponent
258
264
 
259
265
  if handler.method(:call).parameters.length > 1
260
266
  handler.call(component_class, template)
267
+ # :nocov:
261
268
  else
262
269
  handler.call(
263
270
  OpenStruct.new(
@@ -267,6 +274,7 @@ module ViewComponent
267
274
  )
268
275
  )
269
276
  end
277
+ # :nocov:
270
278
  end
271
279
 
272
280
  def call_method_name(variant)
@@ -80,6 +80,9 @@ module ViewComponent
80
80
  initializer "view_component.monkey_patch_render" do |app|
81
81
  next if Rails.version.to_f >= 6.1 || !app.config.view_component.render_monkey_patch_enabled
82
82
 
83
+ # :nocov:
84
+ ViewComponent::Deprecation.deprecation_warning("Monkey patching `render`", "ViewComponent 4.0 will remove the `render` monkey patch")
85
+
83
86
  ActiveSupport.on_load(:action_view) do
84
87
  require "view_component/render_monkey_patch"
85
88
  ActionView::Base.prepend ViewComponent::RenderMonkeyPatch
@@ -91,11 +94,15 @@ module ViewComponent
91
94
  ActionController::Base.prepend ViewComponent::RenderingMonkeyPatch
92
95
  ActionController::Base.prepend ViewComponent::RenderToStringMonkeyPatch
93
96
  end
97
+ # :nocov:
94
98
  end
95
99
 
96
100
  initializer "view_component.include_render_component" do |_app|
97
101
  next if Rails.version.to_f >= 6.1
98
102
 
103
+ # :nocov:
104
+ ViewComponent::Deprecation.deprecation_warning("using `render_component`", "ViewComponent 4.0 will remove `render_component`")
105
+
99
106
  ActiveSupport.on_load(:action_view) do
100
107
  require "view_component/render_component_helper"
101
108
  ActionView::Base.include ViewComponent::RenderComponentHelper
@@ -107,14 +114,19 @@ module ViewComponent
107
114
  ActionController::Base.include ViewComponent::RenderingComponentHelper
108
115
  ActionController::Base.include ViewComponent::RenderComponentToStringHelper
109
116
  end
117
+ # :nocov:
110
118
  end
111
119
 
112
120
  initializer "static assets" do |app|
113
- if app.config.view_component.show_previews
121
+ if serve_static_preview_assets?(app.config)
114
122
  app.middleware.use(::ActionDispatch::Static, "#{root}/app/assets/vendor")
115
123
  end
116
124
  end
117
125
 
126
+ def serve_static_preview_assets?(app_config)
127
+ app_config.view_component.show_previews && app_config.public_file_server.enabled
128
+ end
129
+
118
130
  initializer "compiler mode" do |_app|
119
131
  ViewComponent::Compiler.mode = if Rails.env.development? || Rails.env.test?
120
132
  ViewComponent::Compiler::DEVELOPMENT_MODE
@@ -152,6 +164,16 @@ module ViewComponent
152
164
  end
153
165
  end
154
166
 
167
+ # :nocov:
168
+ if RUBY_VERSION < "3.0.0"
169
+ ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 3.0.0", "ViewComponent 4.0 will remove support for Ruby versions < 3.0.0 ")
170
+ end
171
+
172
+ if Rails.version.to_f < 6.1
173
+ ViewComponent::Deprecation.deprecation_warning("Support for Rails versions < 6.1", "ViewComponent 4.0 will remove support for Rails versions < 6.1 ")
174
+ end
175
+ # :nocov:
176
+
155
177
  app.executor.to_run :before do
156
178
  CompileCache.invalidate! unless ActionView::Base.cache_template_loading
157
179
  end
@@ -104,7 +104,10 @@ module ViewComponent
104
104
  "string, or callable (that is proc, lambda, etc)"
105
105
  end
106
106
 
107
- class SlotPredicateNameError < StandardError
107
+ class InvalidSlotNameError < StandardError
108
+ end
109
+
110
+ class SlotPredicateNameError < InvalidSlotNameError
108
111
  MESSAGE =
109
112
  "COMPONENT declares a slot named SLOT_NAME, which ends with a question mark.\n\n" \
110
113
  "This isn't allowed because the ViewComponent framework already provides predicate " \
@@ -126,7 +129,7 @@ module ViewComponent
126
129
  end
127
130
  end
128
131
 
129
- class ReservedSingularSlotNameError < StandardError
132
+ class ReservedSingularSlotNameError < InvalidSlotNameError
130
133
  MESSAGE =
131
134
  "COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \
132
135
  "To fix this issue, choose a different name."
@@ -136,7 +139,7 @@ module ViewComponent
136
139
  end
137
140
  end
138
141
 
139
- class ReservedPluralSlotNameError < StandardError
142
+ class ReservedPluralSlotNameError < InvalidSlotNameError
140
143
  MESSAGE =
141
144
  "COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \
142
145
  "To fix this issue, choose a different name."
@@ -146,6 +149,16 @@ module ViewComponent
146
149
  end
147
150
  end
148
151
 
152
+ class UncountableSlotNameError < InvalidSlotNameError
153
+ MESSAGE =
154
+ "COMPONENT declares a slot named SLOT_NAME, which is an uncountable word\n\n" \
155
+ "To fix this issue, choose a different name."
156
+
157
+ def initialize(klass_name, slot_name)
158
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
159
+ end
160
+ end
161
+
149
162
  class ContentAlreadySetForPolymorphicSlotError < StandardError
150
163
  MESSAGE = "Content for slot SLOT_NAME has already been provided."
151
164
 
@@ -187,6 +200,7 @@ module ViewComponent
187
200
  "`#controller` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
188
201
  end
189
202
 
203
+ # :nocov:
190
204
  class NoMatchingTemplatesForPreviewError < StandardError
191
205
  MESSAGE = "Found 0 matches for templates for TEMPLATE_IDENTIFIER."
192
206
 
@@ -202,6 +216,7 @@ module ViewComponent
202
216
  super(MESSAGE.gsub("TEMPLATE_IDENTIFIER", template_identifier))
203
217
  end
204
218
  end
219
+ # :nocov:
205
220
 
206
221
  class SystemTestControllerOnlyAllowedInTestError < BaseError
207
222
  MESSAGE = "ViewComponent SystemTest controller must only be called in a test environment for security reasons."
@@ -4,7 +4,11 @@ require "active_support/descendants_tracker"
4
4
 
5
5
  module ViewComponent # :nodoc:
6
6
  class Preview
7
- include Rails.application.routes.url_helpers if defined?(Rails.application.routes.url_helpers)
7
+ if defined?(Rails.application.routes.url_helpers)
8
+ # Workaround from https://stackoverflow.com/questions/20853526/make-yard-ignore-certain-class-extensions to appease YARD
9
+ send(:include, Rails.application.routes.url_helpers)
10
+ end
11
+
8
12
  include ActionView::Helpers::TagHelper
9
13
  include ActionView::Helpers::AssetTagHelper
10
14
  extend ActiveSupport::DescendantsTracker
@@ -7,7 +7,8 @@ namespace :view_component do
7
7
  # :nocov:
8
8
  require "rails/code_statistics"
9
9
 
10
- ::STATS_DIRECTORIES << ["ViewComponents", ViewComponent::Base.view_component_path]
10
+ dir = ViewComponent::Base.view_component_path
11
+ ::STATS_DIRECTORIES << ["ViewComponents", dir] if File.directory?(Rails.root + dir)
11
12
  # :nocov:
12
13
  end
13
14
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
+ require "active_support/inflector/inflections"
4
5
  require "view_component/slot"
5
6
 
6
7
  module ViewComponent
@@ -92,11 +93,11 @@ module ViewComponent
92
93
  get_slot(slot_name)
93
94
  end
94
95
 
95
- define_method "#{slot_name}?" do
96
+ define_method :"#{slot_name}?" do
96
97
  get_slot(slot_name).present?
97
98
  end
98
99
 
99
- define_method "with_#{slot_name}_content" do |content|
100
+ define_method :"with_#{slot_name}_content" do |content|
100
101
  send(setter_method_name) { content.to_s }
101
102
 
102
103
  self
@@ -159,7 +160,7 @@ module ViewComponent
159
160
  end
160
161
  ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
161
162
 
162
- define_method "with_#{singular_name}_content" do |content|
163
+ define_method :"with_#{singular_name}_content" do |content|
163
164
  send(setter_method_name) { content.to_s }
164
165
 
165
166
  self
@@ -179,7 +180,7 @@ module ViewComponent
179
180
  get_slot(slot_name)
180
181
  end
181
182
 
182
- define_method "#{slot_name}?" do
183
+ define_method :"#{slot_name}?" do
183
184
  get_slot(slot_name).present?
184
185
  end
185
186
 
@@ -210,7 +211,7 @@ module ViewComponent
210
211
  get_slot(slot_name)
211
212
  end
212
213
 
213
- define_method("#{slot_name}?") do
214
+ define_method(:"#{slot_name}?") do
214
215
  get_slot(slot_name).present?
215
216
  end
216
217
 
@@ -245,7 +246,7 @@ module ViewComponent
245
246
  end
246
247
  ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
247
248
 
248
- define_method "with_#{poly_slot_name}_content" do |content|
249
+ define_method :"with_#{poly_slot_name}_content" do |content|
249
250
  send(setter_method_name) { content.to_s }
250
251
 
251
252
  self
@@ -295,6 +296,8 @@ module ViewComponent
295
296
  raise ReservedPluralSlotNameError.new(name, slot_name)
296
297
  end
297
298
 
299
+ raise_if_slot_name_uncountable(slot_name)
300
+ raise_if_slot_conflicts_with_call(slot_name)
298
301
  raise_if_slot_ends_with_question_mark(slot_name)
299
302
  raise_if_slot_registered(slot_name)
300
303
  end
@@ -308,6 +311,7 @@ module ViewComponent
308
311
  raise ReservedSingularSlotNameError.new(name, slot_name)
309
312
  end
310
313
 
314
+ raise_if_slot_conflicts_with_call(slot_name)
311
315
  raise_if_slot_ends_with_question_mark(slot_name)
312
316
  raise_if_slot_registered(slot_name)
313
317
  end
@@ -320,7 +324,20 @@ module ViewComponent
320
324
  end
321
325
 
322
326
  def raise_if_slot_ends_with_question_mark(slot_name)
323
- raise SlotPredicateNameError.new(name, slot_name) if slot_name.to_s.ends_with?("?")
327
+ raise SlotPredicateNameError.new(name, slot_name) if slot_name.to_s.end_with?("?")
328
+ end
329
+
330
+ def raise_if_slot_conflicts_with_call(slot_name)
331
+ if slot_name.start_with?("call_")
332
+ raise InvalidSlotNameError, "Slot cannot start with 'call_'. Please rename #{slot_name}"
333
+ end
334
+ end
335
+
336
+ def raise_if_slot_name_uncountable(slot_name)
337
+ slot_name = slot_name.to_s
338
+ if slot_name.pluralize == slot_name.singularize
339
+ raise UncountableSlotNameError.new(name, slot_name)
340
+ end
324
341
  end
325
342
  end
326
343
 
@@ -48,10 +48,14 @@ module ViewComponent
48
48
  @rendered_content =
49
49
  if Rails.version.to_f >= 6.1
50
50
  vc_test_controller.view_context.render(component, args, &block)
51
+
52
+ # :nocov:
51
53
  else
52
54
  vc_test_controller.view_context.render_component(component, &block)
53
55
  end
54
56
 
57
+ # :nocov:
58
+
55
59
  Nokogiri::HTML.fragment(@rendered_content)
56
60
  end
57
61
 
@@ -163,30 +167,47 @@ module ViewComponent
163
167
  # end
164
168
  # ```
165
169
  #
166
- # @param path [String] The path to set for the current request.
170
+ # To specify a request method, pass the method param:
171
+ #
172
+ # ```ruby
173
+ # with_request_url("/users/42", method: "POST") do
174
+ # render_inline(MyComponent.new)
175
+ # end
176
+ # ```
177
+ #
178
+ # @param full_path [String] The path to set for the current request.
167
179
  # @param host [String] The host to set for the current request.
168
- def with_request_url(path, host: nil)
180
+ # @param method [String] The request method to set for the current request.
181
+ def with_request_url(full_path, host: nil, method: nil, format: :html)
169
182
  old_request_host = vc_test_request.host
183
+ old_request_method = vc_test_request.request_method
170
184
  old_request_path_info = vc_test_request.path_info
171
185
  old_request_path_parameters = vc_test_request.path_parameters
172
186
  old_request_query_parameters = vc_test_request.query_parameters
173
187
  old_request_query_string = vc_test_request.query_string
188
+ old_request_format = vc_test_request.format.symbol
174
189
  old_controller = defined?(@vc_test_controller) && @vc_test_controller
175
190
 
176
- path, query = path.split("?", 2)
191
+ path, query = full_path.split("?", 2)
192
+ vc_test_request.instance_variable_set(:@fullpath, full_path)
193
+ vc_test_request.instance_variable_set(:@original_fullpath, full_path)
177
194
  vc_test_request.host = host if host
195
+ vc_test_request.request_method = method if method
178
196
  vc_test_request.path_info = path
179
197
  vc_test_request.path_parameters = Rails.application.routes.recognize_path_with_request(vc_test_request, path, {})
180
198
  vc_test_request.set_header("action_dispatch.request.query_parameters",
181
199
  Rack::Utils.parse_nested_query(query).with_indifferent_access)
182
200
  vc_test_request.set_header(Rack::QUERY_STRING, query)
201
+ vc_test_request.format = format
183
202
  yield
184
203
  ensure
185
204
  vc_test_request.host = old_request_host
205
+ vc_test_request.request_method = old_request_method
186
206
  vc_test_request.path_info = old_request_path_info
187
207
  vc_test_request.path_parameters = old_request_path_parameters
188
208
  vc_test_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
189
209
  vc_test_request.set_header(Rack::QUERY_STRING, old_request_query_string)
210
+ vc_test_request.format = old_request_format
190
211
  @vc_test_controller = old_controller
191
212
  end
192
213
 
@@ -10,6 +10,7 @@ module ViewComponent
10
10
  extend ActiveSupport::Concern
11
11
 
12
12
  HTML_SAFE_TRANSLATION_KEY = /(?:_|\b)html\z/
13
+ TRANSLATION_EXTENSIONS = %w[yml yaml].freeze
13
14
 
14
15
  included do
15
16
  class_attribute :i18n_backend, instance_writer: false, instance_predicate: false
@@ -23,9 +24,16 @@ module ViewComponent
23
24
  def build_i18n_backend
24
25
  return if compiled?
25
26
 
26
- self.i18n_backend = if (translation_files = sidecar_files(%w[yml yaml])).any?
27
- # Returning nil cleans up if translations file has been removed since the last compilation
27
+ # We need to load the translations files from the ancestors so a component
28
+ # can inherit translations from its parent and is able to overwrite them.
29
+ translation_files = ancestors.reverse_each.with_object([]) do |ancestor, files|
30
+ if ancestor.is_a?(Class) && ancestor < ViewComponent::Base
31
+ files.concat(ancestor.sidecar_files(TRANSLATION_EXTENSIONS))
32
+ end
33
+ end
28
34
 
35
+ # In development it will become nil if the translations file is removed
36
+ self.i18n_backend = if translation_files.any?
29
37
  I18nBackend.new(
30
38
  i18n_scope: i18n_scope,
31
39
  load_paths: translation_files
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent::UseHelpers
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def use_helpers(*args)
8
+ args.each do |helper_method|
9
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
10
+ def #{helper_method}(*args, &block)
11
+ raise HelpersCalledBeforeRenderError if view_context.nil?
12
+ __vc_original_view_context.#{helper_method}(*args, &block)
13
+ end
14
+ RUBY
15
+
16
+ ruby2_keywords(helper_method) if respond_to?(:ruby2_keywords, true)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 3
6
- MINOR = 7
6
+ MINOR = 9
7
7
  PATCH = 0
8
8
  PRE = nil
9
9
 
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.7.0
4
+ version: 3.9.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: 2023-10-30 00:00:00.000000000 Z
11
+ date: 2024-01-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -78,14 +78,14 @@ dependencies:
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: 2.12.0
81
+ version: 2.13.0
82
82
  type: :development
83
83
  prerelease: false
84
84
  version_requirements: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
- version: 2.12.0
88
+ version: 2.13.0
89
89
  - !ruby/object:Gem::Dependency
90
90
  name: better_html
91
91
  requirement: !ruby/object:Gem::Requirement
@@ -302,14 +302,14 @@ dependencies:
302
302
  requirements:
303
303
  - - "~>"
304
304
  - !ruby/object:Gem::Version
305
- version: 0.9.25
305
+ version: 0.9.34
306
306
  type: :development
307
307
  prerelease: false
308
308
  version_requirements: !ruby/object:Gem::Requirement
309
309
  requirements:
310
310
  - - "~>"
311
311
  - !ruby/object:Gem::Version
312
- version: 0.9.25
312
+ version: 0.9.34
313
313
  - !ruby/object:Gem::Dependency
314
314
  name: yard-activesupport-concern
315
315
  requirement: !ruby/object:Gem::Requirement
@@ -395,6 +395,7 @@ files:
395
395
  - lib/view_component/test_case.rb
396
396
  - lib/view_component/test_helpers.rb
397
397
  - lib/view_component/translatable.rb
398
+ - lib/view_component/use_helpers.rb
398
399
  - lib/view_component/version.rb
399
400
  - lib/view_component/with_content_helper.rb
400
401
  - lib/yard/mattr_accessor_handler.rb
@@ -420,7 +421,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
420
421
  - !ruby/object:Gem::Version
421
422
  version: '0'
422
423
  requirements: []
423
- rubygems_version: 3.4.5
424
+ rubygems_version: 3.5.3
424
425
  signing_key:
425
426
  specification_version: 4
426
427
  summary: A framework for building reusable, testable & encapsulated view components