view_component 2.62.0 → 2.82.0

Sign up to get free protection for your applications and to get access to all the features.

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"