From 6933040dc1f6690cd30bca5aab982650555c229f Mon Sep 17 00:00:00 2001 From: sneakyvv Date: Mon, 4 Sep 2023 20:17:33 +0200 Subject: [PATCH] [TwigComponent][LiveComponent] Fix DataModelPropsSubscriber for embedded components --- .../DataModelPropsSubscriber.php | 23 ++++++++++- .../Fixtures/Component/InputComponent.php | 22 +++++++++++ .../Component/ParentComponentDataModel.php | 24 ++++++++++++ .../Component/ParentComponentDataModel2.php | 27 +++++++++++++ .../components/input_component.html.twig | 1 + .../parent_component_data_model.html.twig | 2 + .../parent_component_data_model_2.html.twig | 2 + .../DataModelPropsSubscriberTest.php | 39 ++++++++++++++++--- src/TwigComponent/src/ComponentRenderer.php | 19 ++++----- src/TwigComponent/src/ComponentStack.php | 10 ++++- .../src/Twig/ComponentExtension.php | 9 +++++ src/TwigComponent/src/Twig/ComponentNode.php | 6 +++ 12 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 src/LiveComponent/tests/Fixtures/Component/InputComponent.php create mode 100644 src/LiveComponent/tests/Fixtures/Component/ParentComponentDataModel.php create mode 100644 src/LiveComponent/tests/Fixtures/Component/ParentComponentDataModel2.php create mode 100644 src/LiveComponent/tests/Fixtures/templates/components/input_component.html.twig create mode 100644 src/LiveComponent/tests/Fixtures/templates/components/parent_component_data_model.html.twig create mode 100644 src/LiveComponent/tests/Fixtures/templates/components/parent_component_data_model_2.html.twig diff --git a/src/LiveComponent/src/EventListener/DataModelPropsSubscriber.php b/src/LiveComponent/src/EventListener/DataModelPropsSubscriber.php index 59ccfdf2f5e..ef59bb09a50 100644 --- a/src/LiveComponent/src/EventListener/DataModelPropsSubscriber.php +++ b/src/LiveComponent/src/EventListener/DataModelPropsSubscriber.php @@ -13,9 +13,11 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Util\ModelBindingParser; use Symfony\UX\TwigComponent\ComponentStack; use Symfony\UX\TwigComponent\Event\PreMountEvent; +use Symfony\UX\TwigComponent\MountedComponent; /** * Parses the "data-model" key, which triggers extra props to be passed in. @@ -54,8 +56,9 @@ public function onPreMount(PreMountEvent $event): void unset($data['dataModel']); $data['data-model'] = $dataModel; - // the parent is still listed as the "current" component at this point - $parentMountedComponent = $this->componentStack->getCurrentComponent(); + // find the first parent of the component about to be rendered that is a Live Component + // only those can have properties controlled via the data-model attribute + $parentMountedComponent = $this->getCurrentLiveComponent($this->componentStack); if (null === $parentMountedComponent) { throw new \LogicException('You can only pass "data-model" when rendering a component when you\'re rendering inside of a parent component.'); } @@ -76,4 +79,20 @@ public static function getSubscribedEvents(): array PreMountEvent::class => 'onPreMount', ]; } + + private function getCurrentLiveComponent(ComponentStack $componentStack): ?MountedComponent + { + foreach ($componentStack as $mountedComponent) { + if ($this->isLiveComponent($mountedComponent->getComponent()::class)) { + return $mountedComponent; + } + } + + return null; + } + + private function isLiveComponent(string $classname): bool + { + return [] !== (new \ReflectionClass($classname))->getAttributes(AsLiveComponent::class); + } } diff --git a/src/LiveComponent/tests/Fixtures/Component/InputComponent.php b/src/LiveComponent/tests/Fixtures/Component/InputComponent.php new file mode 100644 index 00000000000..c8922842480 --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Component/InputComponent.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component; + +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; + +/** + * @author Bart Vanderstukken + */ +#[AsTwigComponent('input_component')] +final class InputComponent +{ +} diff --git a/src/LiveComponent/tests/Fixtures/Component/ParentComponentDataModel.php b/src/LiveComponent/tests/Fixtures/Component/ParentComponentDataModel.php new file mode 100644 index 00000000000..ac815ccfb66 --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Component/ParentComponentDataModel.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component; + +use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\DefaultActionTrait; + +/** + * @author Bart Vanderstukken + */ +#[AsLiveComponent('parent_component_data_model')] +final class ParentComponentDataModel +{ + use DefaultActionTrait; +} diff --git a/src/LiveComponent/tests/Fixtures/Component/ParentComponentDataModel2.php b/src/LiveComponent/tests/Fixtures/Component/ParentComponentDataModel2.php new file mode 100644 index 00000000000..4f40e356a65 --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Component/ParentComponentDataModel2.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component; + +use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\Attribute\LiveProp; +use Symfony\UX\LiveComponent\DefaultActionTrait; + +/** + * @author Bart Vanderstukken + */ +#[AsLiveComponent('parent_component_data_model_2')] +final class ParentComponentDataModel2 +{ + use DefaultActionTrait; + + #[LiveProp(writable: true)] public string $content; +} diff --git a/src/LiveComponent/tests/Fixtures/templates/components/input_component.html.twig b/src/LiveComponent/tests/Fixtures/templates/components/input_component.html.twig new file mode 100644 index 00000000000..4368bc1ceaa --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/templates/components/input_component.html.twig @@ -0,0 +1 @@ + diff --git a/src/LiveComponent/tests/Fixtures/templates/components/parent_component_data_model.html.twig b/src/LiveComponent/tests/Fixtures/templates/components/parent_component_data_model.html.twig new file mode 100644 index 00000000000..a5fdd1aeca3 --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/templates/components/parent_component_data_model.html.twig @@ -0,0 +1,2 @@ +{% component parent_component_data_model_2 with { content: 'default content on mount' } %} +{% endcomponent %} diff --git a/src/LiveComponent/tests/Fixtures/templates/components/parent_component_data_model_2.html.twig b/src/LiveComponent/tests/Fixtures/templates/components/parent_component_data_model_2.html.twig new file mode 100644 index 00000000000..f7f140ea2ae --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/templates/components/parent_component_data_model_2.html.twig @@ -0,0 +1,2 @@ +{{ component('textarea_component', { dataModel: 'content' }) }} +{% component input_component with { dataModel: 'content' } %}{% endcomponent %} diff --git a/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php b/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php index 2674cea2818..254e09dc32f 100644 --- a/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php +++ b/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php @@ -13,18 +13,16 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpFoundation\Request; +use Symfony\UX\LiveComponent\Tests\LiveComponentTestHelper; use Symfony\UX\TwigComponent\ComponentRenderer; final class DataModelPropsSubscriberTest extends KernelTestCase { + use LiveComponentTestHelper; + public function testDataModelPropsAreSharedToChild(): void { - // work around so that a session is available so CSRF doesn't fail - $session = self::getContainer()->get('session.factory')->createSession(); - $request = Request::create('/'); - $request->setSession($session); - $requestStack = self::getContainer()->get('request_stack'); - $requestStack->push($request); + $this->fakeSession(); /** @var ComponentRenderer $renderer */ $renderer = self::getContainer()->get('ux.twig_component.component_renderer'); @@ -42,4 +40,33 @@ public function testDataModelPropsAreSharedToChild(): void $this->assertStringContainsString('', $html); $this->assertStringContainsString('', $html); } + + public function testDataModelPropsAreAvailableInEmbeddedComponents(): void + { + $this->fakeSession(); + + $templateName = 'components/parent_component_data_model.html.twig'; + $obscuredName = '684c45bf85d3461dbe587407892e59d8'; + $this->addTemplateMap($obscuredName, $templateName); + + /** @var ComponentRenderer $renderer */ + $renderer = self::getContainer()->get('ux.twig_component.component_renderer'); + + $html = $renderer->createAndRender('parent_component_data_model', [ + 'attributes' => ['data-live-id' => 'dummy-live-id'], + ]); + + $this->assertStringContainsString('', $html); + $this->assertStringContainsString('', $html); + } + + private function fakeSession(): void + { + // work around so that a session is available so CSRF doesn't fail + $session = self::getContainer()->get('session.factory')->createSession(); + $request = Request::create('/'); + $request->setSession($session); + $requestStack = self::getContainer()->get('request_stack'); + $requestStack->push($request); + } } diff --git a/src/TwigComponent/src/ComponentRenderer.php b/src/TwigComponent/src/ComponentRenderer.php index 38bc8f6e080..bd7ba4cdd14 100644 --- a/src/TwigComponent/src/ComponentRenderer.php +++ b/src/TwigComponent/src/ComponentRenderer.php @@ -93,17 +93,18 @@ public function embeddedContext(string $name, array $props, array $context, stri $this->componentStack->push($mounted); - try { - $embeddedContext = $this->preRender($mounted, $context)->getVariables(); - - if (!isset($embeddedContext['outerBlocks'])) { - $embeddedContext['outerBlocks'] = new BlockStack(); - } + $embeddedContext = $this->preRender($mounted, $context)->getVariables(); - return $embeddedContext; - } finally { - $this->componentStack->pop(); + if (!isset($embeddedContext['outerBlocks'])) { + $embeddedContext['outerBlocks'] = new BlockStack(); } + + return $embeddedContext; + } + + public function finishEmbeddedComponentRender(): void + { + $this->componentStack->pop(); } private function preRender(MountedComponent $mounted, array $context = []): PreRenderEvent diff --git a/src/TwigComponent/src/ComponentStack.php b/src/TwigComponent/src/ComponentStack.php index 4815ccbee82..aba2919b34b 100644 --- a/src/TwigComponent/src/ComponentStack.php +++ b/src/TwigComponent/src/ComponentStack.php @@ -16,7 +16,7 @@ * * @internal */ -class ComponentStack +class ComponentStack implements \IteratorAggregate { /** * @var MountedComponent[] @@ -60,4 +60,12 @@ public function hasParentComponent(): bool { return (bool) $this->getParentComponent(); } + + /** + * @return MountedComponent[]|\ArrayIterator + */ + public function getIterator(): \Traversable + { + return new \ArrayIterator(array_reverse($this->components)); + } } diff --git a/src/TwigComponent/src/Twig/ComponentExtension.php b/src/TwigComponent/src/Twig/ComponentExtension.php index b2560796a47..9de3cf665f8 100644 --- a/src/TwigComponent/src/Twig/ComponentExtension.php +++ b/src/TwigComponent/src/Twig/ComponentExtension.php @@ -80,6 +80,15 @@ public function embeddedContext(string $name, array $props, array $context, stri } } + public function finishEmbeddedComponentRender(): void + { + try { + $this->container->get(ComponentRenderer::class)->finishEmbeddedComponentRender(); + } catch (\Throwable $e) { + $this->throwRuntimeError($name, $e); + } + } + private function throwRuntimeError(string $name, \Throwable $e): void { if (!($e instanceof \Exception)) { diff --git a/src/TwigComponent/src/Twig/ComponentNode.php b/src/TwigComponent/src/Twig/ComponentNode.php index 98d5aeff796..b356de8138a 100644 --- a/src/TwigComponent/src/Twig/ComponentNode.php +++ b/src/TwigComponent/src/Twig/ComponentNode.php @@ -85,6 +85,12 @@ public function compile(Compiler $compiler): void $compiler->raw('->display($embeddedContext, $embeddedBlocks);'); $compiler->raw("\n"); + $compiler->write('$this->extensions[') + ->string(ComponentExtension::class) + ->raw(']->finishEmbeddedComponentRender()') + ->raw(";\n") + ; + $compiler ->outdent() ->write('}')