Skip to content

Commit a4a2385

Browse files
authored
Fix embedded Live Component rendering (#40)
1 parent 117c5a4 commit a4a2385

File tree

10 files changed

+118
-22
lines changed

10 files changed

+118
-22
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ copy.
125125
Then run the tests with:
126126

127127
```shell
128-
./scripts/test-app.sh
128+
./scripts/test-sandbox.sh
129129
```
130130

131131
#### Updating template stories

sandbox/templates/components/LiveComponent.stories.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import LiveComponent from './LiveComponent.html.twig';
22
import { userEvent, expect, fn, waitFor, within } from '@storybook/test';
3+
import {twig} from "@sensiolabs/storybook-symfony-webpack5";
34

45
export default {
5-
component: LiveComponent
6-
}
7-
8-
export const Default = {
6+
component: LiveComponent,
97
args: {
108
onClick: fn()
119
},
@@ -22,3 +20,15 @@ export const Default = {
2220
await waitFor(() => expect(args.onClick).toHaveBeenCalledOnce());
2321
}
2422
}
23+
24+
export const Default = {
25+
}
26+
27+
export const EmbeddedRender = {
28+
render: () => ({
29+
components: {LiveComponent},
30+
template: twig`
31+
<twig:LiveComponent data-storybook-callbacks="{{ onClick }}">
32+
</twig:LiveComponent>`
33+
}),
34+
}

src/Controller/StorybookController.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,22 @@ public function __construct(
2323

2424
public function __invoke(Request $request, string $story): Response
2525
{
26-
$request = RequestAttributesHelper::withStorybookAttributes($request, ['story' => $story]);
27-
2826
$templateString = $request->getPayload()->get('template');
2927

3028
if (null === $templateString) {
3129
throw new BadRequestHttpException('Missing "template" in request body.');
3230
}
3331

32+
$templateName = \sprintf('%s.html.twig', hash('xxh128', $templateString));
33+
34+
$request = RequestAttributesHelper::withStorybookAttributes($request, [
35+
'story' => $story,
36+
'template' => $templateName,
37+
]);
38+
3439
$args = $this->argsProcessor->process($request);
3540

36-
$storyObj = new Story($story, $templateString, $args);
41+
$storyObj = new Story($story, $templateName, $templateString, $args);
3742

3843
$content = $this->storyRenderer->render($storyObj);
3944

src/EventListener/ProxyRequestListener.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ public function onKernelRequest(RequestEvent $event): void
3131
{
3232
$request = $event->getRequest();
3333

34+
if (!$event->isMainRequest() || 'storybook_render' === $request->attributes->get('_route')) {
35+
return;
36+
}
37+
3438
if (!RequestAttributesHelper::isProxyRequest($request) || !$request->headers->has('referer')) {
3539
return;
3640
}

src/Story.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ final class Story
66
{
77
public function __construct(
88
private readonly string $id,
9+
private readonly string $templateName,
910
private readonly string $template,
1011
private readonly Args $args,
1112
) {
@@ -16,6 +17,11 @@ public function getId(): string
1617
return $this->id;
1718
}
1819

20+
public function getTemplateName(): string
21+
{
22+
return $this->templateName;
23+
}
24+
1925
public function getTemplate(): string
2026
{
2127
return $this->template;

src/Twig/TwigComponentSubscriber.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
99
use Symfony\Component\HttpFoundation\RequestStack;
1010
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
11+
use Symfony\UX\TwigComponent\MountedComponent;
1112

1213
/**
1314
* @author Nicolas Rigaud <squrious@protonmail.com>
@@ -25,10 +26,42 @@ public function __construct(
2526
public static function getSubscribedEvents(): array
2627
{
2728
return [
28-
PreRenderEvent::class => 'onPreRender',
29+
PreRenderEvent::class => [
30+
['inlineRootLiveComponent', 1],
31+
['onPreRender', 0],
32+
],
2933
];
3034
}
3135

36+
public function inlineRootLiveComponent(PreRenderEvent $event): void
37+
{
38+
$request = $this->requestStack->getMainRequest();
39+
40+
if (null === $request || !RequestAttributesHelper::isStorybookRequest($request)) {
41+
return;
42+
}
43+
44+
if (!$event->getMetadata()->get('live', false)) {
45+
// not a live component, skip
46+
return;
47+
}
48+
49+
$storybookAttributes = RequestAttributesHelper::getStorybookAttributes($request);
50+
51+
$mounted = $event->getMountedComponent();
52+
53+
if ($mounted->hasExtraMetadata('hostTemplate') && $mounted->getExtraMetadata('hostTemplate') === $storybookAttributes->template) {
54+
// Dirty hack here: we are rendering a Live Component in the main story template with the embedded strategy.
55+
// The host template actually doesn't exist, which will cause errors because Live Component will try to use
56+
// it when re-rendering itself. As this is only useful for blocks resolution, we can safely remove this.
57+
// Using reflection because no extension point is available here.
58+
$refl = new \ReflectionProperty(MountedComponent::class, 'extraMetadata');
59+
$extraMetadata = $refl->getValue($mounted);
60+
unset($extraMetadata['hostTemplate'], $extraMetadata['embeddedTemplateIndex']);
61+
$refl->setValue($mounted, $extraMetadata);
62+
}
63+
}
64+
3265
public function onPreRender(PreRenderEvent $event): void
3366
{
3467
$request = $this->requestStack->getMainRequest();

src/Util/StorybookAttributes.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44

55
final class StorybookAttributes
66
{
7+
private const REQUIRED_ATTRIBUTES = [
8+
'story',
9+
];
10+
711
public function __construct(
812
public readonly string $story,
13+
public readonly ?string $template = null,
914
) {
1015
}
1116

@@ -14,10 +19,15 @@ public function __construct(
1419
*/
1520
public static function from(array $attributes): self
1621
{
17-
if (!isset($attributes['story'])) {
18-
throw new \InvalidArgumentException('Missing key "story" in attributes.');
22+
foreach (self::REQUIRED_ATTRIBUTES as $attribute) {
23+
if (!isset($attributes[$attribute])) {
24+
throw new \InvalidArgumentException(\sprintf('Missing key "%s" in attributes.', $attribute));
25+
}
1926
}
2027

21-
return new self(story: $attributes['story']);
28+
return new self(
29+
story: $attributes['story'],
30+
template: $attributes['template'] ?? null,
31+
);
2232
}
2333
}

tests/Integration/StoryRendererTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function testRenderStoryWithRestrictedContentThrowsException(string $temp
1717
self::bootKernel();
1818

1919
$renderer = static::getContainer()->get('storybook.story_renderer');
20-
$story = new Story('story', $template, new Args());
20+
$story = new Story('story', 'story.html.twig', $template, new Args());
2121

2222
$this->expectException(UnauthorizedStoryException::class);
2323

@@ -80,7 +80,7 @@ public function testRenderStoryWithAllowedContent(string $template, array $args
8080
self::bootKernel();
8181

8282
$renderer = static::getContainer()->get('storybook.story_renderer');
83-
$story = new Story('story', $template, new Args($args));
83+
$story = new Story('story', 'story.html.twig', $template, new Args($args));
8484

8585
$content = $renderer->render($story);
8686

@@ -169,7 +169,7 @@ public function testComponentUsingTrait()
169169

170170
$renderer = static::getContainer()->get('storybook.story_renderer');
171171

172-
$story = new Story('story', '<twig:ComponentUsingTrait />', new Args());
172+
$story = new Story('story', 'story.html.twig', '<twig:ComponentUsingTrait />', new Args());
173173

174174
$content = $renderer->render($story);
175175

@@ -187,8 +187,8 @@ public function testPassingPropsFromContextVariableWithSameName()
187187

188188
$renderer = static::getContainer()->get('storybook.story_renderer');
189189

190-
$storyWithFunction = new Story('story', '<twig:AnonymousComponent :prop1="prop1"/>', new Args(['prop1' => 'foo']));
191-
$storyWithTag = new Story('story', '<twig:AnonymousComponent :prop1="prop1"></twig:AnonymousComponent>', new Args(['prop1' => 'foo']));
190+
$storyWithFunction = new Story('story', 'story.html.twig', '<twig:AnonymousComponent :prop1="prop1"/>', new Args(['prop1' => 'foo']));
191+
$storyWithTag = new Story('story', 'story.html.twig', '<twig:AnonymousComponent :prop1="prop1"></twig:AnonymousComponent>', new Args(['prop1' => 'foo']));
192192

193193
$this->assertEquals($renderer->render($storyWithFunction), $renderer->render($storyWithTag));
194194
}
@@ -199,7 +199,7 @@ public function testComponentAttributeRendering()
199199

200200
$renderer = static::getContainer()->get('storybook.story_renderer');
201201

202-
$story = new Story('story', '<twig:AnonymousComponent foo="bar"></twig:AnonymousComponent>', new Args());
202+
$story = new Story('story', 'story.html.twig', '<twig:AnonymousComponent foo="bar"></twig:AnonymousComponent>', new Args());
203203

204204
$this->assertStringContainsString('foo="bar"', $renderer->render($story));
205205
}

tests/Unit/StoryRendererTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function testRender()
2929

3030
$story = new Story(
3131
'story',
32+
'story.html.twig',
3233
'',
3334
new Args()
3435
);
@@ -51,6 +52,7 @@ public function testExceptions(Error $twigError, string $expectedException)
5152

5253
$story = new Story(
5354
'story',
55+
'story.html.twig',
5456
'',
5557
new Args()
5658
);

tests/Unit/Util/StorybookAttributesTest.php

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,38 @@
77

88
class StorybookAttributesTest extends TestCase
99
{
10-
public function testCreateFromArray()
10+
/**
11+
* @dataProvider getValidArguments
12+
*/
13+
public function testCreateFromArray(array $array, StorybookAttributes $expected)
1114
{
12-
$attributes = StorybookAttributes::from(['story' => 'story']);
15+
$attributes = StorybookAttributes::from($array);
1316

14-
$this->assertInstanceOf(StorybookAttributes::class, $attributes);
15-
$this->assertEquals('story', $attributes->story);
17+
$this->assertEquals($expected, $attributes);
18+
}
19+
20+
public static function getValidArguments(): iterable
21+
{
22+
yield 'only story' => [
23+
'array' => [
24+
'story' => 'story',
25+
],
26+
'expected' => new StorybookAttributes(
27+
'story',
28+
null
29+
),
30+
];
31+
32+
yield 'with template name' => [
33+
'array' => [
34+
'story' => 'story',
35+
'template' => 'story.html.twig',
36+
],
37+
'expected' => new StorybookAttributes(
38+
'story',
39+
'story.html.twig'
40+
),
41+
];
1642
}
1743

1844
public function testCreateFromInvalidArrayThrowsException()

0 commit comments

Comments
 (0)