Skip to content

Commit d8ac2e1

Browse files
committed
[TwigComponent][LiveComponent] Fix DataModelPropsSubscriber for embedded components
1 parent 9265573 commit d8ac2e1

File tree

10 files changed

+136
-15
lines changed

10 files changed

+136
-15
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component;
13+
14+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
15+
16+
/**
17+
* @author Bart Vanderstukken <bart.vanderstukken@gmail.com>
18+
*/
19+
#[AsTwigComponent('input_component')]
20+
final class InputComponent
21+
{
22+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component;
13+
14+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
15+
use Symfony\UX\LiveComponent\DefaultActionTrait;
16+
17+
/**
18+
* @author Bart Vanderstukken <bart.vanderstukken@gmail.com>
19+
*/
20+
#[AsLiveComponent('parent_component_data_model')]
21+
final class ParentComponentDataModel
22+
{
23+
use DefaultActionTrait;
24+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component;
13+
14+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
15+
use Symfony\UX\LiveComponent\Attribute\LiveProp;
16+
use Symfony\UX\LiveComponent\DefaultActionTrait;
17+
18+
/**
19+
* @author Bart Vanderstukken <bart.vanderstukken@gmail.com>
20+
*/
21+
#[AsLiveComponent('parent_component_data_model_2')]
22+
final class ParentComponentDataModel2
23+
{
24+
use DefaultActionTrait;
25+
26+
#[LiveProp(writable: true)] public string $content;
27+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<input{{ attributes }} />
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{% component parent_component_data_model_2 with { content: 'default content on mount' } %}
2+
{% endcomponent %}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{{ component('textarea_component', { dataModel: 'content' }) }}
2+
{% component input_component with { dataModel: 'content' } %}{% endcomponent %}

src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,16 @@
1313

1414
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
1515
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\UX\LiveComponent\Tests\LiveComponentTestHelper;
1617
use Symfony\UX\TwigComponent\ComponentRenderer;
1718

1819
final class DataModelPropsSubscriberTest extends KernelTestCase
1920
{
21+
use LiveComponentTestHelper;
22+
2023
public function testDataModelPropsAreSharedToChild(): void
2124
{
22-
// work around so that a session is available so CSRF doesn't fail
23-
$session = self::getContainer()->get('session.factory')->createSession();
24-
$request = Request::create('/');
25-
$request->setSession($session);
26-
$requestStack = self::getContainer()->get('request_stack');
27-
$requestStack->push($request);
25+
$this->fakeSession();
2826

2927
/** @var ComponentRenderer $renderer */
3028
$renderer = self::getContainer()->get('ux.twig_component.component_renderer');
@@ -42,4 +40,33 @@ public function testDataModelPropsAreSharedToChild(): void
4240
$this->assertStringContainsString('<textarea data-model="content:value">Hello data-model!</textarea>', $html);
4341
$this->assertStringContainsString('<textarea data-model="content2:value">Value for second child</textarea>', $html);
4442
}
43+
44+
public function testDataModelPropsAreAvailableInEmbeddedComponents(): void
45+
{
46+
$this->fakeSession();
47+
48+
$templateName = 'components/parent_component_data_model.html.twig';
49+
$obscuredName = '684c45bf85d3461dbe587407892e59d8';
50+
$this->addTemplateMap($obscuredName, $templateName);
51+
52+
/** @var ComponentRenderer $renderer */
53+
$renderer = self::getContainer()->get('ux.twig_component.component_renderer');
54+
55+
$html = $renderer->createAndRender('parent_component_data_model', [
56+
'attributes' => ['data-live-id' => 'dummy-live-id'],
57+
]);
58+
59+
$this->assertStringContainsString('<textarea data-model="content">default content on mount</textarea>', $html);
60+
$this->assertStringContainsString('<input data-model="content" value="default content on mount" />', $html);
61+
}
62+
63+
private function fakeSession(): void
64+
{
65+
// work around so that a session is available so CSRF doesn't fail
66+
$session = self::getContainer()->get('session.factory')->createSession();
67+
$request = Request::create('/');
68+
$request->setSession($session);
69+
$requestStack = self::getContainer()->get('request_stack');
70+
$requestStack->push($request);
71+
}
4572
}

src/TwigComponent/src/ComponentRenderer.php

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,18 @@ public function embeddedContext(string $name, array $props, array $context, stri
9393

9494
$this->componentStack->push($mounted);
9595

96-
try {
97-
$embeddedContext = $this->preRender($mounted, $context)->getVariables();
98-
99-
if (!isset($embeddedContext['outerBlocks'])) {
100-
$embeddedContext['outerBlocks'] = new BlockStack();
101-
}
96+
$embeddedContext = $this->preRender($mounted, $context)->getVariables();
10297

103-
return $embeddedContext;
104-
} finally {
105-
$this->componentStack->pop();
98+
if (!isset($embeddedContext['outerBlocks'])) {
99+
$embeddedContext['outerBlocks'] = new BlockStack();
106100
}
101+
102+
return $embeddedContext;
103+
}
104+
105+
public function finishEmbeddedComponentRender(): void
106+
{
107+
$this->componentStack->pop();
107108
}
108109

109110
private function preRender(MountedComponent $mounted, array $context = []): PreRenderEvent

src/TwigComponent/src/Twig/ComponentExtension.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ public function embeddedContext(string $name, array $props, array $context, stri
8080
}
8181
}
8282

83+
public function finishEmbeddedComponentRender(): void
84+
{
85+
try {
86+
$this->container->get(ComponentRenderer::class)->finishEmbeddedComponentRender();
87+
} catch (\Throwable $e) {
88+
$this->throwRuntimeError($name, $e);
89+
}
90+
}
91+
8392
private function throwRuntimeError(string $name, \Throwable $e): void
8493
{
8594
if (!($e instanceof \Exception)) {

src/TwigComponent/src/Twig/ComponentNode.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ public function compile(Compiler $compiler): void
8585
$compiler->raw('->display($embeddedContext, $embeddedBlocks);');
8686
$compiler->raw("\n");
8787

88+
$compiler->write('$this->extensions[')
89+
->string(ComponentExtension::class)
90+
->raw(']->finishEmbeddedComponentRender()')
91+
->raw(";\n")
92+
;
93+
8894
$compiler
8995
->outdent()
9096
->write('}')

0 commit comments

Comments
 (0)