Skip to content

Commit

Permalink
[TwigComponent][LiveComponent] Fix DataModelPropsSubscriber for embed…
Browse files Browse the repository at this point in the history
…ded components
  • Loading branch information
sneakyvv authored and weaverryan committed Sep 5, 2023
1 parent 9265573 commit 6933040
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 18 deletions.
23 changes: 21 additions & 2 deletions src/LiveComponent/src/EventListener/DataModelPropsSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.');
}
Expand All @@ -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);
}
}
22 changes: 22 additions & 0 deletions src/LiveComponent/tests/Fixtures/Component/InputComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bart.vanderstukken@gmail.com>
*/
#[AsTwigComponent('input_component')]
final class InputComponent
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bart.vanderstukken@gmail.com>
*/
#[AsLiveComponent('parent_component_data_model')]
final class ParentComponentDataModel
{
use DefaultActionTrait;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bart.vanderstukken@gmail.com>
*/
#[AsLiveComponent('parent_component_data_model_2')]
final class ParentComponentDataModel2
{
use DefaultActionTrait;

#[LiveProp(writable: true)] public string $content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input{{ attributes }} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% component parent_component_data_model_2 with { content: 'default content on mount' } %}
{% endcomponent %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{ component('textarea_component', { dataModel: 'content' }) }}
{% component input_component with { dataModel: 'content' } %}{% endcomponent %}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -42,4 +40,33 @@ public function testDataModelPropsAreSharedToChild(): void
$this->assertStringContainsString('<textarea data-model="content:value">Hello data-model!</textarea>', $html);
$this->assertStringContainsString('<textarea data-model="content2:value">Value for second child</textarea>', $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('<textarea data-model="content">default content on mount</textarea>', $html);
$this->assertStringContainsString('<input data-model="content" value="default content on mount" />', $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);
}
}
19 changes: 10 additions & 9 deletions src/TwigComponent/src/ComponentRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion src/TwigComponent/src/ComponentStack.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*
* @internal
*/
class ComponentStack
class ComponentStack implements \IteratorAggregate
{
/**
* @var MountedComponent[]
Expand Down Expand Up @@ -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));
}
}
9 changes: 9 additions & 0 deletions src/TwigComponent/src/Twig/ComponentExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
6 changes: 6 additions & 0 deletions src/TwigComponent/src/Twig/ComponentNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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('}')
Expand Down

0 comments on commit 6933040

Please sign in to comment.