view_component 3.5.0 → 3.10.0

Sign up to get free protection for your applications and to get access to all the features.
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