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.

Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/app/assets/vendor/prism.css +3 -195
  4. data/app/assets/vendor/prism.min.js +11 -11
  5. data/app/controllers/concerns/view_component/preview_actions.rb +97 -0
  6. data/app/controllers/view_components_controller.rb +1 -87
  7. data/app/helpers/preview_helper.rb +5 -5
  8. data/app/views/view_components/preview.html.erb +2 -2
  9. data/docs/CHANGELOG.md +427 -1
  10. data/lib/rails/generators/abstract_generator.rb +7 -9
  11. data/lib/rails/generators/component/component_generator.rb +5 -4
  12. data/lib/rails/generators/locale/component_generator.rb +1 -1
  13. data/lib/rails/generators/preview/component_generator.rb +1 -1
  14. data/lib/view_component/base.rb +152 -51
  15. data/lib/view_component/collection.rb +9 -2
  16. data/lib/view_component/compiler.rb +39 -18
  17. data/lib/view_component/config.rb +159 -0
  18. data/lib/view_component/content_areas.rb +1 -1
  19. data/lib/view_component/docs_builder_component.rb +1 -1
  20. data/lib/view_component/engine.rb +16 -30
  21. data/lib/view_component/polymorphic_slots.rb +28 -1
  22. data/lib/view_component/preview.rb +12 -9
  23. data/lib/view_component/render_component_helper.rb +1 -0
  24. data/lib/view_component/render_component_to_string_helper.rb +1 -1
  25. data/lib/view_component/render_to_string_monkey_patch.rb +1 -1
  26. data/lib/view_component/rendering_component_helper.rb +1 -1
  27. data/lib/view_component/rendering_monkey_patch.rb +1 -1
  28. data/lib/view_component/slot_v2.rb +4 -10
  29. data/lib/view_component/slotable.rb +5 -6
  30. data/lib/view_component/slotable_v2.rb +69 -21
  31. data/lib/view_component/test_helpers.rb +80 -8
  32. data/lib/view_component/translatable.rb +13 -14
  33. data/lib/view_component/version.rb +1 -1
  34. data/lib/view_component.rb +1 -0
  35. metadata +47 -18
  36. data/lib/view_component/previewable.rb +0 -62
@@ -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/previewable"
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
- include ActiveSupport::Configurable
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::Previewable
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.new(
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(*); end
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.is_a? ViewComponent::Base
157
- options.__vc_original_view_context = __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
- # config.view_component.test_controller = "MyTestController"
321
+ # ```ruby
322
+ # config.view_component.test_controller = "MyTestController"
323
+ # ```
275
324
  #
276
- # Defaults to ApplicationController. Can also be configured on a per-test
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
- # config.view_component.render_monkey_patch_enabled = false
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
- # config.view_component.view_component_path = "app/my_components"
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 `app/components`.
353
+ # Defaults to `false`
293
354
  #
294
- mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
355
+ mattr_accessor :use_consistent_rendering_lifecycle, instance_writer: false, default: false
295
356
 
296
357
  # Parent class for generated components
297
358
  #
298
- # config.view_component.component_parent_class = "MyBaseComponent"
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
- # config.view_component.generate.sidecar = true
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
- # config.view_component.generate.stimulus_controller = true
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
- # config.view_component.generate.locale = true
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
- # config.view_component.generate.distinct_locale_files = true
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
- # config.view_component.generate.preview = true
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
- # render(ProductsComponent.with_collection(@products, foo: :bar))
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 unless child < 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
- %r{(.*#{Regexp.quote(ViewComponent::Base.view_component_path)})|(\.rb)}, ""
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
- # with_collection_parameter :item
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.new(
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.new(
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.new(
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
- if provided_collection_parameter
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 # rubocop:disable Rails/OutputSafety
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 = { component.collection_parameter => item }
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 = Monitor.new
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
- with_lock do
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
- component_class.class_eval <<-RUBY, template[:path], -1
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 with_lock(&block)
95
+ def with_write_lock(&block)
89
96
  if development?
90
- __vc_compiler_lock.synchronize(&block)
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.with_lock do
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
- group_by { |template| template[:variant] }.
153
- map { |variant, grouped| variant if grouped.length > 1 }.
154
- compact.
155
- sort
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 #{'variant'.pluralize(invalid_variants.count)} " \
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 #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} " \
178
- "found for #{'variant'.pluralize(count)} " \
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).gsub(".", ""))
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