view_component 2.34.0 → 2.35.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.

@@ -21,12 +21,13 @@ module ViewComponent
21
21
 
22
22
  if template_errors.present?
23
23
  raise ViewComponent::TemplateError.new(template_errors) if raise_errors
24
+
24
25
  return false
25
26
  end
26
27
 
27
28
  if subclass_instance_methods.include?(:before_render_check)
28
29
  ActiveSupport::Deprecation.warn(
29
- "`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
30
+ "`before_render_check` will be removed in v3.0.0. Use `#before_render` instead."
30
31
  )
31
32
  end
32
33
 
@@ -77,72 +78,75 @@ module ViewComponent
77
78
  end
78
79
  end
79
80
  RUBY
80
-
81
81
  end
82
82
 
83
83
  def template_errors
84
- @_template_errors ||= begin
85
- errors = []
84
+ @__vc_template_errors ||=
85
+ begin
86
+ errors = []
86
87
 
87
- if (templates + inline_calls).empty?
88
- errors << "Could not find a template file or inline render method for #{component_class}."
89
- end
88
+ if (templates + inline_calls).empty?
89
+ errors << "Could not find a template file or inline render method for #{component_class}."
90
+ end
90
91
 
91
- if templates.count { |template| template[:variant].nil? } > 1
92
- errors << "More than one template found for #{component_class}. There can only be one default template file per component."
93
- end
92
+ if templates.count { |template| template[:variant].nil? } > 1
93
+ errors << "More than one template found for #{component_class}. There can only be one default template file per component."
94
+ end
94
95
 
95
- invalid_variants = templates
96
- .group_by { |template| template[:variant] }
97
- .map { |variant, grouped| variant if grouped.length > 1 }
98
- .compact
99
- .sort
96
+ invalid_variants =
97
+ templates.
98
+ group_by { |template| template[:variant] }.
99
+ map { |variant, grouped| variant if grouped.length > 1 }.
100
+ compact.
101
+ sort
100
102
 
101
- unless invalid_variants.empty?
102
- errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be one template file per variant."
103
- end
103
+ unless invalid_variants.empty?
104
+ errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be one template file per variant."
105
+ end
104
106
 
105
- if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
106
- errors << "Template file and inline render method found for #{component_class}. There can only be a template file or inline render method per component."
107
- end
107
+ if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
108
+ errors << "Template file and inline render method found for #{component_class}. There can only be a template file or inline render method per component."
109
+ end
108
110
 
109
- duplicate_template_file_and_inline_variant_calls =
110
- templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
111
+ duplicate_template_file_and_inline_variant_calls =
112
+ templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
111
113
 
112
- unless duplicate_template_file_and_inline_variant_calls.empty?
113
- count = duplicate_template_file_and_inline_variant_calls.count
114
+ unless duplicate_template_file_and_inline_variant_calls.empty?
115
+ count = duplicate_template_file_and_inline_variant_calls.count
114
116
 
115
- errors << "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} found for #{'variant'.pluralize(count)} #{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be a template file or inline render method per variant."
116
- end
117
+ errors << "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} found for #{'variant'.pluralize(count)} #{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be a template file or inline render method per variant."
118
+ end
117
119
 
118
- errors
119
- end
120
+ errors
121
+ end
120
122
  end
121
123
 
122
124
  def templates
123
- @templates ||= begin
124
- extensions = ActionView::Template.template_handler_extensions
125
-
126
- component_class._sidecar_files(extensions).each_with_object([]) do |path, memo|
127
- pieces = File.basename(path).split(".")
128
- memo << {
129
- path: path,
130
- variant: pieces.second.split("+").second&.to_sym,
131
- handler: pieces.last
132
- }
125
+ @templates ||=
126
+ begin
127
+ extensions = ActionView::Template.template_handler_extensions
128
+
129
+ component_class._sidecar_files(extensions).each_with_object([]) do |path, memo|
130
+ pieces = File.basename(path).split(".")
131
+ memo << {
132
+ path: path,
133
+ variant: pieces.second.split("+").second&.to_sym,
134
+ handler: pieces.last
135
+ }
136
+ end
133
137
  end
134
- end
135
138
  end
136
139
 
137
140
  def inline_calls
138
- @inline_calls ||= begin
139
- # Fetch only ViewComponent ancestor classes to limit the scope of
140
- # finding inline calls
141
- view_component_ancestors =
142
- component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - component_class.included_modules
143
-
144
- view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
145
- end
141
+ @inline_calls ||=
142
+ begin
143
+ # Fetch only ViewComponent ancestor classes to limit the scope of
144
+ # finding inline calls
145
+ view_component_ancestors =
146
+ component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - component_class.included_modules
147
+
148
+ view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
149
+ end
146
150
  end
147
151
 
148
152
  def inline_calls_defined_on_self
@@ -150,7 +154,7 @@ module ViewComponent
150
154
  end
151
155
 
152
156
  def variants
153
- @_variants = (
157
+ @__vc_variants = (
154
158
  templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
155
159
  ).compact.uniq
156
160
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ require "view_component/slot"
6
+
7
+ # DEPRECATED - ContentAreas is deprecated and will be removed in v3.0.0
8
+ module ViewComponent
9
+ module ContentAreas
10
+ extend ActiveSupport::Concern
11
+
12
+ # Assign the provided content to the content area accessor
13
+ #
14
+ # @private
15
+ def with(area, content = nil, &block)
16
+ unless content_areas.include?(area)
17
+ raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
18
+ end
19
+
20
+ if block_given?
21
+ content = view_context.capture(&block)
22
+ end
23
+
24
+ instance_variable_set("@#{area}".to_sym, content)
25
+ nil
26
+ end
27
+
28
+ class_methods do
29
+ def with_content_areas(*areas)
30
+ ActiveSupport::Deprecation.warn(
31
+ "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
32
+ "Use slots (https://viewcomponent.org/guide/slots.html) instead."
33
+ )
34
+
35
+ if areas.include?(:content)
36
+ raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
37
+ end
38
+
39
+ areas.each do |area|
40
+ define_method area.to_sym do
41
+ content unless content_evaluated? # ensure content is loaded so content_areas will be defined
42
+ instance_variable_get(:"@#{area}") if instance_variable_defined?(:"@#{area}")
43
+ end
44
+ end
45
+
46
+ self.content_areas = areas
47
+ end
48
+ end
49
+ end
50
+ end
@@ -97,6 +97,12 @@ module ViewComponent
97
97
  end
98
98
  end
99
99
 
100
+ initializer "static assets" do |app|
101
+ if app.config.view_component.show_previews
102
+ app.middleware.insert_before(::ActionDispatch::Static, ::ActionDispatch::Static, "#{root}/app/assets/vendor")
103
+ end
104
+ end
105
+
100
106
  config.after_initialize do |app|
101
107
  options = app.config.view_component
102
108
 
@@ -71,19 +71,26 @@ module ViewComponent # :nodoc:
71
71
 
72
72
  # Returns the relative path (from preview_path) to the preview example template if the template exists
73
73
  def preview_example_template_path(example)
74
- preview_path = Array(preview_paths).detect do |preview_path|
75
- Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
76
- end
74
+ preview_path =
75
+ Array(preview_paths).detect do |preview_path|
76
+ Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
77
+ end
77
78
 
78
79
  if preview_path.nil?
79
80
  raise PreviewTemplateError, "preview template for example #{example} does not exist"
80
81
  end
81
82
 
82
83
  path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
83
- Pathname.new(path)
84
- .relative_path_from(Pathname.new(preview_path))
85
- .to_s
86
- .sub(/\..*$/, "")
84
+ Pathname.new(path).
85
+ relative_path_from(Pathname.new(preview_path)).
86
+ to_s.
87
+ sub(/\..*$/, "")
88
+ end
89
+
90
+ # Returns the method body for the example from the preview file.
91
+ def preview_source(example)
92
+ source = self.instance_method(example.to_sym).source.split("\n")
93
+ source[1...(source.size - 1)].join("\n")
87
94
  end
88
95
 
89
96
  private
@@ -2,51 +2,49 @@
2
2
 
3
3
  require "active_support/concern"
4
4
 
5
- module ViewComponent # :nodoc:
5
+ module ViewComponent
6
6
  module Previewable
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  included do
10
- # Set a custom default preview layout through app configuration:
10
+ # Enable or disable component previews:
11
11
  #
12
- # config.view_component.default_preview_layout = "component_preview"
12
+ # config.view_component.show_previews = true
13
13
  #
14
- # This affects preview index pages as well as individual component previews
14
+ # Defaults to `true` in development.
15
+ #
16
+ mattr_accessor :show_previews, instance_writer: false
17
+
18
+ # Set a custom default layout used for preview index and individual previews:
19
+ #
20
+ # config.view_component.default_preview_layout = "component_preview"
15
21
  #
16
22
  mattr_accessor :default_preview_layout, instance_writer: false
17
23
 
18
- # Set the location of component previews through app configuration:
24
+ # Set the location of component previews:
19
25
  #
20
26
  # config.view_component.preview_paths << "#{Rails.root}/lib/component_previews"
21
27
  #
22
28
  mattr_accessor :preview_paths, instance_writer: false
23
29
 
24
- # TODO: deprecated, remove in v3.0.0
30
+ # @deprecated Use `preview_paths` instead. Will be removed in v3.0.0.
25
31
  mattr_accessor :preview_path, instance_writer: false
26
32
 
27
- # Enable or disable component previews through app configuration:
28
- #
29
- # config.view_component.show_previews = true
30
- #
31
- # Defaults to +true+ for development environment
32
- #
33
- mattr_accessor :show_previews, instance_writer: false
34
-
35
- # Set the entry route for component previews through app configuration:
33
+ # Set the entry route for component previews:
36
34
  #
37
35
  # config.view_component.preview_route = "/previews"
38
36
  #
39
- # Defaults to +/rails/view_components+ when `show_previews' is enabled
37
+ # Defaults to `/rails/view_components` when `show_previews` is enabled.
40
38
  #
41
39
  mattr_accessor :preview_route, instance_writer: false do
42
40
  "/rails/view_components"
43
41
  end
44
42
 
45
- # Set the controller to be used for previewing components through app configuration:
43
+ # Set the controller used for previewing components:
46
44
  #
47
45
  # config.view_component.preview_controller = "MyPreviewController"
48
46
  #
49
- # Defaults to the provided +ViewComponentsController+
47
+ # Defaults to `ViewComponentsController`.
50
48
  #
51
49
  mattr_accessor :preview_controller, instance_writer: false do
52
50
  "ViewComponentsController"
@@ -6,7 +6,7 @@ module ViewComponent
6
6
  class SlotV2
7
7
  include ViewComponent::WithContentHelper
8
8
 
9
- attr_writer :_component_instance, :_content_block, :_content
9
+ attr_writer :__vc_component_instance, :__vc_content_block, :__vc_content
10
10
 
11
11
  def initialize(parent)
12
12
  @parent = parent
@@ -21,7 +21,7 @@ module ViewComponent
21
21
  # component instance, returning the string.
22
22
  #
23
23
  # If the slot renderable is a function and returns a string, it is
24
- # set as `@_content` and is returned directly.
24
+ # set as `@__vc_content` and is returned directly.
25
25
  #
26
26
  # If there is no slot renderable, we evaluate the block passed to
27
27
  # the slot and return it.
@@ -30,32 +30,33 @@ module ViewComponent
30
30
 
31
31
  view_context = @parent.send(:view_context)
32
32
 
33
- raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.") if defined?(@_content_block) && defined?(@_content_set_by_with_content)
33
+ raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.") if defined?(@__vc_content_block) && defined?(@__vc_content_set_by_with_content)
34
34
 
35
- @content = if defined?(@_component_instance)
36
- if defined?(@_content_set_by_with_content)
37
- @_component_instance.with_content(@_content_set_by_with_content)
35
+ @content =
36
+ if defined?(@__vc_component_instance)
37
+ if defined?(@__vc_content_set_by_with_content)
38
+ @__vc_component_instance.with_content(@__vc_content_set_by_with_content)
38
39
 
39
- view_context.capture do
40
- @_component_instance.render_in(view_context)
40
+ view_context.capture do
41
+ @__vc_component_instance.render_in(view_context)
42
+ end
43
+ elsif defined?(@__vc_content_block)
44
+ view_context.capture do
45
+ # render_in is faster than `parent.render`
46
+ @__vc_component_instance.render_in(view_context, &@__vc_content_block)
47
+ end
48
+ else
49
+ view_context.capture do
50
+ @__vc_component_instance.render_in(view_context)
51
+ end
41
52
  end
42
- elsif defined?(@_content_block)
43
- view_context.capture do
44
- # render_in is faster than `parent.render`
45
- @_component_instance.render_in(view_context, &@_content_block)
46
- end
47
- else
48
- view_context.capture do
49
- @_component_instance.render_in(view_context)
50
- end
51
- end
52
- elsif defined?(@_content)
53
- @_content
54
- elsif defined?(@_content_block)
55
- view_context.capture(&@_content_block)
56
- elsif defined?(@_content_set_by_with_content)
57
- @_content_set_by_with_content
58
- end
53
+ elsif defined?(@__vc_content)
54
+ @__vc_content
55
+ elsif defined?(@__vc_content_block)
56
+ view_context.capture(&@__vc_content_block)
57
+ elsif defined?(@__vc_content_set_by_with_content)
58
+ @__vc_content_set_by_with_content
59
+ end
59
60
 
60
61
  @content
61
62
  end
@@ -80,7 +81,7 @@ module ViewComponent
80
81
  # end
81
82
  #
82
83
  def method_missing(symbol, *args, &block)
83
- @_component_instance.public_send(symbol, *args, &block)
84
+ @__vc_component_instance.public_send(symbol, *args, &block)
84
85
  end
85
86
 
86
87
  def html_safe?
@@ -88,7 +89,7 @@ module ViewComponent
88
89
  end
89
90
 
90
91
  def respond_to_missing?(symbol, include_all = false)
91
- defined?(@_component_instance) && @_component_instance.respond_to?(symbol, include_all)
92
+ defined?(@__vc_component_instance) && @__vc_component_instance.respond_to?(symbol, include_all)
92
93
  end
93
94
  end
94
95
  end
@@ -123,6 +123,7 @@ module ViewComponent
123
123
  slot_instance = args.present? ? slot_class.new(**args) : slot_class.new
124
124
 
125
125
  # Capture block and assign to slot_instance#content
126
+ # rubocop:disable Rails/OutputSafety
126
127
  slot_instance.content = view_context.capture(&block).to_s.strip.html_safe if block_given?
127
128
 
128
129
  if slot[:collection]
@@ -135,7 +136,7 @@ module ViewComponent
135
136
  # Append Slot instance to collection accessor Array
136
137
  instance_variable_get(slot[:instance_variable_name]) << slot_instance
137
138
  else
138
- # Assign the Slot instance to the slot accessor
139
+ # Assign the Slot instance to the slot accessor
139
140
  instance_variable_set(slot[:instance_variable_name], slot_instance)
140
141
  end
141
142
 
@@ -190,10 +190,10 @@ module ViewComponent
190
190
  content unless content_evaluated? # ensure content is loaded so slots will be defined
191
191
 
192
192
  slot = self.class.registered_slots[slot_name]
193
- @_set_slots ||= {}
193
+ @__vc_set_slots ||= {}
194
194
 
195
- if @_set_slots[slot_name]
196
- return @_set_slots[slot_name]
195
+ if @__vc_set_slots[slot_name]
196
+ return @__vc_set_slots[slot_name]
197
197
  end
198
198
 
199
199
  if slot[:collection]
@@ -217,42 +217,43 @@ module ViewComponent
217
217
  # 2. Since we have to pass block content to components when calling
218
218
  # `render`, evaluating the block here would require us to call
219
219
  # `view_context.capture` twice, which is slower
220
- slot._content_block = block if block_given?
220
+ slot.__vc_content_block = block if block_given?
221
221
 
222
222
  # If class
223
223
  if slot_definition[:renderable]
224
- slot._component_instance = slot_definition[:renderable].new(*args, **kwargs)
224
+ slot.__vc_component_instance = slot_definition[:renderable].new(*args, **kwargs)
225
225
  # If class name as a string
226
226
  elsif slot_definition[:renderable_class_name]
227
- slot._component_instance = self.class.const_get(slot_definition[:renderable_class_name]).new(*args, **kwargs)
227
+ slot.__vc_component_instance = self.class.const_get(slot_definition[:renderable_class_name]).new(*args, **kwargs)
228
228
  # If passed a lambda
229
229
  elsif slot_definition[:renderable_function]
230
230
  # Use `bind(self)` to ensure lambda is executed in the context of the
231
231
  # current component. This is necessary to allow the lambda to access helper
232
232
  # methods like `content_tag` as well as parent component state.
233
- renderable_value = if block_given?
234
- slot_definition[:renderable_function].bind(self).call(*args, **kwargs) do |*args, **kwargs|
235
- view_context.capture(*args, **kwargs, &block)
233
+ renderable_value =
234
+ if block_given?
235
+ slot_definition[:renderable_function].bind(self).call(*args, **kwargs) do |*args, **kwargs|
236
+ view_context.capture(*args, **kwargs, &block)
237
+ end
238
+ else
239
+ slot_definition[:renderable_function].bind(self).call(*args, **kwargs)
236
240
  end
237
- else
238
- slot_definition[:renderable_function].bind(self).call(*args, **kwargs)
239
- end
240
241
 
241
242
  # Function calls can return components, so if it's a component handle it specially
242
243
  if renderable_value.respond_to?(:render_in)
243
- slot._component_instance = renderable_value
244
+ slot.__vc_component_instance = renderable_value
244
245
  else
245
- slot._content = renderable_value
246
+ slot.__vc_content = renderable_value
246
247
  end
247
248
  end
248
249
 
249
- @_set_slots ||= {}
250
+ @__vc_set_slots ||= {}
250
251
 
251
252
  if slot_definition[:collection]
252
- @_set_slots[slot_name] ||= []
253
- @_set_slots[slot_name].push(slot)
253
+ @__vc_set_slots[slot_name] ||= []
254
+ @__vc_set_slots[slot_name].push(slot)
254
255
  else
255
- @_set_slots[slot_name] = slot
256
+ @__vc_set_slots[slot_name] = slot
256
257
  end
257
258
 
258
259
  slot