diff --git a/src/Core/Component/ComponentRenderer.php b/src/Core/Component/ComponentRenderer.php
index fe4399ec7..a853d93f5 100644
--- a/src/Core/Component/ComponentRenderer.php
+++ b/src/Core/Component/ComponentRenderer.php
@@ -10,7 +10,6 @@
namespace TYPO3Fluid\Fluid\Core\Component;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
-use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperVariableContainer;
use TYPO3Fluid\Fluid\View\TemplateView;
use TYPO3Fluid\Fluid\ViewHelpers\SlotViewHelper;
@@ -36,14 +35,29 @@ public function renderComponent(string $viewHelperName, array $arguments, array
$renderingContext->setViewHelperResolver($renderingContext->getViewHelperResolver()->getScopedCopy());
$renderingContext->setVariableProvider($renderingContext->getVariableProvider()->getScopeCopy($arguments));
- // Provide slots to SlotViewHelper
- $renderingContext->setViewHelperVariableContainer(new ViewHelperVariableContainer());
- $renderingContext->getViewHelperVariableContainer()->addAll(SlotViewHelper::class, $slots);
+ // Provide slots to SlotViewHelper while preserving the parent ViewHelperVariableContainer
+ // so that context set by outer ViewHelpers (e.g. TYPO3's FormViewHelper) remains available
+ // inside components. Slot state is saved and restored to support proper component nesting.
+ $parentVhvc = $parentRenderingContext->getViewHelperVariableContainer();
+ $previousSlots = $parentVhvc->getAll(SlotViewHelper::class);
+ foreach ($slots as $slotName => $slotClosure) {
+ $parentVhvc->addOrUpdate(SlotViewHelper::class, $slotName, $slotClosure);
+ }
+ $renderingContext->setViewHelperVariableContainer($parentVhvc);
// Create Fluid view for component
// render() call includes validation of provided arguments
$view = new TemplateView($renderingContext);
$view->assignMultiple($this->componentResolver->getAdditionalVariables($viewHelperName));
- return (string)$view->render($this->componentResolver->resolveTemplateName($viewHelperName));
+ $result = (string)$view->render($this->componentResolver->resolveTemplateName($viewHelperName));
+
+ // Restore previous slot state so that outer components can still access their own slots.
+ foreach (array_keys($slots) as $slotName) {
+ $parentVhvc->remove(SlotViewHelper::class, $slotName);
+ if (array_key_exists($slotName, $previousSlots)) {
+ $parentVhvc->addOrUpdate(SlotViewHelper::class, $slotName, $previousSlots[$slotName]);
+ }
+ }
+ return $result;
}
}
diff --git a/tests/Functional/Core/Component/ComponentViewHelperVariableContainerTest.php b/tests/Functional/Core/Component/ComponentViewHelperVariableContainerTest.php
new file mode 100644
index 000000000..08f476c23
--- /dev/null
+++ b/tests/Functional/Core/Component/ComponentViewHelperVariableContainerTest.php
@@ -0,0 +1,73 @@
+getRenderingContext()->setCache(self::$cache);
+ $view->getRenderingContext()->getViewHelperResolver()->addNamespace('my', 'TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ComponentCollections\BasicComponentCollection');
+ $view->getRenderingContext()->getViewHelperResolver()->addNamespace('formComponent', 'TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ComponentCollections\FormComponentCollection');
+ $view->getRenderingContext()->getViewHelperResolver()->addNamespace('form', 'TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers\MockForm');
+ $view->getRenderingContext()->getTemplatePaths()->setTemplateSource($source);
+ return $view->render();
+ }
+
+ #[Test]
+ public function outerViewHelperContextIsAvailableInsideComponent(): void
+ {
+ $source = '';
+ $expected = '' . "\n" . '|fieldNames:field1';
+
+ self::assertSame($expected, $this->renderTemplate($source), 'uncached');
+ self::assertSame($expected, $this->renderTemplate($source), 'cached');
+ }
+
+ #[Test]
+ public function fieldViewHelperWithoutOuterContextRendersWithoutIt(): void
+ {
+ $source = '';
+ $expected = '' . "\n";
+
+ self::assertSame($expected, $this->renderTemplate($source), 'uncached');
+ self::assertSame($expected, $this->renderTemplate($source), 'cached');
+ }
+
+ #[Test]
+ public function multipleComponentsInsideOuterContextAllRegisterThemselves(): void
+ {
+ $source = '';
+ $expected = '' . "\n" . '' . "\n" . '|fieldNames:field1,field2';
+
+ self::assertSame($expected, $this->renderTemplate($source), 'uncached');
+ self::assertSame($expected, $this->renderTemplate($source), 'cached');
+ }
+
+ #[Test]
+ public function nestedComponentSlotsRemainIsolatedWhileSharingOuterContext(): void
+ {
+ $source = '';
+ $expected = '|' . "\n" . '|||' . "\n" . '|fieldNames:field1';
+
+ self::assertSame($expected, $this->renderTemplate($source), 'uncached');
+ self::assertSame($expected, $this->renderTemplate($source), 'cached');
+ }
+}
diff --git a/tests/Functional/Fixtures/ComponentCollections/FormComponentCollection.php b/tests/Functional/Fixtures/ComponentCollections/FormComponentCollection.php
new file mode 100644
index 000000000..6af747e55
--- /dev/null
+++ b/tests/Functional/Fixtures/ComponentCollections/FormComponentCollection.php
@@ -0,0 +1,25 @@
+setTemplateRootPaths([
+ __DIR__ . '/FormComponents/',
+ ]);
+ return $templatePaths;
+ }
+}
diff --git a/tests/Functional/Fixtures/ComponentCollections/FormComponents/FormFieldComponent/FormFieldComponent.html b/tests/Functional/Fixtures/ComponentCollections/FormComponents/FormFieldComponent/FormFieldComponent.html
new file mode 100644
index 000000000..70b4e591a
--- /dev/null
+++ b/tests/Functional/Fixtures/ComponentCollections/FormComponents/FormFieldComponent/FormFieldComponent.html
@@ -0,0 +1 @@
+
diff --git a/tests/Functional/Fixtures/ViewHelpers/MockForm/ContextViewHelper.php b/tests/Functional/Fixtures/ViewHelpers/MockForm/ContextViewHelper.php
new file mode 100644
index 000000000..7e61a1e60
--- /dev/null
+++ b/tests/Functional/Fixtures/ViewHelpers/MockForm/ContextViewHelper.php
@@ -0,0 +1,35 @@
+renderingContext->getViewHelperVariableContainer();
+ $variableContainer->addOrUpdate(self::class, 'fieldNames', []);
+
+ $content = $this->renderChildren();
+
+ $fieldNames = $variableContainer->get(self::class, 'fieldNames', []);
+ $variableContainer->remove(self::class, 'fieldNames');
+
+ return $content . '|fieldNames:' . implode(',', $fieldNames);
+ }
+}
diff --git a/tests/Functional/Fixtures/ViewHelpers/MockForm/FieldViewHelper.php b/tests/Functional/Fixtures/ViewHelpers/MockForm/FieldViewHelper.php
new file mode 100644
index 000000000..65b8ceb08
--- /dev/null
+++ b/tests/Functional/Fixtures/ViewHelpers/MockForm/FieldViewHelper.php
@@ -0,0 +1,43 @@
+registerArgument('name', 'string', 'Field name', true);
+ }
+
+ public function render(): string
+ {
+ $name = $this->arguments['name'];
+ $variableContainer = $this->renderingContext->getViewHelperVariableContainer();
+
+ if (!$variableContainer->exists(ContextViewHelper::class, 'fieldNames')) {
+ return sprintf('', $name);
+ }
+
+ $fieldNames = $variableContainer->get(ContextViewHelper::class, 'fieldNames');
+ $fieldNames[] = $name;
+ $variableContainer->addOrUpdate(ContextViewHelper::class, 'fieldNames', $fieldNames);
+
+ return sprintf('', $name);
+ }
+}