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

@@ -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