diff --git a/.erb_lint.yml b/.erb_lint.yml
index 530f46bdc..6e01701b8 100644
--- a/.erb_lint.yml
+++ b/.erb_lint.yml
@@ -2,6 +2,7 @@
EnableDefaultLinters: true
exclude:
- '**/vendor/**/*'
+ - 'test/sandbox/app/components/content_security_policy_nonce_component.html.erb'
linters:
ErbSafety:
enabled: true
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 38e8dd8c6..c917653d3 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -10,6 +10,10 @@ nav_order: 6
## main
+* Fix bug where inheritance of components with formatless templates improperly raised a NoMethodError.
+
+ *GitHub Copilot*, *Joel Hawksley*, *Cameron Dutro*
+
## 4.6.0
* Add `view_identifier` to the `render.view_component` instrumentation event payload, containing the path to the component's template file (e.g. `app/components/my_component.html.erb`). For components using inline render methods, `view_identifier` will be `nil`.
@@ -22,7 +26,7 @@ nav_order: 6
* Return `html_safe` empty string from `render_in` when `render?` is false.
- *Copilot*
+ *GitHub Copilot*
## 4.5.0
diff --git a/lib/view_component/template.rb b/lib/view_component/template.rb
index 23ca0c5ee..8cc330075 100644
--- a/lib/view_component/template.rb
+++ b/lib/view_component/template.rb
@@ -21,6 +21,13 @@ def initialize(component:, details:, lineno: nil, path: nil)
class File < Template
def initialize(component:, details:, path:)
+ # If the template file has no format (e.g. .erb instead of .html.erb),
+ # assume the default format (html).
+ if details.format.nil?
+ Kernel.warn("WARNING: Template format for #{path} is missing, defaulting to :html.")
+ details = ActionView::TemplateDetails.new(details.locale, details.handler, DEFAULT_FORMAT, details.variant)
+ end
+
@strip_annotation_line = false
# Rails 8.1 added a newline to compiled ERB output (rails/rails#53731).
diff --git a/test/sandbox/app/components/content_security_policy_nonce_component.erb b/test/sandbox/app/components/content_security_policy_nonce_component.html.erb
similarity index 100%
rename from test/sandbox/app/components/content_security_policy_nonce_component.erb
rename to test/sandbox/app/components/content_security_policy_nonce_component.html.erb
diff --git a/test/sandbox/app/components/format_less_child_component.erb b/test/sandbox/app/components/format_less_child_component.erb
new file mode 100644
index 000000000..3b6bbb6e7
--- /dev/null
+++ b/test/sandbox/app/components/format_less_child_component.erb
@@ -0,0 +1 @@
+
<%= content %>
diff --git a/test/sandbox/app/components/format_less_child_component.rb b/test/sandbox/app/components/format_less_child_component.rb
new file mode 100644
index 000000000..6e86972eb
--- /dev/null
+++ b/test/sandbox/app/components/format_less_child_component.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class FormatLessChildComponent < FormatLessParentComponent
+end
diff --git a/test/sandbox/app/components/format_less_parent_component.erb b/test/sandbox/app/components/format_less_parent_component.erb
new file mode 100644
index 000000000..34bdb107d
--- /dev/null
+++ b/test/sandbox/app/components/format_less_parent_component.erb
@@ -0,0 +1 @@
+<%= content %>
diff --git a/test/sandbox/app/components/format_less_parent_component.rb b/test/sandbox/app/components/format_less_parent_component.rb
new file mode 100644
index 000000000..7ec22277a
--- /dev/null
+++ b/test/sandbox/app/components/format_less_parent_component.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class FormatLessParentComponent < ViewComponent::Base
+end
diff --git a/test/sandbox/app/components/invalid_named_parameters_component.erb b/test/sandbox/app/components/invalid_named_parameters_component.html.erb
similarity index 100%
rename from test/sandbox/app/components/invalid_named_parameters_component.erb
rename to test/sandbox/app/components/invalid_named_parameters_component.html.erb
diff --git a/test/sandbox/app/components/invalid_parameters_component.erb b/test/sandbox/app/components/invalid_parameters_component.html.erb
similarity index 100%
rename from test/sandbox/app/components/invalid_parameters_component.erb
rename to test/sandbox/app/components/invalid_parameters_component.html.erb
diff --git a/test/sandbox/test/rendering_test.rb b/test/sandbox/test/rendering_test.rb
index b394bba4d..ee98488ae 100644
--- a/test/sandbox/test/rendering_test.rb
+++ b/test/sandbox/test/rendering_test.rb
@@ -852,6 +852,18 @@ def test_inherited_component_overrides_inherits_template
assert_selector("div", text: "hello, my own template")
end
+ def test_inherited_component_with_format_less_template
+ # Reproduces https://github.com/ViewComponent/view_component/issues/2573
+ # When a parent component uses a format-less template (e.g. .erb or .slim
+ # instead of .html.erb or .html.slim), the child component crashes with
+ # "undefined method 'upcase' for nil" during compilation.
+ render_inline(FormatLessChildComponent.new) do
+ "Hello World"
+ end
+
+ assert_selector("div.child", text: "Hello World")
+ end
+
def test_inherited_inline_component_inherits_inline_method
render_inline(InlineInheritedComponent.new)
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 13353dbd6..f6ddbd2e5 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -14,6 +14,7 @@ module Warning
def self.warn(message)
called_by = caller_locations(1, 1).first.path
return super unless called_by&.start_with?(PROJECT_ROOT) && !called_by.start_with?("#{PROJECT_ROOT}/vendor")
+ return super if message.include?("Template format for")
raise "Warning: #{message}"
end