Skip to content

Commit

Permalink
Fix embedded Live Component rendering (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
squrious authored Oct 26, 2024
1 parent 117c5a4 commit a4a2385
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 22 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ copy.
Then run the tests with:

```shell
./scripts/test-app.sh
./scripts/test-sandbox.sh
```

#### Updating template stories
Expand Down
18 changes: 14 additions & 4 deletions sandbox/templates/components/LiveComponent.stories.js
Original file line number Diff line number Diff line change
@@ -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()
},
Expand All @@ -22,3 +20,15 @@ export const Default = {
await waitFor(() => expect(args.onClick).toHaveBeenCalledOnce());
}
}

export const Default = {
}

export const EmbeddedRender = {
render: () => ({
components: {LiveComponent},
template: twig`
<twig:LiveComponent data-storybook-callbacks="{{ onClick }}">
</twig:LiveComponent>`
}),
}
11 changes: 8 additions & 3 deletions src/Controller/StorybookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
4 changes: 4 additions & 0 deletions src/EventListener/ProxyRequestListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 6 additions & 0 deletions src/Story.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
Expand All @@ -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;
Expand Down
35 changes: 34 additions & 1 deletion src/Twig/TwigComponentSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>
Expand All @@ -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();
Expand Down
16 changes: 13 additions & 3 deletions src/Util/StorybookAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@

final class StorybookAttributes
{
private const REQUIRED_ATTRIBUTES = [
'story',
];

public function __construct(
public readonly string $story,
public readonly ?string $template = null,
) {
}

Expand All @@ -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,
);
}
}
12 changes: 6 additions & 6 deletions tests/Integration/StoryRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -169,7 +169,7 @@ public function testComponentUsingTrait()

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

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

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

Expand All @@ -187,8 +187,8 @@ public function testPassingPropsFromContextVariableWithSameName()

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

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

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

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

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

$this->assertStringContainsString('foo="bar"', $renderer->render($story));
}
Expand Down
2 changes: 2 additions & 0 deletions tests/Unit/StoryRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function testRender()

$story = new Story(
'story',
'story.html.twig',
'',
new Args()
);
Expand All @@ -51,6 +52,7 @@ public function testExceptions(Error $twigError, string $expectedException)

$story = new Story(
'story',
'story.html.twig',
'',
new Args()
);
Expand Down
34 changes: 30 additions & 4 deletions tests/Unit/Util/StorybookAttributesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit a4a2385

Please sign in to comment.