view_component 2.50.0 → 2.69.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.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/app/assets/vendor/prism.css +3 -195
- data/app/assets/vendor/prism.min.js +11 -11
- data/app/controllers/concerns/view_component/preview_actions.rb +97 -0
- data/app/controllers/view_components_controller.rb +1 -87
- data/app/helpers/preview_helper.rb +5 -5
- data/app/views/view_components/preview.html.erb +2 -2
- data/docs/CHANGELOG.md +427 -1
- data/lib/rails/generators/abstract_generator.rb +7 -9
- data/lib/rails/generators/component/component_generator.rb +5 -4
- data/lib/rails/generators/locale/component_generator.rb +1 -1
- data/lib/rails/generators/preview/component_generator.rb +1 -1
- data/lib/view_component/base.rb +152 -51
- data/lib/view_component/collection.rb +9 -2
- data/lib/view_component/compiler.rb +39 -18
- data/lib/view_component/config.rb +159 -0
- data/lib/view_component/content_areas.rb +1 -1
- data/lib/view_component/docs_builder_component.rb +1 -1
- data/lib/view_component/engine.rb +16 -30
- data/lib/view_component/polymorphic_slots.rb +28 -1
- data/lib/view_component/preview.rb +12 -9
- data/lib/view_component/render_component_helper.rb +1 -0
- data/lib/view_component/render_component_to_string_helper.rb +1 -1
- data/lib/view_component/render_to_string_monkey_patch.rb +1 -1
- data/lib/view_component/rendering_component_helper.rb +1 -1
- data/lib/view_component/rendering_monkey_patch.rb +1 -1
- data/lib/view_component/slot_v2.rb +4 -10
- data/lib/view_component/slotable.rb +5 -6
- data/lib/view_component/slotable_v2.rb +69 -21
- data/lib/view_component/test_helpers.rb +80 -8
- data/lib/view_component/translatable.rb +13 -14
- data/lib/view_component/version.rb +1 -1
- data/lib/view_component.rb +1 -0
- metadata +47 -18
- data/lib/view_component/previewable.rb +0 -62
data/lib/view_component/base.rb
CHANGED
@@ -4,19 +4,30 @@ 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/compiler"
|
8
|
+
require "view_component/config"
|
7
9
|
require "view_component/content_areas"
|
8
10
|
require "view_component/polymorphic_slots"
|
9
|
-
require "view_component/
|
11
|
+
require "view_component/preview"
|
10
12
|
require "view_component/slotable"
|
11
13
|
require "view_component/slotable_v2"
|
14
|
+
require "view_component/translatable"
|
12
15
|
require "view_component/with_content_helper"
|
13
16
|
|
14
17
|
module ViewComponent
|
15
18
|
class Base < ActionView::Base
|
16
|
-
|
19
|
+
class << self
|
20
|
+
delegate(*ViewComponent::Config.defaults.keys, to: :config)
|
21
|
+
|
22
|
+
def config
|
23
|
+
@config ||= ViewComponent::Config.defaults
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
17
27
|
include ViewComponent::ContentAreas
|
18
|
-
include ViewComponent::
|
28
|
+
include ViewComponent::PolymorphicSlots
|
19
29
|
include ViewComponent::SlotableV2
|
30
|
+
include ViewComponent::Translatable
|
20
31
|
include ViewComponent::WithContentHelper
|
21
32
|
|
22
33
|
ViewContextCalledBeforeRenderError = Class.new(StandardError)
|
@@ -29,8 +40,25 @@ module ViewComponent
|
|
29
40
|
class_attribute :content_areas
|
30
41
|
self.content_areas = [] # class_attribute:default doesn't work until Rails 5.2
|
31
42
|
|
43
|
+
# Config option that strips trailing whitespace in templates before compiling them.
|
44
|
+
class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false
|
45
|
+
self.__vc_strip_trailing_whitespace = false # class_attribute:default doesn't work until Rails 5.2
|
46
|
+
|
32
47
|
attr_accessor :__vc_original_view_context
|
33
48
|
|
49
|
+
# Components render in their own view context. Helpers and other functionality
|
50
|
+
# require a reference to the original Rails view context, an instance of
|
51
|
+
# `ActionView::Base`. Use this method to set a reference to the original
|
52
|
+
# view context. Objects that implement this method will render in the component's
|
53
|
+
# view context, while objects that don't will render in the original view context
|
54
|
+
# so helpers, etc work as expected.
|
55
|
+
#
|
56
|
+
# @param view_context [ActionView::Base] The original view context.
|
57
|
+
# @return [void]
|
58
|
+
def set_original_view_context(view_context)
|
59
|
+
self.__vc_original_view_context = view_context
|
60
|
+
end
|
61
|
+
|
34
62
|
# EXPERIMENTAL: This API is experimental and may be removed at any time.
|
35
63
|
# Hook for allowing components to do work as part of the compilation process.
|
36
64
|
#
|
@@ -71,6 +99,8 @@ module ViewComponent
|
|
71
99
|
@view_context = view_context
|
72
100
|
self.__vc_original_view_context ||= view_context
|
73
101
|
|
102
|
+
@output_buffer = ActionView::OutputBuffer.new
|
103
|
+
|
74
104
|
@lookup_context ||= view_context.lookup_context
|
75
105
|
|
76
106
|
# required for path helpers in older Rails versions
|
@@ -91,11 +121,9 @@ module ViewComponent
|
|
91
121
|
@current_template = self
|
92
122
|
|
93
123
|
if block && defined?(@__vc_content_set_by_with_content)
|
94
|
-
raise ArgumentError.
|
95
|
-
"It looks like a block was provided after calling `with_content` on #{self.class.name}, " \
|
124
|
+
raise ArgumentError, "It looks like a block was provided after calling `with_content` on #{self.class.name}, " \
|
96
125
|
"which means that ViewComponent doesn't know which content to use.\n\n" \
|
97
126
|
"To fix this issue, use either `with_content` or a block."
|
98
|
-
)
|
99
127
|
end
|
100
128
|
|
101
129
|
@__vc_content_evaluated = false
|
@@ -104,6 +132,11 @@ module ViewComponent
|
|
104
132
|
before_render
|
105
133
|
|
106
134
|
if render?
|
135
|
+
# Ensure `content` is evaluated before rendering the template, this is
|
136
|
+
# needed so slots and other side-effects are performed before the
|
137
|
+
# component template is evaluated.
|
138
|
+
content if self.class.use_consistent_rendering_lifecycle
|
139
|
+
|
107
140
|
render_template_for(@__vc_variant).to_s + _output_postamble
|
108
141
|
else
|
109
142
|
""
|
@@ -112,6 +145,21 @@ module ViewComponent
|
|
112
145
|
@current_template = old_current_template
|
113
146
|
end
|
114
147
|
|
148
|
+
# Subclass components that call `super` inside their template code will cause a
|
149
|
+
# double render if they emit the result:
|
150
|
+
#
|
151
|
+
# ```erb
|
152
|
+
# <%= super %> # double-renders
|
153
|
+
# <% super %> # does not double-render
|
154
|
+
# ```
|
155
|
+
#
|
156
|
+
# Calls `super`, returning `nil` to avoid rendering the result twice.
|
157
|
+
def render_parent
|
158
|
+
mtd = @__vc_variant ? "call_#{@__vc_variant}" : "call"
|
159
|
+
method(mtd).super_method.call
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
|
115
163
|
# EXPERIMENTAL: Optional content to be returned after the rendered template.
|
116
164
|
#
|
117
165
|
# @return [String]
|
@@ -143,7 +191,8 @@ module ViewComponent
|
|
143
191
|
end
|
144
192
|
|
145
193
|
# @private
|
146
|
-
def initialize(*)
|
194
|
+
def initialize(*)
|
195
|
+
end
|
147
196
|
|
148
197
|
# Re-use original view_context if we're not rendering a component.
|
149
198
|
#
|
@@ -153,8 +202,8 @@ module ViewComponent
|
|
153
202
|
#
|
154
203
|
# @private
|
155
204
|
def render(options = {}, args = {}, &block)
|
156
|
-
if options.
|
157
|
-
options.__vc_original_view_context
|
205
|
+
if options.respond_to?(:set_original_view_context)
|
206
|
+
options.set_original_view_context(self.__vc_original_view_context)
|
158
207
|
super
|
159
208
|
else
|
160
209
|
__vc_original_view_context.render(options, args, &block)
|
@@ -224,9 +273,7 @@ module ViewComponent
|
|
224
273
|
# @private
|
225
274
|
def format
|
226
275
|
# Ruby 2.6 throws a warning without checking `defined?`, 2.7 doesn't
|
227
|
-
if defined?(@__vc_variant)
|
228
|
-
@__vc_variant
|
229
|
-
end
|
276
|
+
@__vc_variant if defined?(@__vc_variant)
|
230
277
|
end
|
231
278
|
|
232
279
|
# Use the provided variant instead of the one determined by the current request.
|
@@ -271,36 +318,51 @@ module ViewComponent
|
|
271
318
|
|
272
319
|
# Set the controller used for testing components:
|
273
320
|
#
|
274
|
-
#
|
321
|
+
# ```ruby
|
322
|
+
# config.view_component.test_controller = "MyTestController"
|
323
|
+
# ```
|
275
324
|
#
|
276
|
-
# Defaults to ApplicationController. Can also be
|
277
|
-
# basis using `with_controller_class`.
|
325
|
+
# Defaults to `nil`. If this is falsy, `"ApplicationController"` is used. Can also be
|
326
|
+
# configured on a per-test basis using `with_controller_class`.
|
278
327
|
#
|
279
|
-
mattr_accessor :test_controller
|
280
|
-
@@test_controller = "ApplicationController"
|
281
328
|
|
282
329
|
# Set if render monkey patches should be included or not in Rails <6.1:
|
283
330
|
#
|
284
|
-
#
|
331
|
+
# ```ruby
|
332
|
+
# config.view_component.render_monkey_patch_enabled = false
|
333
|
+
# ```
|
285
334
|
#
|
286
|
-
mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
|
287
335
|
|
288
336
|
# Path for component files
|
289
337
|
#
|
290
|
-
#
|
338
|
+
# ```ruby
|
339
|
+
# config.view_component.view_component_path = "app/my_components"
|
340
|
+
# ```
|
341
|
+
#
|
342
|
+
# Defaults to `nil`. If this is falsy, `app/components` is used.
|
343
|
+
#
|
344
|
+
|
345
|
+
# Evaluate `#content` before `#call` to ensure side-effects are present
|
346
|
+
# during component renders. This will be the default behavior in a future
|
347
|
+
# release.
|
348
|
+
#
|
349
|
+
# ```ruby
|
350
|
+
# config.view_component.use_consistent_rendering_lifecycle = true
|
351
|
+
# ```
|
291
352
|
#
|
292
|
-
# Defaults to `
|
353
|
+
# Defaults to `false`
|
293
354
|
#
|
294
|
-
mattr_accessor :
|
355
|
+
mattr_accessor :use_consistent_rendering_lifecycle, instance_writer: false, default: false
|
295
356
|
|
296
357
|
# Parent class for generated components
|
297
358
|
#
|
298
|
-
#
|
359
|
+
# ```ruby
|
360
|
+
# config.view_component.component_parent_class = "MyBaseComponent"
|
361
|
+
# ```
|
299
362
|
#
|
300
363
|
# Defaults to nil. If this is falsy, generators will use
|
301
364
|
# "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
|
302
365
|
#
|
303
|
-
mattr_accessor :component_parent_class, instance_writer: false
|
304
366
|
|
305
367
|
# Configuration for generators.
|
306
368
|
#
|
@@ -311,25 +373,33 @@ module ViewComponent
|
|
311
373
|
#
|
312
374
|
# Always generate a component with a sidecar directory:
|
313
375
|
#
|
314
|
-
#
|
376
|
+
# ```ruby
|
377
|
+
# config.view_component.generate.sidecar = true
|
378
|
+
# ```
|
315
379
|
#
|
316
380
|
# #### #stimulus_controller
|
317
381
|
#
|
318
382
|
# Always generate a Stimulus controller alongside the component:
|
319
383
|
#
|
320
|
-
#
|
384
|
+
# ```ruby
|
385
|
+
# config.view_component.generate.stimulus_controller = true
|
386
|
+
# ```
|
321
387
|
#
|
322
388
|
# #### #locale
|
323
389
|
#
|
324
390
|
# Always generate translations file alongside the component:
|
325
391
|
#
|
326
|
-
#
|
392
|
+
# ```ruby
|
393
|
+
# config.view_component.generate.locale = true
|
394
|
+
# ```
|
327
395
|
#
|
328
396
|
# #### #distinct_locale_files
|
329
397
|
#
|
330
398
|
# Always generate as many translations files as available locales:
|
331
399
|
#
|
332
|
-
#
|
400
|
+
# ```ruby
|
401
|
+
# config.view_component.generate.distinct_locale_files = true
|
402
|
+
# ```
|
333
403
|
#
|
334
404
|
# One file will be generated for each configured `I18n.available_locales`,
|
335
405
|
# falling back to `[:en]` when no `available_locales` is defined.
|
@@ -338,10 +408,11 @@ module ViewComponent
|
|
338
408
|
#
|
339
409
|
# Always generate preview alongside the component:
|
340
410
|
#
|
341
|
-
#
|
411
|
+
# ```ruby
|
412
|
+
# config.view_component.generate.preview = true
|
413
|
+
# ```
|
342
414
|
#
|
343
415
|
# Defaults to `false`.
|
344
|
-
mattr_accessor :generate, instance_writer: false, default: ActiveSupport::OrderedOptions.new(false)
|
345
416
|
|
346
417
|
class << self
|
347
418
|
# @private
|
@@ -392,7 +463,9 @@ module ViewComponent
|
|
392
463
|
|
393
464
|
# Render a component for each element in a collection ([documentation](/guide/collections)):
|
394
465
|
#
|
395
|
-
#
|
466
|
+
# ```ruby
|
467
|
+
# render(ProductsComponent.with_collection(@products, foo: :bar))
|
468
|
+
# ```
|
396
469
|
#
|
397
470
|
# @param collection [Enumerable] A list of items to pass the ViewComponent one at a time.
|
398
471
|
# @param args [Arguments] Arguments to pass to the ViewComponent every time.
|
@@ -413,10 +486,26 @@ module ViewComponent
|
|
413
486
|
# `compile` defines
|
414
487
|
compile
|
415
488
|
|
489
|
+
# Give the child its own personal #render_template_for to protect against the case when
|
490
|
+
# eager loading is disabled and the parent component is rendered before the child. In
|
491
|
+
# such a scenario, the parent will override ViewComponent::Base#render_template_for,
|
492
|
+
# meaning it will not be called for any children and thus not compile their templates.
|
493
|
+
if !child.instance_methods(false).include?(:render_template_for) && !child.compiled?
|
494
|
+
child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
495
|
+
def render_template_for(variant = nil)
|
496
|
+
# Force compilation here so the compiler always redefines render_template_for.
|
497
|
+
# This is mostly a safeguard to prevent infinite recursion.
|
498
|
+
self.class.compile(raise_errors: true, force: true)
|
499
|
+
# .compile replaces this method; call the new one
|
500
|
+
render_template_for(variant)
|
501
|
+
end
|
502
|
+
RUBY
|
503
|
+
end
|
504
|
+
|
416
505
|
# If Rails application is loaded, add application url_helpers to the component context
|
417
506
|
# we need to check this to use this gem as a dependency
|
418
|
-
if defined?(Rails) && Rails.application
|
419
|
-
child.include Rails.application.routes.url_helpers
|
507
|
+
if defined?(Rails) && Rails.application && !(child < Rails.application.routes.url_helpers)
|
508
|
+
child.include Rails.application.routes.url_helpers
|
420
509
|
end
|
421
510
|
|
422
511
|
# Derive the source location of the component Ruby file from the call stack.
|
@@ -426,7 +515,7 @@ module ViewComponent
|
|
426
515
|
|
427
516
|
# Removes the first part of the path and the extension.
|
428
517
|
child.virtual_path = child.source_location.gsub(
|
429
|
-
|
518
|
+
/(.*#{Regexp.quote(ViewComponent::Base.config.view_component_path)})|(\.rb)/, ""
|
430
519
|
)
|
431
520
|
|
432
521
|
# Set collection parameter to the extended component
|
@@ -445,8 +534,8 @@ module ViewComponent
|
|
445
534
|
# Do as much work as possible in this step, as doing so reduces the amount
|
446
535
|
# of work done each time a component is rendered.
|
447
536
|
# @private
|
448
|
-
def compile(raise_errors: false)
|
449
|
-
compiler.compile(raise_errors: raise_errors)
|
537
|
+
def compile(raise_errors: false, force: false)
|
538
|
+
compiler.compile(raise_errors: raise_errors, force: force)
|
450
539
|
end
|
451
540
|
|
452
541
|
# @private
|
@@ -472,13 +561,35 @@ module ViewComponent
|
|
472
561
|
|
473
562
|
# Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
|
474
563
|
#
|
475
|
-
#
|
564
|
+
# ```ruby
|
565
|
+
# with_collection_parameter :item
|
566
|
+
# ```
|
476
567
|
#
|
477
568
|
# @param parameter [Symbol] The parameter name used when rendering elements of a collection.
|
478
569
|
def with_collection_parameter(parameter)
|
479
570
|
@provided_collection_parameter = parameter
|
480
571
|
end
|
481
572
|
|
573
|
+
# Strips trailing whitespace from templates before compiling them.
|
574
|
+
#
|
575
|
+
# ```ruby
|
576
|
+
# class MyComponent < ViewComponent::Base
|
577
|
+
# strip_trailing_whitespace
|
578
|
+
# end
|
579
|
+
# ```
|
580
|
+
#
|
581
|
+
# @param value [Boolean] Whether or not to strip newlines.
|
582
|
+
def strip_trailing_whitespace(value = true)
|
583
|
+
self.__vc_strip_trailing_whitespace = value
|
584
|
+
end
|
585
|
+
|
586
|
+
# Whether trailing whitespace will be stripped before compilation.
|
587
|
+
#
|
588
|
+
# @return [Boolean]
|
589
|
+
def strip_trailing_whitespace?
|
590
|
+
__vc_strip_trailing_whitespace
|
591
|
+
end
|
592
|
+
|
482
593
|
# Ensure the component initializer accepts the
|
483
594
|
# collection parameter. By default, we don't
|
484
595
|
# validate that the default parameter name
|
@@ -495,20 +606,16 @@ module ViewComponent
|
|
495
606
|
# parameters will be empty and ViewComponent will not be able to render
|
496
607
|
# the component.
|
497
608
|
if initialize_parameters.empty?
|
498
|
-
raise ArgumentError.
|
499
|
-
"The #{self} initializer is empty or invalid." \
|
609
|
+
raise ArgumentError, "The #{self} initializer is empty or invalid." \
|
500
610
|
"It must accept the parameter `#{parameter}` to render it as a collection.\n\n" \
|
501
611
|
"To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
|
502
612
|
"See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
|
503
|
-
)
|
504
613
|
end
|
505
614
|
|
506
|
-
raise ArgumentError
|
507
|
-
"The initializer for #{self} doesn't accept the parameter `#{parameter}`, " \
|
615
|
+
raise ArgumentError, "The initializer for #{self} doesn't accept the parameter `#{parameter}`, " \
|
508
616
|
"which is required in order to render it as a collection.\n\n" \
|
509
617
|
"To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
|
510
618
|
"See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
|
511
|
-
)
|
512
619
|
end
|
513
620
|
|
514
621
|
# Ensure the component initializer doesn't define
|
@@ -518,19 +625,13 @@ module ViewComponent
|
|
518
625
|
def validate_initialization_parameters!
|
519
626
|
return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
|
520
627
|
|
521
|
-
raise ViewComponent::ComponentError
|
522
|
-
"#{self} initializer can't accept the parameter `#{RESERVED_PARAMETER}`, as it will override a " \
|
628
|
+
raise ViewComponent::ComponentError, "#{self} initializer can't accept the parameter `#{RESERVED_PARAMETER}`, as it will override a " \
|
523
629
|
"public ViewComponent method. To fix this issue, rename the parameter."
|
524
|
-
)
|
525
630
|
end
|
526
631
|
|
527
632
|
# @private
|
528
633
|
def collection_parameter
|
529
|
-
|
530
|
-
provided_collection_parameter
|
531
|
-
else
|
532
|
-
name && name.demodulize.underscore.chomp("_component").to_sym
|
533
|
-
end
|
634
|
+
provided_collection_parameter || name && name.demodulize.underscore.chomp("_component").to_sym
|
534
635
|
end
|
535
636
|
|
536
637
|
# @private
|
@@ -10,10 +10,17 @@ module ViewComponent
|
|
10
10
|
delegate :format, to: :component
|
11
11
|
delegate :size, to: :@collection
|
12
12
|
|
13
|
+
attr_accessor :__vc_original_view_context
|
14
|
+
|
15
|
+
def set_original_view_context(view_context)
|
16
|
+
self.__vc_original_view_context = view_context
|
17
|
+
end
|
18
|
+
|
13
19
|
def render_in(view_context, &block)
|
14
20
|
components.map do |component|
|
21
|
+
component.set_original_view_context(__vc_original_view_context)
|
15
22
|
component.render_in(view_context, &block)
|
16
|
-
end.join.html_safe
|
23
|
+
end.join.html_safe
|
17
24
|
end
|
18
25
|
|
19
26
|
def components
|
@@ -54,7 +61,7 @@ module ViewComponent
|
|
54
61
|
end
|
55
62
|
|
56
63
|
def component_options(item, iterator)
|
57
|
-
item_options = {
|
64
|
+
item_options = {component.collection_parameter => item}
|
58
65
|
item_options[component.collection_counter_parameter] = iterator.index + 1 if component.counter_argument_present?
|
59
66
|
item_options[component.collection_iteration_parameter] = iterator.dup if component.iteration_argument_present?
|
60
67
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "concurrent-ruby"
|
4
|
+
|
3
5
|
module ViewComponent
|
4
6
|
class Compiler
|
5
7
|
# Lock required to be obtained before compiling the component
|
@@ -16,7 +18,7 @@ module ViewComponent
|
|
16
18
|
|
17
19
|
def initialize(component_class)
|
18
20
|
@component_class = component_class
|
19
|
-
@__vc_compiler_lock =
|
21
|
+
@__vc_compiler_lock = Concurrent::ReadWriteLock.new
|
20
22
|
end
|
21
23
|
|
22
24
|
def compiled?
|
@@ -27,10 +29,13 @@ module ViewComponent
|
|
27
29
|
self.class.mode == DEVELOPMENT_MODE
|
28
30
|
end
|
29
31
|
|
30
|
-
def compile(raise_errors: false)
|
31
|
-
return if compiled?
|
32
|
+
def compile(raise_errors: false, force: false)
|
33
|
+
return if compiled? && !force
|
34
|
+
return if component_class == ViewComponent::Base
|
35
|
+
|
36
|
+
component_class.superclass.compile(raise_errors: raise_errors) if should_compile_superclass?
|
32
37
|
|
33
|
-
|
38
|
+
with_write_lock do
|
34
39
|
CompileCache.invalidate_class!(component_class)
|
35
40
|
|
36
41
|
subclass_instance_methods = component_class.instance_methods(false)
|
@@ -69,30 +74,36 @@ module ViewComponent
|
|
69
74
|
component_class.send(:undef_method, method_name.to_sym)
|
70
75
|
end
|
71
76
|
|
72
|
-
|
77
|
+
# rubocop:disable Style/EvalWithLocation
|
78
|
+
component_class.class_eval <<-RUBY, template[:path], 0
|
73
79
|
def #{method_name}
|
74
|
-
@output_buffer = ActionView::OutputBuffer.new
|
75
80
|
#{compiled_template(template[:path])}
|
76
81
|
end
|
77
82
|
RUBY
|
83
|
+
# rubocop:enable Style/EvalWithLocation
|
78
84
|
end
|
79
85
|
|
80
86
|
define_render_template_for
|
81
87
|
|
88
|
+
component_class.build_i18n_backend
|
82
89
|
component_class._after_compile
|
83
90
|
|
84
91
|
CompileCache.register(component_class)
|
85
92
|
end
|
86
93
|
end
|
87
94
|
|
88
|
-
def
|
95
|
+
def with_write_lock(&block)
|
89
96
|
if development?
|
90
|
-
__vc_compiler_lock.
|
97
|
+
__vc_compiler_lock.with_write_lock(&block)
|
91
98
|
else
|
92
99
|
block.call
|
93
100
|
end
|
94
101
|
end
|
95
102
|
|
103
|
+
def with_read_lock(&block)
|
104
|
+
__vc_compiler_lock.with_read_lock(&block)
|
105
|
+
end
|
106
|
+
|
96
107
|
private
|
97
108
|
|
98
109
|
attr_reader :component_class
|
@@ -118,7 +129,7 @@ module ViewComponent
|
|
118
129
|
if development?
|
119
130
|
component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
120
131
|
def render_template_for(variant = nil)
|
121
|
-
self.class.compiler.
|
132
|
+
self.class.compiler.with_read_lock do
|
122
133
|
#{body}
|
123
134
|
end
|
124
135
|
end
|
@@ -148,15 +159,15 @@ module ViewComponent
|
|
148
159
|
end
|
149
160
|
|
150
161
|
invalid_variants =
|
151
|
-
templates
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
162
|
+
templates
|
163
|
+
.group_by { |template| template[:variant] }
|
164
|
+
.map { |variant, grouped| variant if grouped.length > 1 }
|
165
|
+
.compact
|
166
|
+
.sort
|
156
167
|
|
157
168
|
unless invalid_variants.empty?
|
158
169
|
errors <<
|
159
|
-
"More than one template found for #{
|
170
|
+
"More than one template found for #{"variant".pluralize(invalid_variants.count)} " \
|
160
171
|
"#{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. " \
|
161
172
|
"There can only be one template file per variant."
|
162
173
|
end
|
@@ -174,8 +185,8 @@ module ViewComponent
|
|
174
185
|
count = duplicate_template_file_and_inline_variant_calls.count
|
175
186
|
|
176
187
|
errors <<
|
177
|
-
"Template #{
|
178
|
-
"found for #{
|
188
|
+
"Template #{"file".pluralize(count)} and inline render #{"method".pluralize(count)} " \
|
189
|
+
"found for #{"variant".pluralize(count)} " \
|
179
190
|
"#{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} " \
|
180
191
|
"in #{component_class}. " \
|
181
192
|
"There can only be a template file or inline render method per variant."
|
@@ -233,8 +244,9 @@ module ViewComponent
|
|
233
244
|
end
|
234
245
|
|
235
246
|
def compiled_template(file_path)
|
236
|
-
handler = ActionView::Template.handler_for_extension(File.extname(file_path).
|
247
|
+
handler = ActionView::Template.handler_for_extension(File.extname(file_path).delete("."))
|
237
248
|
template = File.read(file_path)
|
249
|
+
template.rstrip! if component_class.strip_trailing_whitespace?
|
238
250
|
|
239
251
|
if handler.method(:call).parameters.length > 1
|
240
252
|
handler.call(component_class, template)
|
@@ -256,5 +268,14 @@ module ViewComponent
|
|
256
268
|
"call"
|
257
269
|
end
|
258
270
|
end
|
271
|
+
|
272
|
+
def should_compile_superclass?
|
273
|
+
development? &&
|
274
|
+
templates.empty? &&
|
275
|
+
!(
|
276
|
+
component_class.instance_methods(false).include?(:call) ||
|
277
|
+
component_class.private_instance_methods(false).include?(:call)
|
278
|
+
)
|
279
|
+
end
|
259
280
|
end
|
260
281
|
end
|