diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 186c663..77643a0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -125,7 +125,7 @@ copy.
Then run the tests with:
```shell
-./scripts/test-app.sh
+./scripts/test-sandbox.sh
```
#### Updating template stories
diff --git a/sandbox/templates/components/LiveComponent.stories.js b/sandbox/templates/components/LiveComponent.stories.js
index 71a7816..d743524 100644
--- a/sandbox/templates/components/LiveComponent.stories.js
+++ b/sandbox/templates/components/LiveComponent.stories.js
@@ -1,11 +1,9 @@
import LiveComponent from './LiveComponent.html.twig';
import { userEvent, expect, fn, waitFor, within } from '@storybook/test';
+import {twig} from "@sensiolabs/storybook-symfony-webpack5";
export default {
- component: LiveComponent
-}
-
-export const Default = {
+ component: LiveComponent,
args: {
onClick: fn()
},
@@ -22,3 +20,15 @@ export const Default = {
await waitFor(() => expect(args.onClick).toHaveBeenCalledOnce());
}
}
+
+export const Default = {
+}
+
+export const EmbeddedRender = {
+ render: () => ({
+ components: {LiveComponent},
+ template: twig`
+
+ `
+ }),
+}
diff --git a/src/Controller/StorybookController.php b/src/Controller/StorybookController.php
index 37ae987..cbcdabc 100644
--- a/src/Controller/StorybookController.php
+++ b/src/Controller/StorybookController.php
@@ -23,17 +23,22 @@ public function __construct(
public function __invoke(Request $request, string $story): Response
{
- $request = RequestAttributesHelper::withStorybookAttributes($request, ['story' => $story]);
-
$templateString = $request->getPayload()->get('template');
if (null === $templateString) {
throw new BadRequestHttpException('Missing "template" in request body.');
}
+ $templateName = \sprintf('%s.html.twig', hash('xxh128', $templateString));
+
+ $request = RequestAttributesHelper::withStorybookAttributes($request, [
+ 'story' => $story,
+ 'template' => $templateName,
+ ]);
+
$args = $this->argsProcessor->process($request);
- $storyObj = new Story($story, $templateString, $args);
+ $storyObj = new Story($story, $templateName, $templateString, $args);
$content = $this->storyRenderer->render($storyObj);
diff --git a/src/EventListener/ProxyRequestListener.php b/src/EventListener/ProxyRequestListener.php
index 2615ee9..0ba554a 100644
--- a/src/EventListener/ProxyRequestListener.php
+++ b/src/EventListener/ProxyRequestListener.php
@@ -31,6 +31,10 @@ public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
+ if (!$event->isMainRequest() || 'storybook_render' === $request->attributes->get('_route')) {
+ return;
+ }
+
if (!RequestAttributesHelper::isProxyRequest($request) || !$request->headers->has('referer')) {
return;
}
diff --git a/src/Story.php b/src/Story.php
index 593961b..000dbc8 100644
--- a/src/Story.php
+++ b/src/Story.php
@@ -6,6 +6,7 @@ final class Story
{
public function __construct(
private readonly string $id,
+ private readonly string $templateName,
private readonly string $template,
private readonly Args $args,
) {
@@ -16,6 +17,11 @@ public function getId(): string
return $this->id;
}
+ public function getTemplateName(): string
+ {
+ return $this->templateName;
+ }
+
public function getTemplate(): string
{
return $this->template;
diff --git a/src/Twig/TwigComponentSubscriber.php b/src/Twig/TwigComponentSubscriber.php
index 28d6a11..6425659 100644
--- a/src/Twig/TwigComponentSubscriber.php
+++ b/src/Twig/TwigComponentSubscriber.php
@@ -8,6 +8,7 @@
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
+use Symfony\UX\TwigComponent\MountedComponent;
/**
* @author Nicolas Rigaud
@@ -25,10 +26,42 @@ public function __construct(
public static function getSubscribedEvents(): array
{
return [
- PreRenderEvent::class => 'onPreRender',
+ PreRenderEvent::class => [
+ ['inlineRootLiveComponent', 1],
+ ['onPreRender', 0],
+ ],
];
}
+ public function inlineRootLiveComponent(PreRenderEvent $event): void
+ {
+ $request = $this->requestStack->getMainRequest();
+
+ if (null === $request || !RequestAttributesHelper::isStorybookRequest($request)) {
+ return;
+ }
+
+ if (!$event->getMetadata()->get('live', false)) {
+ // not a live component, skip
+ return;
+ }
+
+ $storybookAttributes = RequestAttributesHelper::getStorybookAttributes($request);
+
+ $mounted = $event->getMountedComponent();
+
+ if ($mounted->hasExtraMetadata('hostTemplate') && $mounted->getExtraMetadata('hostTemplate') === $storybookAttributes->template) {
+ // Dirty hack here: we are rendering a Live Component in the main story template with the embedded strategy.
+ // The host template actually doesn't exist, which will cause errors because Live Component will try to use
+ // it when re-rendering itself. As this is only useful for blocks resolution, we can safely remove this.
+ // Using reflection because no extension point is available here.
+ $refl = new \ReflectionProperty(MountedComponent::class, 'extraMetadata');
+ $extraMetadata = $refl->getValue($mounted);
+ unset($extraMetadata['hostTemplate'], $extraMetadata['embeddedTemplateIndex']);
+ $refl->setValue($mounted, $extraMetadata);
+ }
+ }
+
public function onPreRender(PreRenderEvent $event): void
{
$request = $this->requestStack->getMainRequest();
diff --git a/src/Util/StorybookAttributes.php b/src/Util/StorybookAttributes.php
index aa4f1f0..4ae9bc6 100644
--- a/src/Util/StorybookAttributes.php
+++ b/src/Util/StorybookAttributes.php
@@ -4,8 +4,13 @@
final class StorybookAttributes
{
+ private const REQUIRED_ATTRIBUTES = [
+ 'story',
+ ];
+
public function __construct(
public readonly string $story,
+ public readonly ?string $template = null,
) {
}
@@ -14,10 +19,15 @@ public function __construct(
*/
public static function from(array $attributes): self
{
- if (!isset($attributes['story'])) {
- throw new \InvalidArgumentException('Missing key "story" in attributes.');
+ foreach (self::REQUIRED_ATTRIBUTES as $attribute) {
+ if (!isset($attributes[$attribute])) {
+ throw new \InvalidArgumentException(\sprintf('Missing key "%s" in attributes.', $attribute));
+ }
}
- return new self(story: $attributes['story']);
+ return new self(
+ story: $attributes['story'],
+ template: $attributes['template'] ?? null,
+ );
}
}
diff --git a/tests/Integration/StoryRendererTest.php b/tests/Integration/StoryRendererTest.php
index 35ba71d..9eab3ef 100644
--- a/tests/Integration/StoryRendererTest.php
+++ b/tests/Integration/StoryRendererTest.php
@@ -17,7 +17,7 @@ public function testRenderStoryWithRestrictedContentThrowsException(string $temp
self::bootKernel();
$renderer = static::getContainer()->get('storybook.story_renderer');
- $story = new Story('story', $template, new Args());
+ $story = new Story('story', 'story.html.twig', $template, new Args());
$this->expectException(UnauthorizedStoryException::class);
@@ -80,7 +80,7 @@ public function testRenderStoryWithAllowedContent(string $template, array $args
self::bootKernel();
$renderer = static::getContainer()->get('storybook.story_renderer');
- $story = new Story('story', $template, new Args($args));
+ $story = new Story('story', 'story.html.twig', $template, new Args($args));
$content = $renderer->render($story);
@@ -169,7 +169,7 @@ public function testComponentUsingTrait()
$renderer = static::getContainer()->get('storybook.story_renderer');
- $story = new Story('story', '', new Args());
+ $story = new Story('story', 'story.html.twig', '', new Args());
$content = $renderer->render($story);
@@ -187,8 +187,8 @@ public function testPassingPropsFromContextVariableWithSameName()
$renderer = static::getContainer()->get('storybook.story_renderer');
- $storyWithFunction = new Story('story', '', new Args(['prop1' => 'foo']));
- $storyWithTag = new Story('story', '', new Args(['prop1' => 'foo']));
+ $storyWithFunction = new Story('story', 'story.html.twig', '', new Args(['prop1' => 'foo']));
+ $storyWithTag = new Story('story', 'story.html.twig', '', new Args(['prop1' => 'foo']));
$this->assertEquals($renderer->render($storyWithFunction), $renderer->render($storyWithTag));
}
@@ -199,7 +199,7 @@ public function testComponentAttributeRendering()
$renderer = static::getContainer()->get('storybook.story_renderer');
- $story = new Story('story', '', new Args());
+ $story = new Story('story', 'story.html.twig', '', new Args());
$this->assertStringContainsString('foo="bar"', $renderer->render($story));
}
diff --git a/tests/Unit/StoryRendererTest.php b/tests/Unit/StoryRendererTest.php
index 32d459e..bc11db7 100644
--- a/tests/Unit/StoryRendererTest.php
+++ b/tests/Unit/StoryRendererTest.php
@@ -29,6 +29,7 @@ public function testRender()
$story = new Story(
'story',
+ 'story.html.twig',
'',
new Args()
);
@@ -51,6 +52,7 @@ public function testExceptions(Error $twigError, string $expectedException)
$story = new Story(
'story',
+ 'story.html.twig',
'',
new Args()
);
diff --git a/tests/Unit/Util/StorybookAttributesTest.php b/tests/Unit/Util/StorybookAttributesTest.php
index 89c4df8..3f60f6f 100644
--- a/tests/Unit/Util/StorybookAttributesTest.php
+++ b/tests/Unit/Util/StorybookAttributesTest.php
@@ -7,12 +7,38 @@
class StorybookAttributesTest extends TestCase
{
- public function testCreateFromArray()
+ /**
+ * @dataProvider getValidArguments
+ */
+ public function testCreateFromArray(array $array, StorybookAttributes $expected)
{
- $attributes = StorybookAttributes::from(['story' => 'story']);
+ $attributes = StorybookAttributes::from($array);
- $this->assertInstanceOf(StorybookAttributes::class, $attributes);
- $this->assertEquals('story', $attributes->story);
+ $this->assertEquals($expected, $attributes);
+ }
+
+ public static function getValidArguments(): iterable
+ {
+ yield 'only story' => [
+ 'array' => [
+ 'story' => 'story',
+ ],
+ 'expected' => new StorybookAttributes(
+ 'story',
+ null
+ ),
+ ];
+
+ yield 'with template name' => [
+ 'array' => [
+ 'story' => 'story',
+ 'template' => 'story.html.twig',
+ ],
+ 'expected' => new StorybookAttributes(
+ 'story',
+ 'story.html.twig'
+ ),
+ ];
}
public function testCreateFromInvalidArrayThrowsException()