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

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