Skip to content

Commit b9ba327

Browse files
committed
remove need for init_live_component, just {{ $attributes }}
This works even for components without the HasAttributesTrait.
1 parent 2dfc72d commit b9ba327

File tree

9 files changed

+96
-51
lines changed

9 files changed

+96
-51
lines changed

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
2222
use Symfony\UX\LiveComponent\ComponentValidator;
2323
use Symfony\UX\LiveComponent\ComponentValidatorInterface;
24+
use Symfony\UX\LiveComponent\EventListener\AddLiveAttributesSubscriber;
2425
use Symfony\UX\LiveComponent\EventListener\LiveComponentSubscriber;
2526
use Symfony\UX\LiveComponent\LiveComponentHydrator;
2627
use Symfony\UX\LiveComponent\PropertyHydratorInterface;
@@ -81,6 +82,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
8182

8283
$container->register('ux.live_component.twig.component_runtime', LiveComponentRuntime::class)
8384
->setArguments([
85+
new Reference('twig'),
8486
new Reference('ux.live_component.component_hydrator'),
8587
new Reference('ux.twig_component.component_factory'),
8688
new Reference(UrlGeneratorInterface::class),
@@ -93,6 +95,11 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
9395
->addTag('container.service_subscriber', ['key' => 'validator', 'id' => 'validator'])
9496
;
9597

98+
$container->register('ux.live_component.add_attributes_subscriber', AddLiveAttributesSubscriber::class)
99+
->setArguments([new Reference('ux.live_component.twig.component_runtime')])
100+
->addTag('kernel.event_subscriber')
101+
;
102+
96103
$container->setAlias(ComponentValidatorInterface::class, ComponentValidator::class);
97104
}
98105
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Symfony\UX\LiveComponent\EventListener;
4+
5+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
6+
use Symfony\UX\LiveComponent\Twig\LiveComponentRuntime;
7+
use Symfony\UX\TwigComponent\ComponentAttributes;
8+
use Symfony\UX\TwigComponent\EventListener\PreRenderEvent;
9+
10+
/**
11+
* @author Kevin Bond <[email protected]>
12+
*/
13+
final class AddLiveAttributesSubscriber implements EventSubscriberInterface
14+
{
15+
public function __construct(private LiveComponentRuntime $runtime)
16+
{
17+
// todo make lazy?
18+
}
19+
20+
public function onPreRender(PreRenderEvent $event): void
21+
{
22+
if (!isset($event->config['live'])) {
23+
// not a live component, skip
24+
return;
25+
}
26+
27+
$attributes = $this->runtime->getLiveAttributes($event->component, $event->config);
28+
29+
if (isset($event->context['attributes']) && $event->context['attributes'] instanceof ComponentAttributes) {
30+
// merge with existing attributes if available
31+
$attributes = $attributes->merge($event->context['attributes']->all());
32+
}
33+
34+
$event->context['attributes'] = $attributes;
35+
}
36+
37+
public static function getSubscribedEvents(): array
38+
{
39+
return [PreRenderEvent::class => 'onPreRender'];
40+
}
41+
}

src/LiveComponent/src/Twig/LiveComponentExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class LiveComponentExtension extends AbstractExtension
2424
public function getFunctions(): array
2525
{
2626
return [
27-
new TwigFunction('init_live_component', [LiveComponentRuntime::class, 'renderLiveAttributes'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['html_attr']]),
27+
new TwigFunction('init_live_component', [LiveComponentRuntime::class, 'renderLiveAttributes'], ['needs_context' => true, 'is_safe' => ['html_attr']]),
2828
new TwigFunction('component_url', [LiveComponentRuntime::class, 'getComponentUrl']),
2929
];
3030
}

src/LiveComponent/src/Twig/LiveComponentRuntime.php

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
1515
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
1616
use Symfony\UX\LiveComponent\LiveComponentHydrator;
17+
use Symfony\UX\TwigComponent\ComponentAttributes;
1718
use Symfony\UX\TwigComponent\ComponentFactory;
1819
use Twig\Environment;
1920

@@ -24,47 +25,22 @@
2425
*/
2526
final class LiveComponentRuntime
2627
{
27-
private LiveComponentHydrator $hydrator;
28-
private ComponentFactory $factory;
29-
private UrlGeneratorInterface $urlGenerator;
30-
private ?CsrfTokenManagerInterface $csrfTokenManager;
31-
32-
public function __construct(LiveComponentHydrator $hydrator, ComponentFactory $factory, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager = null)
33-
{
34-
$this->hydrator = $hydrator;
35-
$this->factory = $factory;
36-
$this->urlGenerator = $urlGenerator;
37-
$this->csrfTokenManager = $csrfTokenManager;
28+
public function __construct(
29+
private Environment $twig,
30+
private LiveComponentHydrator $hydrator,
31+
private ComponentFactory $factory,
32+
private UrlGeneratorInterface $urlGenerator,
33+
private ?CsrfTokenManagerInterface $csrfTokenManager = null
34+
) {
3835
}
3936

40-
public function renderLiveAttributes(Environment $env, array $context): string
37+
public function renderLiveAttributes(array $context): string
4138
{
4239
if (!isset($context['_component_config'])) {
4340
throw new \LogicException('init_live_component can only be called within a component template.');
4441
}
4542

46-
if (!isset($context['_component_config']['live'])) {
47-
throw new \LogicException(sprintf('"%s" is not a Live Component.', $context['_component_config']['class']));
48-
}
49-
50-
$name = $context['_component_config']['name'];
51-
$url = $this->urlGenerator->generate('live_component', ['component' => $name]);
52-
$data = $this->hydrator->dehydrate($context['this']);
53-
54-
$ret = sprintf(
55-
'data-controller="live" data-live-url-value="%s" data-live-data-value="%s"',
56-
twig_escape_filter($env, $url, 'html_attr'),
57-
twig_escape_filter($env, json_encode($data, \JSON_THROW_ON_ERROR), 'html_attr'),
58-
);
59-
60-
if (!$this->csrfTokenManager) {
61-
return $ret;
62-
}
63-
64-
return sprintf('%s data-live-csrf-value="%s"',
65-
$ret,
66-
$this->csrfTokenManager->getToken($name)->getValue()
67-
);
43+
return $this->getLiveAttributes($context['this'], $context['_component_config']);
6844
}
6945

7046
public function getComponentUrl(string $name, array $props = []): string
@@ -74,4 +50,26 @@ public function getComponentUrl(string $name, array $props = []): string
7450

7551
return $this->urlGenerator->generate('live_component', $params);
7652
}
53+
54+
public function getLiveAttributes(object $component, array $config): ComponentAttributes
55+
{
56+
if (!isset($config['live'])) {
57+
throw new \LogicException(sprintf('"%s" is not a Live Component.', $config['class']));
58+
}
59+
60+
$url = $this->urlGenerator->generate('live_component', ['component' => $config['name']]);
61+
$data = $this->hydrator->dehydrate($component);
62+
63+
$attributes = [
64+
'data-controller' => 'live',
65+
'data-live-url-value' => twig_escape_filter($this->twig, $url, 'html_attr'),
66+
'data-live-data-value' => twig_escape_filter($this->twig, json_encode($data, \JSON_THROW_ON_ERROR), 'html_attr'),
67+
];
68+
69+
if ($this->csrfTokenManager) {
70+
$attributes['data-live-csrf-value'] = $this->csrfTokenManager->getToken($config['name'])->getValue();
71+
}
72+
73+
return new ComponentAttributes($attributes);
74+
}
7775
}
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
<div
2-
{{ init_live_component() }}
3-
>
1+
<div{{ attributes }}>
42
Count: {{ this.count }}
53
BeforeReRenderCalled: {{ this.beforeReRenderCalled ? 'Yes' : 'No' }}
64
</div>

src/TwigComponent/src/ComponentRenderer.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
1515
use Symfony\UX\TwigComponent\EventListener\PreRenderEvent;
1616
use Twig\Environment;
17+
use Twig\Extension\EscaperExtension;
1718

1819
/**
1920
* @author Kevin Bond <[email protected]>
@@ -22,12 +23,20 @@
2223
*/
2324
final class ComponentRenderer
2425
{
26+
private bool $safeClassesRegistered = false;
27+
2528
public function __construct(private Environment $twig, private EventDispatcherInterface $dispatcher)
2629
{
2730
}
2831

2932
public function render(object $component, array $config): string
3033
{
34+
if (!$this->safeClassesRegistered) {
35+
$this->twig->getExtension(EscaperExtension::class)->addSafeClass(ComponentAttributes::class, ['html']);
36+
37+
$this->safeClassesRegistered = true;
38+
}
39+
3140
$event = new PreRenderEvent(
3241
$component,
3342
$config['template'],

src/TwigComponent/src/HasAttributesTrait.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\UX\TwigComponent;
1313

1414
use Symfony\UX\LiveComponent\Attribute\LiveProp;
15+
use Symfony\UX\LiveComponent\Attribute\PostHydrate;
1516
use Symfony\UX\TwigComponent\Attribute\PostMount;
1617

1718
/**
@@ -30,7 +31,8 @@ public function setAttributes(array|ComponentAttributes $attributes): void
3031
}
3132

3233
#[PostMount(priority: -1000)]
33-
public function mountAttributes(array $data): array
34+
#[PostHydrate]
35+
public function mountAttributes(array $data = []): array
3436
{
3537
if (isset($this->attributes)) {
3638
$data = array_merge($this->attributes->all(), $data);

src/TwigComponent/src/Twig/ComponentExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class ComponentExtension extends AbstractExtension
2424
public function getFunctions(): array
2525
{
2626
return [
27-
new TwigFunction('component', [ComponentRuntime::class, 'render'], ['is_safe' => ['all'], 'needs_environment' => true]),
27+
new TwigFunction('component', [ComponentRuntime::class, 'render'], ['is_safe' => ['all']]),
2828
];
2929
}
3030
}

src/TwigComponent/src/Twig/ComponentRuntime.php

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,8 @@
1111

1212
namespace Symfony\UX\TwigComponent\Twig;
1313

14-
use Symfony\UX\TwigComponent\ComponentAttributes;
1514
use Symfony\UX\TwigComponent\ComponentFactory;
1615
use Symfony\UX\TwigComponent\ComponentRenderer;
17-
use Twig\Environment;
18-
use Twig\Extension\EscaperExtension;
1916

2017
/**
2118
* @author Kevin Bond <[email protected]>
@@ -26,22 +23,15 @@ final class ComponentRuntime
2623
{
2724
private ComponentFactory $componentFactory;
2825
private ComponentRenderer $componentRenderer;
29-
private bool $safeClassesRegistered = false;
3026

3127
public function __construct(ComponentFactory $componentFactory, ComponentRenderer $componentRenderer)
3228
{
3329
$this->componentFactory = $componentFactory;
3430
$this->componentRenderer = $componentRenderer;
3531
}
3632

37-
public function render(Environment $twig, string $name, array $props = []): string
33+
public function render(string $name, array $props = []): string
3834
{
39-
if (!$this->safeClassesRegistered) {
40-
$twig->getExtension(EscaperExtension::class)->addSafeClass(ComponentAttributes::class, ['html']);
41-
42-
$this->safeClassesRegistered = true;
43-
}
44-
4535
return $this->componentRenderer->render(
4636
$this->componentFactory->create($name, $props),
4737
$this->componentFactory->configFor($name)

0 commit comments

Comments
 (0)