view_component 2.62.0 → 2.82.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.

Potentially problematic release.


This version of view_component might be problematic. Click here for more details.

Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/app/assets/vendor/prism.css +3 -195
  4. data/app/assets/vendor/prism.min.js +11 -11
  5. data/app/controllers/concerns/view_component/preview_actions.rb +6 -10
  6. data/app/controllers/view_components_system_test_controller.rb +7 -0
  7. data/app/helpers/preview_helper.rb +2 -2
  8. data/app/views/view_components/preview.html.erb +2 -2
  9. data/docs/CHANGELOG.md +383 -21
  10. data/lib/rails/generators/abstract_generator.rb +3 -5
  11. data/lib/rails/generators/component/component_generator.rb +5 -4
  12. data/lib/rails/generators/locale/component_generator.rb +1 -1
  13. data/lib/rails/generators/preview/component_generator.rb +10 -3
  14. data/lib/view_component/base.rb +41 -47
  15. data/lib/view_component/compiler.rb +57 -68
  16. data/lib/view_component/config.rb +176 -0
  17. data/lib/view_component/content_areas.rb +2 -3
  18. data/lib/view_component/deprecation.rb +2 -2
  19. data/lib/view_component/docs_builder_component.html.erb +1 -1
  20. data/lib/view_component/docs_builder_component.rb +1 -1
  21. data/lib/view_component/engine.rb +23 -29
  22. data/lib/view_component/polymorphic_slots.rb +11 -5
  23. data/lib/view_component/render_component_helper.rb +1 -0
  24. data/lib/view_component/slotable.rb +2 -3
  25. data/lib/view_component/slotable_v2.rb +48 -8
  26. data/lib/view_component/system_test_case.rb +13 -0
  27. data/lib/view_component/system_test_helpers.rb +27 -0
  28. data/lib/view_component/test_helpers.rb +65 -15
  29. data/lib/view_component/translatable.rb +1 -1
  30. data/lib/view_component/version.rb +1 -1
  31. data/lib/view_component.rb +6 -3
  32. metadata +50 -18
  33. data/lib/view_component/previewable.rb +0 -62
  34. data/lib/view_component/render_preview_helper.rb +0 -50
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails"
4
+ require "view_component/base"
4
5
 
5
6
  module ViewComponent
6
7
  class Engine < Rails::Engine # :nodoc:
7
- config.view_component = ActiveSupport::OrderedOptions.new
8
- config.view_component.preview_paths ||= []
8
+ config.view_component = ViewComponent::Base.config
9
9
 
10
10
  rake_tasks do
11
11
  load "view_component/rails/tasks/view_component.rake"
@@ -14,25 +14,19 @@ module ViewComponent
14
14
  initializer "view_component.set_configs" do |app|
15
15
  options = app.config.view_component
16
16
 
17
- options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
18
- options.show_previews = Rails.env.development? || Rails.env.test? if options.show_previews.nil?
19
- options.show_previews_source ||= ViewComponent::Base.show_previews_source
17
+ %i[generate preview_controller preview_route show_previews_source].each do |config_option|
18
+ options[config_option] ||= ViewComponent::Base.public_send(config_option)
19
+ end
20
20
  options.instrumentation_enabled = false if options.instrumentation_enabled.nil?
21
- options.preview_route ||= ViewComponent::Base.preview_route
22
- options.preview_controller ||= ViewComponent::Base.preview_controller
21
+ options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
22
+ options.show_previews = (Rails.env.development? || Rails.env.test?) if options.show_previews.nil?
23
23
 
24
24
  if options.show_previews
25
+ # This is still necessary because when `config.view_component` is declared, `Rails.root` is unspecified.
25
26
  options.preview_paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root) && Dir.exist?(
26
27
  "#{Rails.root}/test/components/previews"
27
28
  )
28
29
 
29
- if options.preview_path.present?
30
- ViewComponent::Deprecation.warn(
31
- "`preview_path` will be removed in v3.0.0. Use `preview_paths` instead."
32
- )
33
- options.preview_paths << options.preview_path
34
- end
35
-
36
30
  if options.show_previews_source
37
31
  require "method_source"
38
32
 
@@ -41,10 +35,6 @@ module ViewComponent
41
35
  end
42
36
  end
43
37
  end
44
-
45
- ActiveSupport.on_load(:view_component) do
46
- options.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
47
- end
48
38
  end
49
39
 
50
40
  initializer "view_component.enable_instrumentation" do |app|
@@ -72,12 +62,6 @@ module ViewComponent
72
62
  end
73
63
  end
74
64
 
75
- initializer "view_component.compile_config_methods" do
76
- ActiveSupport.on_load(:view_component) do
77
- config.compile_methods! if config.respond_to?(:compile_methods!)
78
- end
79
- end
80
-
81
65
  initializer "view_component.monkey_patch_render" do |app|
82
66
  next if Rails.version.to_f >= 6.1 || !app.config.view_component.render_monkey_patch_enabled
83
67
 
@@ -94,7 +78,7 @@ module ViewComponent
94
78
  end
95
79
  end
96
80
 
97
- initializer "view_component.include_render_component" do |app|
81
+ initializer "view_component.include_render_component" do |_app|
98
82
  next if Rails.version.to_f >= 6.1
99
83
 
100
84
  ActiveSupport.on_load(:action_view) do
@@ -116,7 +100,7 @@ module ViewComponent
116
100
  end
117
101
  end
118
102
 
119
- initializer "compiler mode" do |app|
103
+ initializer "compiler mode" do |_app|
120
104
  ViewComponent::Compiler.mode = if Rails.env.development? || Rails.env.test?
121
105
  ViewComponent::Compiler::DEVELOPMENT_MODE
122
106
  else
@@ -147,6 +131,12 @@ module ViewComponent
147
131
  end
148
132
  end
149
133
 
134
+ if Rails.env.test?
135
+ app.routes.prepend do
136
+ get("_system_test_entrypoint", to: "view_components_system_test#system_test_entrypoint")
137
+ end
138
+ end
139
+
150
140
  app.executor.to_run :before do
151
141
  CompileCache.invalidate! unless ActionView::Base.cache_template_loading
152
142
  end
@@ -154,13 +144,17 @@ module ViewComponent
154
144
  end
155
145
  end
156
146
 
147
+ if RUBY_VERSION < "2.7.0"
148
+ ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 2.7.0")
149
+ end
150
+
157
151
  # :nocov:
158
152
  unless defined?(ViewComponent::Base)
159
153
  require "view_component/deprecation"
160
154
 
161
- ViewComponent::Deprecation.warn(
162
- "This manually engine loading is deprecated and will be removed in v3.0.0. " \
163
- "Remove `require \"view_component/engine\"`."
155
+ ViewComponent::Deprecation.deprecation_warning(
156
+ "Manually loading the engine",
157
+ "remove `require \"view_component/engine\"`"
164
158
  )
165
159
 
166
160
  require "view_component"
@@ -42,6 +42,10 @@ module ViewComponent
42
42
  define_method(getter_name) do
43
43
  get_slot(slot_name)
44
44
  end
45
+
46
+ define_method("#{getter_name}?") do
47
+ get_slot(slot_name).present?
48
+ end
45
49
  end
46
50
 
47
51
  renderable_hash = types.each_with_object({}) do |(poly_type, poly_callable), memo|
@@ -57,10 +61,12 @@ module ViewComponent
57
61
  end
58
62
 
59
63
  define_method(setter_name) do |*args, &block|
60
- ViewComponent::Deprecation.warn(
61
- "polymorphic slot setters like `#{setter_name}` are deprecated and will be removed in " \
62
- "ViewComponent v3.0.0.\n\nUse `with_#{setter_name}` instead."
63
- )
64
+ if _warn_on_deprecated_slot_setter
65
+ ViewComponent::Deprecation.deprecation_warning(
66
+ "Using polymorphic slot setters like `#{setter_name}`",
67
+ :"`with_#{setter_name}`"
68
+ )
69
+ end
64
70
 
65
71
  set_polymorphic_slot(slot_name, poly_type, *args, &block)
66
72
  end
@@ -83,7 +89,7 @@ module ViewComponent
83
89
  def set_polymorphic_slot(slot_name, poly_type = nil, *args, &block)
84
90
  slot_definition = self.class.registered_slots[slot_name]
85
91
 
86
- if !slot_definition[:collection] && get_slot(slot_name)
92
+ if !slot_definition[:collection] && (defined?(@__vc_set_slots) && @__vc_set_slots[slot_name])
87
93
  raise ArgumentError, "content for slot '#{slot_name}' has already been provided"
88
94
  end
89
95
 
@@ -3,6 +3,7 @@
3
3
  module ViewComponent
4
4
  module RenderComponentHelper # :nodoc:
5
5
  def render_component(component, &block)
6
+ component.set_original_view_context(__vc_original_view_context) if is_a?(ViewComponent::Base)
6
7
  component.render_in(self, &block)
7
8
  end
8
9
  end
@@ -23,9 +23,8 @@ module ViewComponent
23
23
  # class_name: "Header" # class name string, used to instantiate Slot
24
24
  # )
25
25
  def with_slot(*slot_names, collection: false, class_name: nil)
26
- ViewComponent::Deprecation.warn(
27
- "`with_slot` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
28
- "Use the new slots API (https://viewcomponent.org/guide/slots.html) instead."
26
+ ViewComponent::Deprecation.deprecation_warning(
27
+ "`with_slot`", "use the new slots API (https://viewcomponent.org/guide/slots.html) instead"
29
28
  )
30
29
 
31
30
  slot_names.each do |slot_name|
@@ -17,9 +17,19 @@ module ViewComponent
17
17
  # Hash of registered Slots
18
18
  class_attribute :registered_slots
19
19
  self.registered_slots = {}
20
+
21
+ class_attribute :_warn_on_deprecated_slot_setter
22
+ self._warn_on_deprecated_slot_setter = false
20
23
  end
21
24
 
22
25
  class_methods do
26
+ ##
27
+ # Enables deprecations coming to the Slots API in ViewComponent v3
28
+ #
29
+ def warn_on_deprecated_slot_setter
30
+ self._warn_on_deprecated_slot_setter = true
31
+ end
32
+
23
33
  ##
24
34
  # Registers a sub-component
25
35
  #
@@ -70,6 +80,7 @@ module ViewComponent
70
80
  # <% end %>
71
81
  def renders_one(slot_name, callable = nil)
72
82
  validate_singular_slot_name(slot_name)
83
+ validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
73
84
 
74
85
  define_method :"with_#{slot_name}" do |*args, &block|
75
86
  set_slot(slot_name, nil, *args, &block)
@@ -80,7 +91,16 @@ module ViewComponent
80
91
  if args.empty? && block.nil?
81
92
  get_slot(slot_name)
82
93
  else
83
- # Deprecated: Will remove in 3.0
94
+ if _warn_on_deprecated_slot_setter
95
+ stack = caller_locations(3)
96
+
97
+ ViewComponent::Deprecation.deprecation_warning(
98
+ "Setting a slot with `##{slot_name}`",
99
+ "use `#with_#{slot_name}` to set the slot instead",
100
+ stack
101
+ )
102
+ end
103
+
84
104
  set_slot(slot_name, nil, *args, &block)
85
105
  end
86
106
  end
@@ -98,11 +118,11 @@ module ViewComponent
98
118
  #
99
119
  # = Example
100
120
  #
101
- # render_many :items, -> (name:) { ItemComponent.new(name: name }
121
+ # renders_many :items, -> (name:) { ItemComponent.new(name: name }
102
122
  #
103
123
  # # OR
104
124
  #
105
- # render_many :items, ItemComponent
125
+ # renders_many :items, ItemComponent
106
126
  #
107
127
  # = Rendering sub-components
108
128
  #
@@ -131,16 +151,22 @@ module ViewComponent
131
151
  # <% end %>
132
152
  # <% end %>
133
153
  def renders_many(slot_name, callable = nil)
134
- validate_plural_slot_name(slot_name)
135
-
136
154
  singular_name = ActiveSupport::Inflector.singularize(slot_name)
155
+ validate_plural_slot_name(slot_name)
156
+ validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
137
157
 
138
158
  # Define setter for singular names
139
159
  # for example `renders_many :items` allows fetching all tabs with
140
160
  # `component.tabs` and setting a tab with `component.tab`
141
- #
142
- # Deprecated: Will remove in 3.0
161
+
143
162
  define_method singular_name do |*args, &block|
163
+ if _warn_on_deprecated_slot_setter
164
+ ViewComponent::Deprecation.deprecation_warning(
165
+ "Setting a slot with `##{singular_name}`",
166
+ "use `#with_#{singular_name}` to set the slot instead"
167
+ )
168
+ end
169
+
144
170
  set_slot(slot_name, nil, *args, &block)
145
171
  end
146
172
  ruby2_keywords(singular_name.to_sym) if respond_to?(:ruby2_keywords, true)
@@ -162,7 +188,13 @@ module ViewComponent
162
188
  if collection_args.nil? && block.nil?
163
189
  get_slot(slot_name)
164
190
  else
165
- # Deprecated: Will remove in 3.0
191
+ if _warn_on_deprecated_slot_setter
192
+ ViewComponent::Deprecation.deprecation_warning(
193
+ "Setting a slot with `##{slot_name}`",
194
+ "use `#with_#{slot_name}` to set the slot instead"
195
+ )
196
+ end
197
+
166
198
  collection_args.map do |args|
167
199
  set_slot(slot_name, nil, **args, &block)
168
200
  end
@@ -242,6 +274,14 @@ module ViewComponent
242
274
  end
243
275
 
244
276
  def validate_singular_slot_name(slot_name)
277
+ if slot_name.to_sym == :content
278
+ raise ArgumentError.new(
279
+ "#{self} declares a slot named content, which is a reserved word in ViewComponent.\n\n" \
280
+ "Content passed to a ViewComponent as a block is captured and assigned to the `content` accessor without having to create an explicit slot.\n\n" \
281
+ "To fix this issue, either use the `content` accessor directly or choose a different slot name."
282
+ )
283
+ end
284
+
245
285
  if RESERVED_NAMES[:singular].include?(slot_name.to_sym)
246
286
  raise ArgumentError.new(
247
287
  "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/test_case"
4
+
5
+ module ViewComponent
6
+ class SystemTestCase < ActionDispatch::SystemTestCase
7
+ include ViewComponent::SystemTestHelpers
8
+
9
+ def page
10
+ Capybara.current_session
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module SystemTestHelpers
5
+ include TestHelpers
6
+
7
+ #
8
+ # Returns a block that can be used to visit the path of the inline rendered component.
9
+ # @param fragment [Nokogiri::Fragment] The fragment returned from `render_inline`.
10
+ # @param layout [String] The (optional) layout to use.
11
+ # @return [Proc] A block that can be used to visit the path of the inline rendered component.
12
+ def with_rendered_component_path(fragment, layout: false, &block)
13
+ # Add './tmp/view_components/' directory if it doesn't exist to store the rendered component HTML
14
+ FileUtils.mkdir_p("./tmp/view_components/") unless Dir.exist?("./tmp/view_components/")
15
+
16
+ file = Tempfile.new(["rendered_#{fragment.class.name}", ".html"], "tmp/view_components/")
17
+ begin
18
+ file.write(controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
19
+ file.rewind
20
+
21
+ block.call("/_system_test_entrypoint?file=#{file.path.split("/").last}")
22
+ ensure
23
+ file.unlink
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "view_component/render_preview_helper"
4
-
5
3
  module ViewComponent
6
4
  module TestHelpers
7
5
  begin
@@ -37,10 +35,7 @@ module ViewComponent
37
35
  #
38
36
  # @return [String]
39
37
  def rendered_component
40
- ViewComponent::Deprecation.warn(
41
- "`rendered_component` is deprecated and will be removed in v3.0.0. " \
42
- "Use `page` instead."
43
- )
38
+ ViewComponent::Deprecation.deprecation_warning("`rendered_component`", :"`page`")
44
39
 
45
40
  rendered_content
46
41
  end
@@ -67,21 +62,60 @@ module ViewComponent
67
62
  Nokogiri::HTML.fragment(@rendered_content)
68
63
  end
69
64
 
70
- # Execute the given block in the view context. Internally sets `page` to be a
71
- # `Capybara::Node::Simple`, allowing for Capybara assertions to be used:
65
+ # Render a preview inline. Internally sets `page` to be a `Capybara::Node::Simple`,
66
+ # allowing for Capybara assertions to be used:
67
+ #
68
+ # ```ruby
69
+ # render_preview(:default)
70
+ # assert_text("Hello, World!")
71
+ # ```
72
+ #
73
+ # Note: `#rendered_preview` expects a preview to be defined with the same class
74
+ # name as the calling test, but with `Test` replaced with `Preview`:
75
+ #
76
+ # MyComponentTest -> MyComponentPreview etc.
77
+ #
78
+ # In RSpec, `Preview` is appended to `described_class`.
79
+ #
80
+ # @param name [String] The name of the preview to be rendered.
81
+ # @param from [ViewComponent::Preview] The class of the preview to be rendered.
82
+ # @param params [Hash] Parameters to be passed to the preview.
83
+ # @return [Nokogiri::HTML]
84
+ def render_preview(name, from: preview_class, params: {})
85
+ previews_controller = build_controller(Rails.application.config.view_component.preview_controller.constantize)
86
+
87
+ # From what I can tell, it's not possible to overwrite all request parameters
88
+ # at once, so we set them individually here.
89
+ params.each do |k, v|
90
+ previews_controller.request.params[k] = v
91
+ end
92
+
93
+ previews_controller.request.params[:path] = "#{from.preview_name}/#{name}"
94
+ previews_controller.set_response!(ActionDispatch::Response.new)
95
+ result = previews_controller.previews
96
+
97
+ @rendered_content = result
98
+
99
+ Nokogiri::HTML.fragment(@rendered_content)
100
+ end
101
+
102
+ # Execute the given block in the view context (using `instance_exec`).
103
+ # Internally sets `page` to be a `Capybara::Node::Simple`, allowing for
104
+ # Capybara assertions to be used. All arguments are forwarded to the block.
72
105
  #
73
106
  # ```ruby
74
- # render_in_view_context do
75
- # render(MyComponent.new)
107
+ # render_in_view_context(arg1, arg2:) do |arg1, arg2:|
108
+ # render(MyComponent.new(arg1, arg2))
76
109
  # end
77
110
  #
78
111
  # assert_text("Hello, World!")
79
112
  # ```
80
- def render_in_view_context(&block)
113
+ def render_in_view_context(*args, &block)
81
114
  @page = nil
82
- @rendered_content = controller.view_context.instance_exec(&block)
115
+ @rendered_content = controller.view_context.instance_exec(*args, &block)
83
116
  Nokogiri::HTML.fragment(@rendered_content)
84
117
  end
118
+ ruby2_keywords(:render_in_view_context) if respond_to?(:ruby2_keywords, true)
85
119
 
86
120
  # @private
87
121
  def controller
@@ -151,10 +185,11 @@ module ViewComponent
151
185
  old_request_query_string = request.query_string
152
186
  old_controller = defined?(@controller) && @controller
153
187
 
188
+ path, query = path.split("?", 2)
154
189
  request.path_info = path
155
- request.path_parameters = Rails.application.routes.recognize_path(path)
156
- request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_nested_query(path.split("?")[1]))
157
- request.set_header(Rack::QUERY_STRING, path.split("?")[1])
190
+ request.path_parameters = Rails.application.routes.recognize_path_with_request(request, path, {})
191
+ request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_nested_query(query))
192
+ request.set_header(Rack::QUERY_STRING, query)
158
193
  yield
159
194
  ensure
160
195
  request.path_info = old_request_path_info
@@ -168,5 +203,20 @@ module ViewComponent
168
203
  def build_controller(klass)
169
204
  klass.new.tap { |c| c.request = request }.extend(Rails.application.routes.url_helpers)
170
205
  end
206
+
207
+ private
208
+
209
+ def preview_class
210
+ result = if respond_to?(:described_class)
211
+ raise "`render_preview` expected a described_class, but it is nil." if described_class.nil?
212
+
213
+ "#{described_class}Preview"
214
+ else
215
+ self.class.name.gsub("Test", "Preview")
216
+ end
217
+ result = result.constantize
218
+ rescue NameError
219
+ raise NameError, "`render_preview` expected to find #{result}, but it does not exist."
220
+ end
171
221
  end
172
222
  end
@@ -23,7 +23,7 @@ module ViewComponent
23
23
  def build_i18n_backend
24
24
  return if CompileCache.compiled? self
25
25
 
26
- self.i18n_backend = if (translation_files = _sidecar_files(%w[yml yaml])).any?
26
+ self.i18n_backend = if (translation_files = sidecar_files(%w[yml yaml])).any?
27
27
  # Returning nil cleans up if translations file has been removed since the last compilation
28
28
 
29
29
  I18nBackend.new(
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 62
6
+ MINOR = 82
7
7
  PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
@@ -10,21 +10,24 @@ module ViewComponent
10
10
  autoload :Compiler
11
11
  autoload :CompileCache
12
12
  autoload :ComponentError
13
+ autoload :Config
13
14
  autoload :Deprecation
14
15
  autoload :Instrumentation
15
16
  autoload :Preview
16
17
  autoload :PreviewTemplateError
17
18
  autoload :TestHelpers
19
+ autoload :SystemTestHelpers
18
20
  autoload :TestCase
21
+ autoload :SystemTestCase
19
22
  autoload :TemplateError
20
23
  autoload :Translatable
21
24
  end
22
25
 
23
26
  # :nocov:
24
27
  if defined?(ViewComponent::Engine)
25
- ViewComponent::Deprecation.warn(
26
- "Manually loading the engine is deprecated and will be removed in v3.0.0. " \
27
- "Remove `require \"view_component/engine\"`."
28
+ ViewComponent::Deprecation.deprecation_warning(
29
+ "Manually loading the engine",
30
+ "remove `require \"view_component/engine\"`"
28
31
  )
29
32
  elsif defined?(Rails::Engine)
30
33
  require "view_component/engine"