view_component 3.23.2 → 4.12.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/view_component/preview_actions.rb +11 -14
  3. data/app/controllers/view_components_system_test_controller.rb +25 -19
  4. data/app/views/test_mailer/test_asset_email.html.erb +1 -0
  5. data/app/views/test_mailer/test_url_email.html.erb +1 -0
  6. data/app/views/view_components/preview.html.erb +1 -9
  7. data/docs/CHANGELOG.md +612 -108
  8. data/lib/{rails/generators → generators/view_component}/abstract_generator.rb +2 -2
  9. data/lib/{rails/generators → generators/view_component}/component/component_generator.rb +17 -4
  10. data/lib/{rails/generators → generators/view_component}/component/templates/component.rb.tt +6 -1
  11. data/lib/{rails/generators/erb/component_generator.rb → generators/view_component/erb/erb_generator.rb} +4 -3
  12. data/lib/{rails/generators/haml/component_generator.rb → generators/view_component/haml/haml_generator.rb} +3 -3
  13. data/lib/{rails/generators/locale/component_generator.rb → generators/view_component/locale/locale_generator.rb} +3 -3
  14. data/lib/{rails/generators/preview/component_generator.rb → generators/view_component/preview/preview_generator.rb} +3 -3
  15. data/lib/{rails/generators/rspec/component_generator.rb → generators/view_component/rspec/rspec_generator.rb} +3 -3
  16. data/lib/{rails/generators/slim/component_generator.rb → generators/view_component/slim/slim_generator.rb} +3 -3
  17. data/lib/{rails/generators/stimulus/component_generator.rb → generators/view_component/stimulus/stimulus_generator.rb} +3 -3
  18. data/lib/generators/view_component/tailwindcss/tailwindcss_generator.rb +11 -0
  19. data/lib/{rails/generators/test_unit/component_generator.rb → generators/view_component/test_unit/test_unit_generator.rb} +2 -2
  20. data/lib/view_component/base.rb +252 -163
  21. data/lib/view_component/collection.rb +33 -35
  22. data/lib/view_component/compiler.rb +55 -80
  23. data/lib/view_component/config.rb +55 -85
  24. data/lib/view_component/configurable.rb +19 -3
  25. data/lib/view_component/deprecation.rb +1 -1
  26. data/lib/view_component/engine.rb +46 -106
  27. data/lib/view_component/errors.rb +16 -34
  28. data/lib/view_component/inline_template.rb +4 -4
  29. data/lib/view_component/instrumentation.rb +21 -14
  30. data/lib/view_component/preview.rb +7 -12
  31. data/lib/view_component/request_details.rb +30 -0
  32. data/lib/view_component/slot.rb +7 -15
  33. data/lib/view_component/slotable.rb +94 -80
  34. data/lib/view_component/system_spec_helpers.rb +11 -0
  35. data/lib/view_component/system_test_helpers.rb +2 -2
  36. data/lib/view_component/template.rb +159 -83
  37. data/lib/view_component/test_helpers.rb +67 -50
  38. data/lib/view_component/translatable.rb +33 -32
  39. data/lib/view_component/version.rb +3 -3
  40. data/lib/view_component.rb +9 -6
  41. metadata +36 -556
  42. data/app/assets/vendor/prism.css +0 -4
  43. data/app/assets/vendor/prism.min.js +0 -12
  44. data/app/helpers/preview_helper.rb +0 -85
  45. data/app/views/view_components/_preview_source.html.erb +0 -17
  46. data/lib/rails/generators/tailwindcss/component_generator.rb +0 -11
  47. data/lib/view_component/capture_compatibility.rb +0 -44
  48. data/lib/view_component/component_error.rb +0 -6
  49. data/lib/view_component/rails/tasks/view_component.rake +0 -20
  50. data/lib/view_component/render_component_helper.rb +0 -10
  51. data/lib/view_component/render_component_to_string_helper.rb +0 -9
  52. data/lib/view_component/render_monkey_patch.rb +0 -13
  53. data/lib/view_component/render_to_string_monkey_patch.rb +0 -13
  54. data/lib/view_component/rendering_component_helper.rb +0 -9
  55. data/lib/view_component/rendering_monkey_patch.rb +0 -13
  56. data/lib/view_component/slotable_default.rb +0 -20
  57. data/lib/view_component/use_helpers.rb +0 -42
  58. /data/lib/{rails/generators → generators/view_component}/erb/templates/component.html.erb.tt +0 -0
  59. /data/lib/{rails/generators → generators/view_component}/haml/templates/component.html.haml.tt +0 -0
  60. /data/lib/{rails/generators → generators/view_component}/preview/templates/component_preview.rb.tt +0 -0
  61. /data/lib/{rails/generators → generators/view_component}/rspec/templates/component_spec.rb.tt +0 -0
  62. /data/lib/{rails/generators → generators/view_component}/slim/templates/component.html.slim.tt +0 -0
  63. /data/lib/{rails/generators → generators/view_component}/stimulus/templates/component_controller.js.tt +0 -0
  64. /data/lib/{rails/generators → generators/view_component}/stimulus/templates/component_controller.ts.tt +0 -0
  65. /data/lib/{rails/generators → generators/view_component}/tailwindcss/templates/component.html.erb.tt +0 -0
  66. /data/lib/{rails/generators → generators/view_component}/test_unit/templates/component_test.rb.tt +0 -0
@@ -18,18 +18,7 @@ module ViewComponent
18
18
  def assert_component_rendered
19
19
  assert_selector("body")
20
20
  end
21
- rescue LoadError
22
- # We don't have a test case for running an application without capybara installed.
23
- # It's probably fine to leave this without coverage.
24
- # :nocov:
25
- if ENV["DEBUG"]
26
- warn(
27
- "WARNING in `ViewComponent::TestHelpers`: Add `capybara` " \
28
- "to Gemfile to use Capybara assertions."
29
- )
30
- end
31
-
32
- # :nocov:
21
+ rescue LoadError # We don't have a test case for running an application without capybara installed.
33
22
  end
34
23
 
35
24
  # Returns the result of a render_inline call.
@@ -46,21 +35,23 @@ module ViewComponent
46
35
  # ```
47
36
  #
48
37
  # @param component [ViewComponent::Base, ViewComponent::Collection] The instance of the component to be rendered.
49
- # @return [Nokogiri::HTML]
38
+ #
39
+ # @return [Nokogiri::HTML5]
50
40
  def render_inline(component, **args, &block)
51
41
  @page = nil
52
- @rendered_content =
53
- if Rails.version.to_f >= 6.1
54
- vc_test_controller.view_context.render(component, args, &block)
55
-
56
- # :nocov:
57
- else
58
- vc_test_controller.view_context.render_component(component, &block)
59
- end
42
+ @rendered_content = vc_test_view_context.render(component, args, &block)
60
43
 
61
- # :nocov:
44
+ fragment = Nokogiri::HTML5.fragment(@rendered_content, context: "template")
45
+ @vc_test_view_context = nil
46
+ fragment
47
+ end
62
48
 
63
- Nokogiri::HTML.fragment(@rendered_content)
49
+ # Returns the view context used to render components in tests. Note that the view context
50
+ # is reset after each call to `render_inline`.
51
+ #
52
+ # @return [ActionView::Base]
53
+ def vc_test_view_context
54
+ @vc_test_view_context ||= vc_test_controller.view_context
64
55
  end
65
56
 
66
57
  # `JSON.parse`-d component output.
@@ -81,19 +72,20 @@ module ViewComponent
81
72
  # assert_text("Hello, World!")
82
73
  # ```
83
74
  #
84
- # Note: `#rendered_preview` expects a preview to be defined with the same class
85
- # name as the calling test, but with `Test` replaced with `Preview`:
86
- #
87
- # MyComponentTest -> MyComponentPreview etc.
88
- #
89
- # In RSpec, `Preview` is appended to `described_class`.
90
- #
91
75
  # @param name [String] The name of the preview to be rendered.
92
76
  # @param from [ViewComponent::Preview] The class of the preview to be rendered.
93
77
  # @param params [Hash] Parameters to be passed to the preview.
94
- # @return [Nokogiri::HTML]
78
+ #
79
+ # @return [Nokogiri::HTML5]
80
+ #
81
+ # @note `#rendered_preview` expects a preview to be defined with the same class
82
+ # name as the calling test, but with `Test` replaced with `Preview`:
83
+ #
84
+ # MyComponentTest -> MyComponentPreview etc.
85
+ #
86
+ # In RSpec, `Preview` is appended to `described_class`.
95
87
  def render_preview(name, from: __vc_test_helpers_preview_class, params: {})
96
- previews_controller = __vc_test_helpers_build_controller(Rails.application.config.view_component.preview_controller.constantize)
88
+ previews_controller = __vc_test_helpers_build_controller(Rails.application.config.view_component.previews.controller.constantize)
97
89
 
98
90
  # From what I can tell, it's not possible to overwrite all request parameters
99
91
  # at once, so we set them individually here.
@@ -107,7 +99,7 @@ module ViewComponent
107
99
 
108
100
  @rendered_content = result
109
101
 
110
- Nokogiri::HTML.fragment(@rendered_content)
102
+ Nokogiri::HTML5.fragment(@rendered_content)
111
103
  end
112
104
 
113
105
  # Execute the given block in the view context (using `instance_exec`).
@@ -121,12 +113,11 @@ module ViewComponent
121
113
  #
122
114
  # assert_text("Hello, World!")
123
115
  # ```
124
- def render_in_view_context(*args, &block)
116
+ def render_in_view_context(...)
125
117
  @page = nil
126
- @rendered_content = vc_test_controller.view_context.instance_exec(*args, &block)
127
- Nokogiri::HTML.fragment(@rendered_content)
118
+ @rendered_content = vc_test_view_context.instance_exec(...)
119
+ Nokogiri::HTML5.fragment(@rendered_content)
128
120
  end
129
- ruby2_keywords(:render_in_view_context) if respond_to?(:ruby2_keywords, true)
130
121
 
131
122
  # Set the Action Pack request variant for the given block:
132
123
  #
@@ -136,11 +127,11 @@ module ViewComponent
136
127
  # end
137
128
  # ```
138
129
  #
139
- # @param variant [Symbol] The variant to be set for the provided block.
140
- def with_variant(variant)
130
+ # @param variants [Array<Symbol>] The variants to be set for the provided block.
131
+ def with_variant(*variants)
141
132
  old_variants = vc_test_controller.view_context.lookup_context.variants
142
133
 
143
- vc_test_controller.view_context.lookup_context.variants = variant
134
+ vc_test_controller.view_context.lookup_context.variants += variants
144
135
  yield
145
136
  ensure
146
137
  vc_test_controller.view_context.lookup_context.variants = old_variants
@@ -173,9 +164,14 @@ module ViewComponent
173
164
  # end
174
165
  # ```
175
166
  #
176
- # @param format [Symbol] The format to be set for the provided block.
177
- def with_format(format)
178
- with_request_url("/", format: format) { yield }
167
+ # @param formats [Array<Symbol>] The format(s) to be set for the provided block.
168
+ def with_format(*formats)
169
+ old_formats = vc_test_controller.view_context.lookup_context.formats
170
+
171
+ vc_test_controller.view_context.lookup_context.formats = formats
172
+ yield
173
+ ensure
174
+ vc_test_controller.view_context.lookup_context.formats = old_formats
179
175
  end
180
176
 
181
177
  # Set the URL of the current request (such as when using request-dependent path helpers):
@@ -202,10 +198,19 @@ module ViewComponent
202
198
  # end
203
199
  # ```
204
200
  #
201
+ # To specify a protocol, pass the protocol param:
202
+ #
203
+ # ```ruby
204
+ # with_request_url("/users/42", protocol: :https) do
205
+ # render_inline(MyComponent.new)
206
+ # end
207
+ # ```
208
+ #
205
209
  # @param full_path [String] The path to set for the current request.
206
210
  # @param host [String] The host to set for the current request.
207
211
  # @param method [String] The request method to set for the current request.
208
- def with_request_url(full_path, host: nil, method: nil, format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT)
212
+ # @param protocol [Symbol] The protocol to set for the current request (e.g., `:http` or `:https`).
213
+ def with_request_url(full_path, host: nil, method: nil, protocol: nil)
209
214
  old_request_host = vc_test_request.host
210
215
  old_request_method = vc_test_request.request_method
211
216
  old_request_path_info = vc_test_request.path_info
@@ -213,6 +218,7 @@ module ViewComponent
213
218
  old_request_query_parameters = vc_test_request.query_parameters
214
219
  old_request_query_string = vc_test_request.query_string
215
220
  old_request_format = vc_test_request.format.symbol
221
+ old_request_scheme = vc_test_request.scheme
216
222
  old_controller = defined?(@vc_test_controller) && @vc_test_controller
217
223
 
218
224
  path, query = full_path.split("?", 2)
@@ -220,16 +226,17 @@ module ViewComponent
220
226
  vc_test_request.instance_variable_set(:@original_fullpath, full_path)
221
227
  vc_test_request.host = host if host
222
228
  vc_test_request.request_method = method if method
229
+ vc_test_request.set_header(Rack::RACK_URL_SCHEME, protocol.to_s) if protocol
223
230
  vc_test_request.path_info = path
224
231
  vc_test_request.path_parameters = Rails.application.routes.recognize_path_with_request(vc_test_request, path, {})
225
232
  vc_test_request.set_header("action_dispatch.request.query_parameters",
226
233
  Rack::Utils.parse_nested_query(query).with_indifferent_access)
227
234
  vc_test_request.set_header(Rack::QUERY_STRING, query)
228
- vc_test_request.format = format
229
235
  yield
230
236
  ensure
231
237
  vc_test_request.host = old_request_host
232
238
  vc_test_request.request_method = old_request_method
239
+ vc_test_request.set_header(Rack::RACK_URL_SCHEME, old_request_scheme)
233
240
  vc_test_request.path_info = old_request_path_info
234
241
  vc_test_request.path_parameters = old_request_path_parameters
235
242
  vc_test_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
@@ -250,7 +257,20 @@ module ViewComponent
250
257
  #
251
258
  # @return [ActionController::Base]
252
259
  def vc_test_controller
253
- @vc_test_controller ||= __vc_test_helpers_build_controller(Base.test_controller.constantize)
260
+ @vc_test_controller ||= __vc_test_helpers_build_controller(vc_test_controller_class)
261
+ end
262
+
263
+ # Set the controller used by `render_inline`:
264
+ #
265
+ # ```ruby
266
+ # def vc_test_controller_class
267
+ # MyTestController
268
+ # end
269
+ # ```
270
+ def vc_test_controller_class
271
+ return @__vc_test_controller_class if defined?(@__vc_test_controller_class)
272
+
273
+ defined?(ApplicationController) ? ApplicationController : ActionController::Base
254
274
  end
255
275
 
256
276
  # Access the request used by `render_inline`:
@@ -284,11 +304,9 @@ module ViewComponent
284
304
 
285
305
  def __vc_test_helpers_preview_class
286
306
  result = if respond_to?(:described_class)
287
- # :nocov:
288
- raise "`render_preview` expected a described_class, but it is nil." if described_class.nil?
307
+ raise ArgumentError.new("`render_preview` expected a described_class, but it is nil.") if described_class.nil?
289
308
 
290
309
  "#{described_class}Preview"
291
- # :nocov:
292
310
  else
293
311
  self.class.name.gsub("Test", "Preview")
294
312
  end
@@ -296,6 +314,5 @@ module ViewComponent
296
314
  rescue NameError
297
315
  raise NameError, "`render_preview` expected to find #{result}, but it does not exist."
298
316
  end
299
- # :nocov:
300
317
  end
301
318
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
- require "set"
5
4
  require "i18n"
6
5
  require "active_support/concern"
7
6
 
@@ -10,19 +9,22 @@ module ViewComponent
10
9
  extend ActiveSupport::Concern
11
10
 
12
11
  HTML_SAFE_TRANSLATION_KEY = /(?:_|\b)html\z/
12
+ private_constant :HTML_SAFE_TRANSLATION_KEY
13
+
13
14
  TRANSLATION_EXTENSIONS = %w[yml yaml].freeze
15
+ private_constant :TRANSLATION_EXTENSIONS
14
16
 
15
17
  included do
16
- class_attribute :i18n_backend, instance_writer: false, instance_predicate: false
18
+ class_attribute :__vc_i18n_backend, instance_writer: false, instance_predicate: false
17
19
  end
18
20
 
19
21
  class_methods do
20
- def i18n_scope
21
- @i18n_scope ||= virtual_path.sub(%r{^/}, "").gsub(%r{/_?}, ".")
22
+ def __vc_i18n_scope
23
+ @__vc_i18n_scope ||= virtual_path.sub(%r{^/}, "").gsub(%r{/_?}, ".")
22
24
  end
23
25
 
24
- def build_i18n_backend
25
- return if compiled?
26
+ def __vc_build_i18n_backend
27
+ return if __vc_compiled?
26
28
 
27
29
  # We need to load the translations files from the ancestors so a component
28
30
  # can inherit translations from its parent and is able to overwrite them.
@@ -33,31 +35,31 @@ module ViewComponent
33
35
  end
34
36
 
35
37
  # In development it will become nil if the translations file is removed
36
- self.i18n_backend = if translation_files.any?
38
+ self.__vc_i18n_backend = if translation_files.any?
37
39
  I18nBackend.new(
38
- i18n_scope: i18n_scope,
40
+ scope: __vc_i18n_scope,
39
41
  load_paths: translation_files
40
42
  )
41
43
  end
42
44
  end
43
45
 
44
- def i18n_key(key, scope = nil)
46
+ def __vc_i18n_key(key, scope = nil)
45
47
  scope = scope.join(".") if scope.is_a? Array
46
48
  key = key&.to_s unless key.is_a?(String)
47
49
  key = "#{scope}.#{key}" if scope
48
- key = "#{i18n_scope}#{key}" if key.start_with?(".")
50
+ key = "#{__vc_i18n_scope}#{key}" if key.start_with?(".")
49
51
  key
50
52
  end
51
53
 
52
54
  def translate(key = nil, **options)
53
55
  return key.map { |k| translate(k, **options) } if key.is_a?(Array)
54
56
 
55
- ensure_compiled
57
+ __vc_ensure_compiled
56
58
 
57
59
  locale = options.delete(:locale) || ::I18n.locale
58
- key = i18n_key(key, options.delete(:scope))
60
+ key = __vc_i18n_key(key, options.delete(:scope))
59
61
 
60
- i18n_backend.translate(locale, key, options)
62
+ __vc_i18n_backend.translate(locale, key, options)
61
63
  end
62
64
 
63
65
  alias_method :t, :translate
@@ -66,18 +68,18 @@ module ViewComponent
66
68
  class I18nBackend < ::I18n::Backend::Simple
67
69
  EMPTY_HASH = {}.freeze
68
70
 
69
- def initialize(i18n_scope:, load_paths:)
70
- @i18n_scope = i18n_scope.split(".").map(&:to_sym)
71
- @load_paths = load_paths
71
+ def initialize(scope:, load_paths:)
72
+ @__vc_i18n_scope = scope.split(".").map(&:to_sym)
73
+ @__vc_load_paths = load_paths
72
74
  end
73
75
 
74
76
  # Ensure the Simple backend won't load paths from ::I18n.load_path
75
77
  def load_translations
76
- super(@load_paths)
78
+ super(@__vc_load_paths)
77
79
  end
78
80
 
79
81
  def scope_data(data)
80
- @i18n_scope.reverse_each do |part|
82
+ @__vc_i18n_scope.reverse_each do |part|
81
83
  data = {part => data}
82
84
  end
83
85
  data
@@ -91,44 +93,43 @@ module ViewComponent
91
93
  def translate(key = nil, **options)
92
94
  raise ViewComponent::TranslateCalledBeforeRenderError if view_context.nil?
93
95
 
94
- return super unless i18n_backend
96
+ return @view_context.translate(key, **options) unless __vc_i18n_backend
95
97
  return key.map { |k| translate(k, **options) } if key.is_a?(Array)
96
98
 
97
99
  locale = options.delete(:locale) || ::I18n.locale
98
- key = self.class.i18n_key(key, options.delete(:scope))
100
+ key = self.class.__vc_i18n_key(key, options.delete(:scope))
99
101
  as_html = HTML_SAFE_TRANSLATION_KEY.match?(key)
100
102
 
101
- html_escape_translation_options!(options) if as_html
103
+ __vc_html_escape_translation_options!(options) if as_html
102
104
 
103
- if key.start_with?(i18n_scope + ".")
105
+ if key.start_with?(__vc_i18n_scope + ".")
104
106
  translated =
105
107
  catch(:exception) do
106
- i18n_backend.translate(locale, key, options)
108
+ __vc_i18n_backend.translate(locale, key, options)
107
109
  end
108
110
 
109
111
  # Fallback to the global translations
110
112
  if translated.is_a? ::I18n::MissingTranslation
111
- return super(key, locale: locale, **options)
113
+ return @view_context.translate(key, locale: locale, **options)
112
114
  end
113
115
 
114
- translated = html_safe_translation(translated) if as_html
116
+ translated = __vc_html_safe_translation(translated) if as_html
115
117
  translated
116
118
  else
117
- super(key, locale: locale, **options)
119
+ @view_context.translate(key, locale: locale, **options)
118
120
  end
119
121
  end
120
122
  alias_method :t, :translate
121
123
 
122
- # Exposes .i18n_scope as an instance method
123
- def i18n_scope
124
- self.class.i18n_scope
124
+ def __vc_i18n_scope
125
+ self.class.__vc_i18n_scope
125
126
  end
126
127
 
127
128
  private
128
129
 
129
- def html_safe_translation(translation)
130
+ def __vc_html_safe_translation(translation)
130
131
  if translation.respond_to?(:map)
131
- translation.map { |element| html_safe_translation(element) }
132
+ translation.map { |element| __vc_html_safe_translation(element) }
132
133
  else
133
134
  # It's assumed here that objects loaded by the i18n backend will respond to `#html_safe?`.
134
135
  # It's reasonable that if we're in Rails, `active_support/core_ext/string/output_safety.rb`
@@ -137,7 +138,7 @@ module ViewComponent
137
138
  end
138
139
  end
139
140
 
140
- def html_escape_translation_options!(options)
141
+ def __vc_html_escape_translation_options!(options)
141
142
  options.except(*::I18n::RESERVED_KEYS).each do |name, value|
142
143
  next if name == :count && value.is_a?(Numeric)
143
144
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  module ViewComponent
4
4
  module VERSION
5
- MAJOR = 3
6
- MINOR = 23
7
- PATCH = 2
5
+ MAJOR = 4
6
+ MINOR = 12
7
+ PATCH = 0
8
8
  PRE = nil
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
@@ -2,25 +2,28 @@
2
2
 
3
3
  require "action_view"
4
4
  require "active_support/dependencies/autoload"
5
+ require "view_component/version"
5
6
 
6
7
  module ViewComponent
7
8
  extend ActiveSupport::Autoload
8
9
 
9
10
  autoload :Base
10
- autoload :CaptureCompatibility
11
11
  autoload :Compiler
12
12
  autoload :CompileCache
13
- autoload :ComponentError
14
13
  autoload :Config
15
14
  autoload :Deprecation
16
15
  autoload :InlineTemplate
17
16
  autoload :Instrumentation
18
17
  autoload :Preview
19
- autoload :TestHelpers
20
- autoload :SystemTestHelpers
21
- autoload :TestCase
22
- autoload :SystemTestCase
23
18
  autoload :Translatable
19
+
20
+ if defined?(Rails.env) && Rails.env.test?
21
+ autoload :TestHelpers
22
+ autoload :SystemSpecHelpers
23
+ autoload :SystemTestHelpers
24
+ autoload :TestCase
25
+ autoload :SystemTestCase
26
+ end
24
27
  end
25
28
 
26
29
  require "view_component/engine" if defined?(Rails::Engine)