diff --git a/src/TwigComponent/CHANGELOG.md b/src/TwigComponent/CHANGELOG.md
index a4578a1c545..c2065acd61a 100644
--- a/src/TwigComponent/CHANGELOG.md
+++ b/src/TwigComponent/CHANGELOG.md
@@ -4,6 +4,7 @@
- Add option `profiler.collect_components` to control component data collection
in the profiler (enabled in debug mode by default)
+- Add support for using directory name as component name for anonymous components
## 2.30
diff --git a/src/TwigComponent/doc/index.rst b/src/TwigComponent/doc/index.rst
index 6cae99346a3..ed4d37bf2c3 100644
--- a/src/TwigComponent/doc/index.rst
+++ b/src/TwigComponent/doc/index.rst
@@ -676,6 +676,43 @@ the subdirectory:
{# renders as: #}
Click Me!
+If your anonymous component lives in a directory with the same name, you can
+name the component file ``index.html.twig`` to avoid repetition:
+
+.. code-block:: html+twig
+
+ {# templates/components/Menu/index.html.twig #}
+
+
+ {% block content %}{% endblock %}
+
+
+
+.. code-block:: html+twig
+
+ {# templates/components/Menu/Item.html.twig #}
+ {% props href, label %}
+
+ {% block content %}{{ label }}{% endblock %}
+
+
+.. code-block:: html+twig
+
+ {# index.html.twig #}
+ ...
+
+ Home
+ About
+
+
+ {# renders as: #}
+
+
Like normal, you can pass extra attributes that will be rendered on the element:
.. code-block:: html+twig
diff --git a/src/TwigComponent/src/ComponentTemplateFinder.php b/src/TwigComponent/src/ComponentTemplateFinder.php
index 0ceb583e1f3..df63f4b7377 100644
--- a/src/TwigComponent/src/ComponentTemplateFinder.php
+++ b/src/TwigComponent/src/ComponentTemplateFinder.php
@@ -66,6 +66,11 @@ public function findAnonymousComponentTemplate(string $name): ?string
return $template;
}
+ $template = rtrim($this->directory, '/').'/'.$componentPath.'/index.html.twig';
+ if ($loader->exists($template)) {
+ return $template;
+ }
+
$parts = explode('/', $componentPath, 2);
if (\count($parts) < 2) {
return null;
diff --git a/src/TwigComponent/tests/Fixtures/templates/anonymous/Bar.html.twig b/src/TwigComponent/tests/Fixtures/templates/anonymous/Bar.html.twig
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/TwigComponent/tests/Fixtures/templates/anonymous/Bar/index.html.twig b/src/TwigComponent/tests/Fixtures/templates/anonymous/Bar/index.html.twig
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/TwigComponent/tests/Fixtures/templates/anonymous/Menu/Item.html.twig b/src/TwigComponent/tests/Fixtures/templates/anonymous/Menu/Item.html.twig
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/TwigComponent/tests/Fixtures/templates/anonymous/Menu/index.html.twig b/src/TwigComponent/tests/Fixtures/templates/anonymous/Menu/index.html.twig
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/TwigComponent/tests/Integration/ComponentFactoryTest.php b/src/TwigComponent/tests/Integration/ComponentFactoryTest.php
index c2189c955f4..ab4639ad0de 100644
--- a/src/TwigComponent/tests/Integration/ComponentFactoryTest.php
+++ b/src/TwigComponent/tests/Integration/ComponentFactoryTest.php
@@ -178,6 +178,29 @@ public function testLoadingAnonymousComponentFromBundle()
$this->assertNull($metadata->get('class'));
}
+ public function testLoadingAnonymousComponentWithFallback()
+ {
+ self::bootKernel(['environment' => 'anonymous_directory']);
+
+ $metadata = $this->factory()->metadataFor('Menu');
+
+ $this->assertSame('anonymous/Menu/index.html.twig', $metadata->getTemplate());
+ $this->assertSame('Menu', $metadata->getName());
+ $this->assertNull($metadata->get('class'));
+
+ $metadata = $this->factory()->metadataFor('Menu:Item');
+
+ $this->assertSame('anonymous/Menu/Item.html.twig', $metadata->getTemplate());
+ $this->assertSame('Menu:Item', $metadata->getName());
+ $this->assertNull($metadata->get('class'));
+
+ // Ensure anonymous/Bar.html.twig takes precedence over anonymous/Bar/index.html.twig
+ $metadata = $this->factory()->metadataFor('Bar');
+ $this->assertSame('anonymous/Bar.html.twig', $metadata->getTemplate());
+ $this->assertSame('Bar', $metadata->getName());
+ $this->assertNull($metadata->get('class'));
+ }
+
public function testAutoNamingInSubDirectory()
{
$metadata = $this->factory()->metadataFor('SubDirectory:ComponentInSubDirectory');
diff --git a/src/TwigComponent/tests/Unit/ComponentTemplateFinderTest.php b/src/TwigComponent/tests/Unit/ComponentTemplateFinderTest.php
index 66a19bd0646..685a0db575f 100644
--- a/src/TwigComponent/tests/Unit/ComponentTemplateFinderTest.php
+++ b/src/TwigComponent/tests/Unit/ComponentTemplateFinderTest.php
@@ -117,6 +117,8 @@ public function testFindTemplateWithinDirectory()
'foo/bar.html.twig',
'bar/foo/bar.html.twig',
'foo/foo/bar.html.twig',
+ 'foo/qux/index.html.twig',
+ 'foo/foo/baz/index.html.twig',
];
$loader = $this->createLoader($templates);
$finder = new ComponentTemplateFinder($loader, 'foo');
@@ -124,6 +126,9 @@ public function testFindTemplateWithinDirectory()
$this->assertEquals('foo/bar.html.twig', $finder->findAnonymousComponentTemplate('bar'));
$this->assertEquals('foo/foo/bar.html.twig', $finder->findAnonymousComponentTemplate('foo:bar'));
$this->assertEquals('foo/foo/bar.html.twig', $finder->findAnonymousComponentTemplate('foo:bar'));
+
+ $this->assertEquals('foo/qux/index.html.twig', $finder->findAnonymousComponentTemplate('qux'));
+ $this->assertEquals('foo/foo/baz/index.html.twig', $finder->findAnonymousComponentTemplate('foo:baz'));
}
private function createLoader(array $templates): LoaderInterface