view_component 2.31.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.

Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/assets/vendor/prism.css +196 -0
  4. data/app/assets/vendor/prism.min.js +12 -0
  5. data/app/helpers/preview_helper.rb +19 -0
  6. data/app/views/test_mailer/test_email.html.erb +1 -0
  7. data/app/views/view_components/_preview_source.html.erb +17 -0
  8. data/app/views/view_components/preview.html.erb +6 -2
  9. data/{CHANGELOG.md → docs/CHANGELOG.md} +131 -1
  10. data/lib/rails/generators/abstract_generator.rb +29 -0
  11. data/lib/rails/generators/component/component_generator.rb +5 -5
  12. data/lib/rails/generators/erb/component_generator.rb +7 -16
  13. data/lib/rails/generators/haml/component_generator.rb +6 -16
  14. data/lib/rails/generators/slim/component_generator.rb +6 -16
  15. data/lib/view_component.rb +2 -0
  16. data/lib/view_component/base.rb +142 -74
  17. data/lib/view_component/collection.rb +3 -1
  18. data/lib/view_component/compile_cache.rb +1 -0
  19. data/lib/view_component/compiler.rb +58 -54
  20. data/lib/view_component/content_areas.rb +50 -0
  21. data/lib/view_component/engine.rb +19 -2
  22. data/lib/view_component/instrumentation.rb +17 -0
  23. data/lib/view_component/preview.rb +14 -7
  24. data/lib/view_component/previewable.rb +16 -18
  25. data/lib/view_component/slot_v2.rb +28 -27
  26. data/lib/view_component/slotable.rb +2 -1
  27. data/lib/view_component/slotable_v2.rb +23 -22
  28. data/lib/view_component/test_helpers.rb +6 -1
  29. data/lib/view_component/translatable.rb +6 -5
  30. data/lib/view_component/version.rb +1 -1
  31. data/lib/view_component/with_content_helper.rb +1 -1
  32. data/lib/yard/mattr_accessor_handler.rb +19 -0
  33. metadata +94 -43
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rails/generators/abstract_generator"
4
+
3
5
  module Rails
4
6
  module Generators
5
7
  class ComponentGenerator < Rails::Generators::NamedBase
8
+ include ViewComponent::AbstractGenerator
9
+
6
10
  source_root File.expand_path("templates", __dir__)
7
11
 
8
12
  argument :attributes, type: :array, default: [], banner: "attribute"
@@ -10,7 +14,7 @@ module Rails
10
14
  class_option :inline, type: :boolean, default: false
11
15
 
12
16
  def create_component_file
13
- template "component.rb", File.join("app/components", class_path, "#{file_name}_component.rb")
17
+ template "component.rb", File.join(component_path, class_path, "#{file_name}_component.rb")
14
18
  end
15
19
 
16
20
  hook_for :test_framework
@@ -23,10 +27,6 @@ module Rails
23
27
 
24
28
  private
25
29
 
26
- def file_name
27
- @_file_name ||= super.sub(/_component\z/i, "")
28
- end
29
-
30
30
  def parent_class
31
31
  defined?(ApplicationComponent) ? "ApplicationComponent" : "ViewComponent::Base"
32
32
  end
@@ -1,32 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators/erb"
4
+ require "rails/generators/abstract_generator"
4
5
 
5
6
  module Erb
6
7
  module Generators
7
8
  class ComponentGenerator < Base
9
+ include ViewComponent::AbstractGenerator
10
+
8
11
  source_root File.expand_path("templates", __dir__)
9
12
  class_option :sidecar, type: :boolean, default: false
10
13
  class_option :inline, type: :boolean, default: false
11
14
 
12
- def copy_view_file
13
- unless options["inline"]
14
- template "component.html.erb", destination
15
- end
16
- end
17
-
18
- private
19
-
20
- def destination
21
- if options["sidecar"]
22
- File.join("app/components", class_path, "#{file_name}_component", "#{file_name}_component.html.erb")
23
- else
24
- File.join("app/components", class_path, "#{file_name}_component.html.erb")
25
- end
15
+ def engine_name
16
+ "erb"
26
17
  end
27
18
 
28
- def file_name
29
- @_file_name ||= super.sub(/_component\z/i, "")
19
+ def copy_view_file
20
+ super
30
21
  end
31
22
  end
32
23
  end
@@ -5,27 +5,17 @@ require "rails/generators/erb/component_generator"
5
5
  module Haml
6
6
  module Generators
7
7
  class ComponentGenerator < Erb::Generators::ComponentGenerator
8
+ include ViewComponent::AbstractGenerator
9
+
8
10
  source_root File.expand_path("templates", __dir__)
9
11
  class_option :sidecar, type: :boolean, default: false
10
12
 
11
- def copy_view_file
12
- if !options["inline"]
13
- template "component.html.haml", destination
14
- end
15
- end
16
-
17
- private
18
-
19
- def destination
20
- if options["sidecar"]
21
- File.join("app/components", class_path, "#{file_name}_component", "#{file_name}_component.html.haml")
22
- else
23
- File.join("app/components", class_path, "#{file_name}_component.html.haml")
24
- end
13
+ def engine_name
14
+ "haml"
25
15
  end
26
16
 
27
- def file_name
28
- @_file_name ||= super.sub(/_component\z/i, "")
17
+ def copy_view_file
18
+ super
29
19
  end
30
20
  end
31
21
  end
@@ -5,27 +5,17 @@ require "rails/generators/erb/component_generator"
5
5
  module Slim
6
6
  module Generators
7
7
  class ComponentGenerator < Erb::Generators::ComponentGenerator
8
+ include ViewComponent::AbstractGenerator
9
+
8
10
  source_root File.expand_path("templates", __dir__)
9
11
  class_option :sidecar, type: :boolean, default: false
10
12
 
11
- def copy_view_file
12
- if !options["inline"]
13
- template "component.html.slim", destination
14
- end
15
- end
16
-
17
- private
18
-
19
- def destination
20
- if options["sidecar"]
21
- File.join("app/components", class_path, "#{file_name}_component", "#{file_name}_component.html.slim")
22
- else
23
- File.join("app/components", class_path, "#{file_name}_component.html.slim")
24
- end
13
+ def engine_name
14
+ "slim"
25
15
  end
26
16
 
27
- def file_name
28
- @_file_name ||= super.sub(/_component\z/i, "")
17
+ def copy_view_file
18
+ super
29
19
  end
30
20
  end
31
21
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "action_view"
3
4
  require "active_support/dependencies/autoload"
4
5
 
@@ -8,6 +9,7 @@ module ViewComponent
8
9
  autoload :Base
9
10
  autoload :Compiler
10
11
  autoload :ComponentError
12
+ autoload :Instrumentation
11
13
  autoload :Preview
12
14
  autoload :PreviewTemplateError
13
15
  autoload :TestHelpers
@@ -4,6 +4,7 @@ require "action_view"
4
4
  require "active_support/configurable"
5
5
  require "view_component/collection"
6
6
  require "view_component/compile_cache"
7
+ require "view_component/content_areas"
7
8
  require "view_component/previewable"
8
9
  require "view_component/slotable"
9
10
  require "view_component/slotable_v2"
@@ -12,6 +13,7 @@ require "view_component/with_content_helper"
12
13
  module ViewComponent
13
14
  class Base < ActionView::Base
14
15
  include ActiveSupport::Configurable
16
+ include ViewComponent::ContentAreas
15
17
  include ViewComponent::Previewable
16
18
  include ViewComponent::SlotableV2
17
19
  include ViewComponent::WithContentHelper
@@ -30,6 +32,7 @@ module ViewComponent
30
32
  # Hook for allowing components to do work as part of the compilation process.
31
33
  #
32
34
  # For example, one might compile component-specific assets at this point.
35
+ # @private TODO: add documentation
33
36
  def self._after_compile
34
37
  # noop
35
38
  end
@@ -58,6 +61,7 @@ module ViewComponent
58
61
  # returns:
59
62
  # <span title="greeting">Hello, world!</span>
60
63
  #
64
+ # @private
61
65
  def render_in(view_context, &block)
62
66
  self.class.compile(raise_errors: true)
63
67
 
@@ -74,22 +78,24 @@ module ViewComponent
74
78
  @virtual_path ||= virtual_path
75
79
 
76
80
  # For template variants (+phone, +desktop, etc.)
77
- @variant ||= @lookup_context.variants.first
81
+ @__vc_variant ||= @lookup_context.variants.first
78
82
 
79
83
  # For caching, such as #cache_if
80
84
  @current_template = nil unless defined?(@current_template)
81
85
  old_current_template = @current_template
82
86
  @current_template = self
83
87
 
84
- raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.") if block && defined?(@_content_set_by_with_content)
88
+ if block && defined?(@__vc_content_set_by_with_content)
89
+ raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.")
90
+ end
85
91
 
86
- @_content_evaluated = false
87
- @_render_in_block = block
92
+ @__vc_content_evaluated = false
93
+ @__vc_render_in_block = block
88
94
 
89
95
  before_render
90
96
 
91
97
  if render?
92
- render_template_for(@variant)
98
+ render_template_for(@__vc_variant).to_s + _output_postamble
93
99
  else
94
100
  ""
95
101
  end
@@ -97,18 +103,36 @@ module ViewComponent
97
103
  @current_template = old_current_template
98
104
  end
99
105
 
106
+ # EXPERIMENTAL: Optional content to be returned after the rendered template.
107
+ #
108
+ # @return [String]
109
+ def _output_postamble
110
+ ""
111
+ end
112
+
113
+ # Called before rendering the component. Override to perform operations that depend on having access to the view context, such as helpers.
114
+ #
115
+ # @return [void]
100
116
  def before_render
101
117
  before_render_check
102
118
  end
103
119
 
120
+ # Called after rendering the component.
121
+ #
122
+ # @deprecated Use `#before_render` instead. Will be removed in v3.0.0.
123
+ # @return [void]
104
124
  def before_render_check
105
125
  # noop
106
126
  end
107
127
 
128
+ # Override to determine whether the ViewComponent should render.
129
+ #
130
+ # @return [Boolean]
108
131
  def render?
109
132
  true
110
133
  end
111
134
 
135
+ # @private
112
136
  def initialize(*); end
113
137
 
114
138
  # Re-use original view_context if we're not rendering a component.
@@ -116,6 +140,8 @@ module ViewComponent
116
140
  # This prevents an exception when rendering a partial inside of a component that has also been rendered outside
117
141
  # of the component. This is due to the partials compiled template method existing in the parent `view_context`,
118
142
  # and not the component's `view_context`.
143
+ #
144
+ # @private
119
145
  def render(options = {}, args = {}, &block)
120
146
  if options.is_a? ViewComponent::Base
121
147
  super
@@ -124,60 +150,62 @@ module ViewComponent
124
150
  end
125
151
  end
126
152
 
153
+ # The current controller. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
154
+ #
155
+ # @return [ActionController::Base]
127
156
  def controller
128
157
  raise ViewContextCalledBeforeRenderError, "`controller` can only be called at render time." if view_context.nil?
129
- @controller ||= view_context.controller
158
+
159
+ @__vc_controller ||= view_context.controller
130
160
  end
131
161
 
132
- # Provides a proxy to access helper methods from the context of the current controller
162
+ # A proxy through which to access helpers. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
163
+ #
164
+ # @return [ActionView::Base]
133
165
  def helpers
134
166
  raise ViewContextCalledBeforeRenderError, "`helpers` can only be called at render time." if view_context.nil?
135
- @helpers ||= controller.view_context
167
+
168
+ @__vc_helpers ||= controller.view_context
136
169
  end
137
170
 
138
171
  # Exposes .virtual_path as an instance method
172
+ #
173
+ # @private
139
174
  def virtual_path
140
175
  self.class.virtual_path
141
176
  end
142
177
 
143
178
  # For caching, such as #cache_if
179
+ # @private
144
180
  def view_cache_dependencies
145
181
  []
146
182
  end
147
183
 
148
184
  # For caching, such as #cache_if
185
+ #
186
+ # @private
149
187
  def format
150
188
  # Ruby 2.6 throws a warning without checking `defined?`, 2.7 does not
151
- if defined?(@variant)
152
- @variant
153
- end
154
- end
155
-
156
- # Assign the provided content to the content area accessor
157
- def with(area, content = nil, &block)
158
- unless content_areas.include?(area)
159
- raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
160
- end
161
-
162
- if block_given?
163
- content = view_context.capture(&block)
189
+ if defined?(@__vc_variant)
190
+ @__vc_variant
164
191
  end
165
-
166
- instance_variable_set("@#{area}".to_sym, content)
167
- nil
168
192
  end
169
193
 
194
+ # Use the provided variant instead of the one determined by the current request.
195
+ #
196
+ # @param variant [Symbol] The variant to be used by the component.
197
+ # @return [self]
170
198
  def with_variant(variant)
171
- @variant = variant
199
+ @__vc_variant = variant
172
200
 
173
201
  self
174
202
  end
175
203
 
176
- # Exposes the current request to the component.
177
- # Use sparingly as doing so introduces coupling
178
- # that inhibits encapsulation & reuse.
204
+ # The current request. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
205
+ #
206
+ # @return [ActionDispatch::Request]
179
207
  def request
180
- @request ||= controller.request
208
+ @request ||= controller.request if controller.respond_to?(:request)
181
209
  end
182
210
 
183
211
  private
@@ -185,31 +213,54 @@ module ViewComponent
185
213
  attr_reader :view_context
186
214
 
187
215
  def content
188
- return @_content if defined?(@_content)
189
- @_content_evaluated = true
190
-
191
- @_content = if @view_context && @_render_in_block
192
- view_context.capture(self, &@_render_in_block)
193
- elsif defined?(@_content_set_by_with_content)
194
- @_content_set_by_with_content
195
- end
216
+ @__vc_content_evaluated = true
217
+ return @__vc_content if defined?(@__vc_content)
218
+
219
+ @__vc_content =
220
+ if @view_context && @__vc_render_in_block
221
+ view_context.capture(self, &@__vc_render_in_block)
222
+ elsif defined?(@__vc_content_set_by_with_content)
223
+ @__vc_content_set_by_with_content
224
+ end
196
225
  end
197
226
 
198
227
  def content_evaluated?
199
- @_content_evaluated
228
+ @__vc_content_evaluated
200
229
  end
201
230
 
202
- # The controller used for testing components.
203
- # Defaults to ApplicationController, but can be configured
204
- # on a per-test basis using `with_controller_class`.
205
- # This should be set early in the initialization process and should be a string.
231
+ # Set the controller used for testing components:
232
+ #
233
+ # config.view_component.test_controller = "MyTestController"
234
+ #
235
+ # Defaults to ApplicationController. Can also be configured on a per-test
236
+ # basis using `with_controller_class`.
237
+ #
206
238
  mattr_accessor :test_controller
207
239
  @@test_controller = "ApplicationController"
208
240
 
209
- # Configure if render monkey patches should be included or not in Rails <6.1.
241
+ # Set if render monkey patches should be included or not in Rails <6.1:
242
+ #
243
+ # config.view_component.render_monkey_patch_enabled = false
244
+ #
210
245
  mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
211
246
 
247
+ # Enable or disable source code previews in component previews:
248
+ #
249
+ # config.view_component.show_previews_source = true
250
+ #
251
+ # Defaults to `false`.
252
+ #
253
+ mattr_accessor :show_previews_source, instance_writer: false, default: false
254
+
255
+ # Path for component files
256
+ #
257
+ # config.view_component.view_component_path = "app/my_components"
258
+ #
259
+ # Defaults to "app/components".
260
+ mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
261
+
212
262
  class << self
263
+ # @private
213
264
  attr_accessor :source_location, :virtual_path
214
265
 
215
266
  # EXPERIMENTAL: This API is experimental and may be removed at any time.
@@ -219,6 +270,7 @@ module ViewComponent
219
270
  # strings starting without the "dot", example: `["erb", "haml"]`.
220
271
  #
221
272
  # For example, one might collect sidecar CSS files that need to be compiled.
273
+ # @private TODO: add documentation
222
274
  def _sidecar_files(extensions)
223
275
  return [] unless source_location
224
276
 
@@ -239,11 +291,12 @@ module ViewComponent
239
291
  # end
240
292
  #
241
293
  # Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
242
- nested_component_files = if name.include?("::") && component_name != filename
243
- Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
244
- else
245
- []
246
- end
294
+ nested_component_files =
295
+ if name.include?("::") && component_name != filename
296
+ Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
297
+ else
298
+ []
299
+ end
247
300
 
248
301
  # view files in the same directory as the component
249
302
  sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]
@@ -253,16 +306,24 @@ module ViewComponent
253
306
  (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files).uniq
254
307
  end
255
308
 
256
- # Render a component collection.
309
+ # Render a component for each element in a collection ([documentation](/guide/collections)):
310
+ #
311
+ # render(ProductsComponent.with_collection(@products, foo: :bar))
312
+ #
313
+ # @param collection [Enumerable] A list of items to pass the ViewComponent one at a time.
314
+ # @param args [Arguments] Arguments to pass to the ViewComponent every time.
257
315
  def with_collection(collection, **args)
258
316
  Collection.new(self, collection, **args)
259
317
  end
260
318
 
261
319
  # Provide identifier for ActionView template annotations
320
+ #
321
+ # @private
262
322
  def short_identifier
263
323
  @short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
264
324
  end
265
325
 
326
+ # @private
266
327
  def inherited(child)
267
328
  # Compile so child will inherit compiled `call_*` template methods that
268
329
  # `compile` defines
@@ -280,11 +341,14 @@ module ViewComponent
280
341
  child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].absolute_path
281
342
 
282
343
  # Removes the first part of the path and the extension.
283
- child.virtual_path = child.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
344
+ child.virtual_path = child.source_location.gsub(
345
+ %r{(.*#{Regexp.quote(ViewComponent::Base.view_component_path)})|(\.rb)}, ""
346
+ )
284
347
 
285
348
  super
286
349
  end
287
350
 
351
+ # @private
288
352
  def compiled?
289
353
  compiler.compiled?
290
354
  end
@@ -293,50 +357,39 @@ module ViewComponent
293
357
  #
294
358
  # Do as much work as possible in this step, as doing so reduces the amount
295
359
  # of work done each time a component is rendered.
360
+ # @private
296
361
  def compile(raise_errors: false)
297
362
  compiler.compile(raise_errors: raise_errors)
298
363
  end
299
364
 
365
+ # @private
300
366
  def compiler
301
- @_compiler ||= Compiler.new(self)
367
+ @__vc_compiler ||= Compiler.new(self)
302
368
  end
303
369
 
304
370
  # we'll eventually want to update this to support other types
371
+ # @private
305
372
  def type
306
373
  "text/html"
307
374
  end
308
375
 
376
+ # @private
309
377
  def format
310
378
  :html
311
379
  end
312
380
 
381
+ # @private
313
382
  def identifier
314
383
  source_location
315
384
  end
316
385
 
317
- def with_content_areas(*areas)
318
- ActiveSupport::Deprecation.warn(
319
- "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
320
- "Use slots (https://viewcomponent.org/guide/slots.html) instead."
321
- )
322
-
323
- if areas.include?(:content)
324
- raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
325
- end
326
-
327
- areas.each do |area|
328
- define_method area.to_sym do
329
- content unless content_evaluated? # ensure content is loaded so content_areas will be defined
330
- instance_variable_get(:"@#{area}") if instance_variable_defined?(:"@#{area}")
331
- end
332
- end
333
-
334
- self.content_areas = areas
335
- end
336
-
337
- # Support overriding collection parameter name
338
- def with_collection_parameter(param)
339
- @provided_collection_parameter = param
386
+ # Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
387
+ #
388
+ # with_collection_parameter :item
389
+ #
390
+ # @param parameter [Symbol] The parameter name used when rendering elements of a collection.
391
+ def with_collection_parameter(parameter)
392
+ @provided_collection_parameter = parameter
340
393
  end
341
394
 
342
395
  # Ensure the component initializer accepts the
@@ -344,6 +397,7 @@ module ViewComponent
344
397
  # validate that the default parameter name
345
398
  # is accepted, as support for collection
346
399
  # rendering is optional.
400
+ # @private TODO: add documentation
347
401
  def validate_collection_parameter!(validate_default: false)
348
402
  parameter = validate_default ? collection_parameter : provided_collection_parameter
349
403
 
@@ -368,6 +422,7 @@ module ViewComponent
368
422
  # Ensure the component initializer does not define
369
423
  # invalid parameters that could override the framework's
370
424
  # methods.
425
+ # @private TODO: add documentation
371
426
  def validate_initialization_parameters!
372
427
  return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
373
428
 
@@ -378,6 +433,7 @@ module ViewComponent
378
433
  )
379
434
  end
380
435
 
436
+ # @private
381
437
  def collection_parameter
382
438
  if provided_collection_parameter
383
439
  provided_collection_parameter
@@ -386,14 +442,26 @@ module ViewComponent
386
442
  end
387
443
  end
388
444
 
445
+ # @private
389
446
  def collection_counter_parameter
390
447
  "#{collection_parameter}_counter".to_sym
391
448
  end
392
449
 
450
+ # @private
393
451
  def counter_argument_present?
394
452
  initialize_parameter_names.include?(collection_counter_parameter)
395
453
  end
396
454
 
455
+ # @private
456
+ def collection_iteration_parameter
457
+ "#{collection_parameter}_iteration".to_sym
458
+ end
459
+
460
+ # @private
461
+ def iteration_argument_present?
462
+ initialize_parameter_names.include?(collection_iteration_parameter)
463
+ end
464
+
397
465
  private
398
466
 
399
467
  def initialize_parameter_names