view_component 3.5.0 → 3.10.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: ce590ddce7011ae4362dde0733f78ba65992c31add6bac215555515d02ae1dc6
4
- data.tar.gz: df4867599a61d2757e2f226a3c6b187981772e2d225fc442f9184484c0423e64
3
+ metadata.gz: d3f4f53b04ea58ed971bef33e1bea8ab65c2564c64a49927ca5db90c118d23bc
4
+ data.tar.gz: 1a6b6024ffd5baf72cb80726b0853cf3fcab80d43505c87648cc57d6726e9185
5
5
  SHA512:
6
- metadata.gz: 0a39d71f089c0f4285dc1eecd324e44afc73f55565002a45c2d37292cc4fa8b05713f7237f04b530617ddc0bb2ccd08fab0c7d010400790dc00daf2d6c9c0546
7
- data.tar.gz: 252a63a4ed7eefd7db529dc93081d3ccfb78555ac1d051540f7a69829a41d4d22fd94557fa2fe9eb88c012bd8dccfd2ceb813aae2582877e387a2df347e2d915
6
+ metadata.gz: 0f75de114591fca8e662e6fa15086c0b6b11c5862a05e68abff123ba19b7b23b7f67e7b65f8adca8a78688011254117643e9f0e76b92e5a0c0c51d82ecb85194
7
+ data.tar.gz: 9771fc3c30b8472bb2f0793573926cd7b05e8f991091af18f8e51cfd5370ea8ac7f1eff40488eb0e1434a81683e901fcd74659763620f2e5ae8a26d4db0c943b
@@ -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,171 @@ nav_order: 5
10
10
 
11
11
  ## main
12
12
 
13
+ ## 3.10.0
14
+
15
+ * Fix html escaping in `#call` for non-strings.
16
+
17
+ *Reegan Viljoen, Cameron Dutro*
18
+
19
+ * Add `output_preamble` to match `output_postamble`, using the same safety checks.
20
+
21
+ *Kali Donovan, Michael Daross*
22
+
23
+ * Exclude html escaping of I18n reserved keys with `I18n::RESERVED_KEYS` rather than `I18n.reserved_keys_pattern`.
24
+
25
+ *Nick Coyne*
26
+
27
+ * Update CI configuration to use `Appraisal`.
28
+
29
+ *Hans Lemuet, Simon Fish*
30
+
31
+ ## 3.9.0
32
+
33
+ * Don’t break `rails stats` if ViewComponent path is missing.
34
+
35
+ *Claudio Baccigalupo*
36
+
37
+ * Add deprecation warnings for EOL ruby and Rails versions and patches associated with them.
38
+
39
+ *Reegan Viljoen*
40
+
41
+ * Add support for Ruby 3.3.
42
+
43
+ *Reegan Viljoen*
44
+
45
+ * Allow translations to be inherited and overridden in subclasses.
46
+
47
+ *Elia Schito*
48
+
49
+ * Resolve console warnings when running test suite.
50
+
51
+ *Joel Hawksley*
52
+
53
+ * Fix spelling in a local variable.
54
+
55
+ *Olle Jonsson*
56
+
57
+ * Avoid duplicating rendered string when `output_postamble` is blank.
58
+
59
+ *Mitchell Henke*
60
+
61
+ * Ensure HTML output safety.
62
+
63
+ *Cameron Dutro*
64
+
65
+ ## 3.8.0
66
+
67
+ * Use correct value for the `config.action_dispatch.show_exceptions` config option for edge Rails.
68
+
69
+ *Cameron Dutro*
70
+
71
+ * Remove unsupported versions of Rails & Ruby from CI matrix.
72
+
73
+ *Reegan Viljoen*
74
+
75
+ * Raise error when uncountable slot names are used in `renders_many`
76
+
77
+ *Hugo Chantelauze*
78
+ *Reegan Viljoen*
79
+
80
+ * Replace usage of `String#ends_with?` with `String#end_with?` to reduce the dependency on ActiveSupport core extensions.
81
+
82
+ *halo*
83
+
84
+ * Don't add ActionDispatch::Static middleware unless `public_file_server.enabled`.
85
+
86
+ *Daniel Gonzalez*
87
+ *Reegan Viljoen*
88
+
89
+ * Resolve an issue where slots starting with `call` would cause a `NameError`
90
+
91
+ *Blake Williams*
92
+
93
+ * Add `use_helper` API.
94
+
95
+ *Reegan Viljoen*
96
+
97
+ * Fix bug where the `Rails` module wasn't being searched from the root namespace.
98
+
99
+ *Zenéixe*
100
+
101
+ * Fix bug where `#with_request_url`, set the incorrect `request.fullpath`.
102
+
103
+ *Nachiket Pusalkar*
104
+
105
+ * Allow setting method when using the `with_request_url` test helper.
106
+
107
+ *Andrew Duthie*
108
+
109
+ ## 3.7.0
110
+
111
+ * Support Rails 7.1 in CI.
112
+
113
+ *Reegan Viljoen*
114
+ *Cameron Dutro*
115
+
116
+ * Document the capture compatibility patch on the Known issues page.
117
+
118
+ *Simon Fish*
119
+
120
+ * Add Simundia to list of companies using ViewComponent.
121
+
122
+ *Alexandre Ignjatovic*
123
+
124
+ * Reduce UnboundMethod objects by memoizing initialize_parameters.
125
+
126
+ *Rainer Borene*
127
+
128
+ * Improve docs about inline templates interpolation.
129
+
130
+ *Hans Lemuet*
131
+
132
+ * Update generators.md to clarify the way of changing `config.view_component.view_component_path`.
133
+
134
+ *Shozo Hatta*
135
+
136
+ * Attempt to fix Ferrum timeout errors by creating driver with unique name.
137
+
138
+ *Cameron Dutro*
139
+
140
+ ## 3.6.0
141
+
142
+ * Refer to `helpers` in `NameError` message in development and test environments.
143
+
144
+ *Simon Fish*
145
+
146
+ * Fix API documentation and revert unnecessary change in `preview.rb`.
147
+
148
+ *Richard Macklin*
149
+
150
+ * Initialize ViewComponent::Config with defaults before framework load.
151
+
152
+ *Simon Fish*
153
+
154
+ * Add 3.2 to the list of Ruby CI versions
155
+
156
+ *Igor Drozdov*
157
+
158
+ * Stop running PVC's `docs:preview` rake task in CI, as the old docsite has been removed.
159
+
160
+ *Cameron Dutro*
161
+
162
+ * Minor testing documentation improvement.
163
+
164
+ *Travis Gaff*
165
+
166
+ * Add SearchApi to users list.
167
+
168
+ *Sebastjan Prachovskij*
169
+
170
+ * Fix `#with_request_url` to ensure `request.query_parameters` is an instance of ActiveSupport::HashWithIndifferentAccess.
171
+
172
+ *milk1000cc*
173
+
174
+ * Add PeopleForce to list of companies using ViewComponent.
175
+
176
+ *Volodymyr Khandiuk*
177
+
13
178
  ## 3.5.0
14
179
 
15
180
  * Add Skroutz to users list.
@@ -112,6 +277,17 @@ This release makes the following breaking changes, many of which have long been
112
277
 
113
278
  *Joel Hawksley*
114
279
 
280
+ For example:
281
+
282
+ ```diff
283
+ <%= render BlogComponent.new do |component| %>
284
+ - <% component.header do %>
285
+ + <% component.with_header do %>
286
+ <%= link_to "My blog", root_path %>
287
+ <% end %>
288
+ <% end %>
289
+ ```
290
+
115
291
  * BREAKING: Remove deprecated SlotsV1 in favor of current SlotsV2.
116
292
 
117
293
  *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
@@ -22,12 +23,8 @@ module ViewComponent
22
23
  #
23
24
  # @return [ActiveSupport::OrderedOptions]
24
25
  def config
25
- @config ||= ActiveSupport::OrderedOptions.new
26
+ ViewComponent::Config.current
26
27
  end
27
-
28
- # Replaces the entire config. You shouldn't need to use this directly
29
- # unless you're building a `ViewComponent::Config` elsewhere.
30
- attr_writer :config
31
28
  end
32
29
 
33
30
  include ViewComponent::InlineTemplate
@@ -107,7 +104,14 @@ module ViewComponent
107
104
  before_render
108
105
 
109
106
  if render?
110
- render_template_for(@__vc_variant).to_s + output_postamble
107
+ # Avoid allocating new string when output_preamble and output_postamble are blank
108
+ rendered_template = safe_render_template_for(@__vc_variant).to_s
109
+
110
+ if output_preamble.blank? && output_postamble.blank?
111
+ rendered_template
112
+ else
113
+ safe_output_preamble + rendered_template + safe_output_postamble
114
+ end
111
115
  else
112
116
  ""
113
117
  end
@@ -154,11 +158,18 @@ module ViewComponent
154
158
  end
155
159
  end
156
160
 
161
+ # Optional content to be returned before the rendered template.
162
+ #
163
+ # @return [String]
164
+ def output_preamble
165
+ @@default_output_preamble ||= "".html_safe
166
+ end
167
+
157
168
  # Optional content to be returned after the rendered template.
158
169
  #
159
170
  # @return [String]
160
171
  def output_postamble
161
- ""
172
+ @@default_output_postamble ||= "".html_safe
162
173
  end
163
174
 
164
175
  # Called before rendering the component. Override to perform operations that
@@ -223,6 +234,22 @@ module ViewComponent
223
234
  @__vc_helpers ||= __vc_original_view_context || controller.view_context
224
235
  end
225
236
 
237
+ if ::Rails.env.development? || ::Rails.env.test?
238
+ # @private
239
+ def method_missing(method_name, *args) # rubocop:disable Style/MissingRespondToMissing
240
+ super
241
+ rescue => e # rubocop:disable Style/RescueStandardError
242
+ e.set_backtrace e.backtrace.tap(&:shift)
243
+ raise e, <<~MESSAGE.chomp if view_context && e.is_a?(NameError) && helpers.respond_to?(method_name)
244
+ #{e.message}
245
+
246
+ You may be trying to call a method provided as a view helper. Did you mean `helpers.#{method_name}'?
247
+ MESSAGE
248
+
249
+ raise
250
+ end
251
+ end
252
+
226
253
  # Exposes .virtual_path as an instance method
227
254
  #
228
255
  # @private
@@ -289,6 +316,44 @@ module ViewComponent
289
316
  defined?(@__vc_content_evaluated) && @__vc_content_evaluated
290
317
  end
291
318
 
319
+ def maybe_escape_html(text)
320
+ return text if request && !request.format.html?
321
+ return text if text.blank?
322
+
323
+ if text.html_safe?
324
+ text
325
+ else
326
+ yield
327
+ html_escape(text)
328
+ end
329
+ end
330
+
331
+ def safe_render_template_for(variant)
332
+ if compiler.renders_template_for_variant?(variant)
333
+ render_template_for(variant)
334
+ else
335
+ maybe_escape_html(render_template_for(variant)) do
336
+ Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. The output will be automatically escaped, but you may want to investigate.")
337
+ end
338
+ end
339
+ end
340
+
341
+ def safe_output_preamble
342
+ maybe_escape_html(output_preamble) do
343
+ 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.")
344
+ end
345
+ end
346
+
347
+ def safe_output_postamble
348
+ maybe_escape_html(output_postamble) do
349
+ 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.")
350
+ end
351
+ end
352
+
353
+ def compiler
354
+ @compiler ||= self.class.compiler
355
+ end
356
+
292
357
  # Set the controller used for testing components:
293
358
  #
294
359
  # ```ruby
@@ -542,6 +607,7 @@ module ViewComponent
542
607
  # @param parameter [Symbol] The parameter name used when rendering elements of a collection.
543
608
  def with_collection_parameter(parameter)
544
609
  @provided_collection_parameter = parameter
610
+ @initialize_parameters = nil
545
611
  end
546
612
 
547
613
  # Strips trailing whitespace from templates before compiling them.
@@ -603,7 +669,7 @@ module ViewComponent
603
669
 
604
670
  # @private
605
671
  def collection_counter_parameter
606
- "#{collection_parameter}_counter".to_sym
672
+ :"#{collection_parameter}_counter"
607
673
  end
608
674
 
609
675
  # @private
@@ -613,7 +679,7 @@ module ViewComponent
613
679
 
614
680
  # @private
615
681
  def collection_iteration_parameter
616
- "#{collection_parameter}_iteration".to_sym
682
+ :"#{collection_parameter}_iteration"
617
683
  end
618
684
 
619
685
  # @private
@@ -637,7 +703,7 @@ module ViewComponent
637
703
  end
638
704
 
639
705
  def initialize_parameters
640
- instance_method(:initialize).parameters
706
+ @initialize_parameters ||= instance_method(:initialize).parameters
641
707
  end
642
708
 
643
709
  def provided_collection_parameter
@@ -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)
@@ -167,6 +167,14 @@ module ViewComponent
167
167
  end
168
168
  end
169
169
 
170
+ # @!attribute current
171
+ # @return [ViewComponent::Config]
172
+ # Returns the current ViewComponent::Config. This is persisted against this
173
+ # class so that config options remain accessible before the rest of
174
+ # ViewComponent has loaded. Defaults to an instance of ViewComponent::Config
175
+ # with all other documented defaults set.
176
+ class_attribute :current, default: defaults, instance_predicate: false
177
+
170
178
  def initialize
171
179
  @config = self.class.defaults
172
180
  end
@@ -6,7 +6,7 @@ require "view_component/deprecation"
6
6
 
7
7
  module ViewComponent
8
8
  class Engine < Rails::Engine # :nodoc:
9
- config.view_component = ViewComponent::Config.defaults
9
+ config.view_component = ViewComponent::Config.current
10
10
 
11
11
  rake_tasks do
12
12
  load "view_component/rails/tasks/view_component.rake"
@@ -15,6 +15,9 @@ module ViewComponent
15
15
  initializer "view_component.set_configs" do |app|
16
16
  options = app.config.view_component
17
17
 
18
+ %i[generate preview_controller preview_route show_previews_source].each do |config_option|
19
+ options[config_option] ||= ViewComponent::Base.public_send(config_option)
20
+ end
18
21
  options.instrumentation_enabled = false if options.instrumentation_enabled.nil?
19
22
  options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
20
23
  options.show_previews = (Rails.env.development? || Rails.env.test?) if options.show_previews.nil?
@@ -37,8 +40,6 @@ module ViewComponent
37
40
 
38
41
  initializer "view_component.enable_instrumentation" do |app|
39
42
  ActiveSupport.on_load(:view_component) do
40
- Base.config = app.config.view_component
41
-
42
43
  if app.config.view_component.instrumentation_enabled.present?
43
44
  # :nocov: Re-executing the below in tests duplicates initializers and causes order-dependent failures.
44
45
  ViewComponent::Base.prepend(ViewComponent::Instrumentation)
@@ -79,6 +80,9 @@ module ViewComponent
79
80
  initializer "view_component.monkey_patch_render" do |app|
80
81
  next if Rails.version.to_f >= 6.1 || !app.config.view_component.render_monkey_patch_enabled
81
82
 
83
+ # :nocov:
84
+ ViewComponent::Deprecation.deprecation_warning("Monkey patching `render`", "ViewComponent 4.0 will remove the `render` monkey patch")
85
+
82
86
  ActiveSupport.on_load(:action_view) do
83
87
  require "view_component/render_monkey_patch"
84
88
  ActionView::Base.prepend ViewComponent::RenderMonkeyPatch
@@ -90,11 +94,15 @@ module ViewComponent
90
94
  ActionController::Base.prepend ViewComponent::RenderingMonkeyPatch
91
95
  ActionController::Base.prepend ViewComponent::RenderToStringMonkeyPatch
92
96
  end
97
+ # :nocov:
93
98
  end
94
99
 
95
100
  initializer "view_component.include_render_component" do |_app|
96
101
  next if Rails.version.to_f >= 6.1
97
102
 
103
+ # :nocov:
104
+ ViewComponent::Deprecation.deprecation_warning("using `render_component`", "ViewComponent 4.0 will remove `render_component`")
105
+
98
106
  ActiveSupport.on_load(:action_view) do
99
107
  require "view_component/render_component_helper"
100
108
  ActionView::Base.include ViewComponent::RenderComponentHelper
@@ -106,14 +114,19 @@ module ViewComponent
106
114
  ActionController::Base.include ViewComponent::RenderingComponentHelper
107
115
  ActionController::Base.include ViewComponent::RenderComponentToStringHelper
108
116
  end
117
+ # :nocov:
109
118
  end
110
119
 
111
120
  initializer "static assets" do |app|
112
- if app.config.view_component.show_previews
121
+ if serve_static_preview_assets?(app.config)
113
122
  app.middleware.use(::ActionDispatch::Static, "#{root}/app/assets/vendor")
114
123
  end
115
124
  end
116
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
+
117
130
  initializer "compiler mode" do |_app|
118
131
  ViewComponent::Compiler.mode = if Rails.env.development? || Rails.env.test?
119
132
  ViewComponent::Compiler::DEVELOPMENT_MODE
@@ -151,6 +164,16 @@ module ViewComponent
151
164
  end
152
165
  end
153
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
+
154
177
  app.executor.to_run :before do
155
178
  CompileCache.invalidate! unless ActionView::Base.cache_template_loading
156
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,29 +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
- vc_test_request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_nested_query(query))
198
+ vc_test_request.set_header("action_dispatch.request.query_parameters",
199
+ Rack::Utils.parse_nested_query(query).with_indifferent_access)
181
200
  vc_test_request.set_header(Rack::QUERY_STRING, query)
201
+ vc_test_request.format = format
182
202
  yield
183
203
  ensure
184
204
  vc_test_request.host = old_request_host
205
+ vc_test_request.request_method = old_request_method
185
206
  vc_test_request.path_info = old_request_path_info
186
207
  vc_test_request.path_parameters = old_request_path_parameters
187
208
  vc_test_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
188
209
  vc_test_request.set_header(Rack::QUERY_STRING, old_request_query_string)
210
+ vc_test_request.format = old_request_format
189
211
  @vc_test_controller = old_controller
190
212
  end
191
213
 
@@ -233,9 +255,11 @@ module ViewComponent
233
255
 
234
256
  def __vc_test_helpers_preview_class
235
257
  result = if respond_to?(:described_class)
258
+ # :nocov:
236
259
  raise "`render_preview` expected a described_class, but it is nil." if described_class.nil?
237
260
 
238
261
  "#{described_class}Preview"
262
+ # :nocov:
239
263
  else
240
264
  self.class.name.gsub("Test", "Preview")
241
265
  end
@@ -243,5 +267,6 @@ module ViewComponent
243
267
  rescue NameError
244
268
  raise NameError, "`render_preview` expected to find #{result}, but it does not exist."
245
269
  end
270
+ # :nocov:
246
271
  end
247
272
  end
@@ -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
@@ -130,8 +138,7 @@ module ViewComponent
130
138
  end
131
139
 
132
140
  def html_escape_translation_options!(options)
133
- options.each do |name, value|
134
- next if ::I18n.reserved_keys_pattern.match?(name)
141
+ options.except(*::I18n::RESERVED_KEYS).each do |name, value|
135
142
  next if name == :count && value.is_a?(Numeric)
136
143
 
137
144
  options[name] = ERB::Util.html_escape(value.to_s)
@@ -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 = 5
6
+ MINOR = 10
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.5.0
4
+ version: 3.10.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-07-24 00:00:00.000000000 Z
11
+ date: 2024-01-09 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