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); + } +}