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.

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