view_component 2.50.0 → 2.69.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.
- 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
|