Skip to content

Commit 5df34f2

Browse files
committed
Renaming class that was in a bad directory & new event
initial attempt at conditional child rendering upgrading browser
1 parent 4909915 commit 5df34f2

File tree

14 files changed

+255
-18
lines changed

14 files changed

+255
-18
lines changed

src/LiveComponent/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"symfony/security-csrf": "^5.4|^6.0",
4343
"symfony/twig-bundle": "^5.4|^6.0",
4444
"symfony/validator": "^5.4|^6.0",
45-
"zenstruck/browser": "^0.9.1",
45+
"zenstruck/browser": "^1.2.0",
4646
"zenstruck/foundry": "^1.10"
4747
},
4848
"conflict": {

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
use Symfony\UX\LiveComponent\ComponentValidatorInterface;
2323
use Symfony\UX\LiveComponent\Controller\BatchActionController;
2424
use Symfony\UX\LiveComponent\EventListener\AddLiveAttributesSubscriber;
25+
use Symfony\UX\LiveComponent\EventListener\InterceptChildComponentRenderSubscriber;
2526
use Symfony\UX\LiveComponent\EventListener\LiveComponentSubscriber;
2627
use Symfony\UX\LiveComponent\Form\Type\LiveCollectionType;
2728
use Symfony\UX\LiveComponent\LiveComponentHydrator;
2829
use Symfony\UX\LiveComponent\Twig\DeterministicTwigIdCalculator;
2930
use Symfony\UX\LiveComponent\Twig\LiveComponentExtension as LiveComponentTwigExtension;
3031
use Symfony\UX\LiveComponent\Twig\LiveComponentRuntime;
3132
use Symfony\UX\LiveComponent\Util\FingerprintCalculator;
33+
use Symfony\UX\LiveComponent\Util\TwigAttributeHelper;
3234
use Symfony\UX\TwigComponent\ComponentFactory;
3335
use Symfony\UX\TwigComponent\ComponentRenderer;
3436
use Symfony\UX\TwigComponent\ComponentStack;
@@ -89,6 +91,18 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
8991
->addTag('container.service_subscriber') // csrf
9092
;
9193

94+
$container->register('ux.live_component.intercept_child_component_render_subscriber', InterceptChildComponentRenderSubscriber::class)
95+
->setArguments([
96+
new Reference('ux.twig_component.component_stack'),
97+
new Reference('ux.live_component.deterministic_id_calculator'),
98+
new Reference('ux.live_component.fingerprint_calculator'),
99+
new Reference('ux.live_component.attribute_helper'),
100+
new Reference('ux.twig_component.component_factory'),
101+
new Reference('ux.live_component.component_hydrator'),
102+
])
103+
->addTag('kernel.event_subscriber');
104+
;
105+
92106
$container->register('ux.live_component.twig.component_extension', LiveComponentTwigExtension::class)
93107
->addTag('twig.extension')
94108
;
@@ -106,13 +120,17 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
106120
->addTag('container.service_subscriber', ['key' => 'validator', 'id' => 'validator'])
107121
;
108122

123+
$container->register('ux.live_component.attribute_helper', TwigAttributeHelper::class)
124+
->setArguments([new Reference('twig')]);
125+
109126
$container->register('ux.live_component.add_attributes_subscriber', AddLiveAttributesSubscriber::class)
110127
->addTag('kernel.event_subscriber')
111128
->addTag('container.service_subscriber', ['key' => LiveComponentHydrator::class, 'id' => 'ux.live_component.component_hydrator'])
112129
->addTag('container.service_subscriber', ['key' => ComponentStack::class, 'id' => 'ux.twig_component.component_stack'])
130+
->addTag('container.service_subscriber', ['key' => TwigAttributeHelper::class, 'id' => 'ux.live_component.attribute_helper'])
113131
->addTag('container.service_subscriber', ['key' => DeterministicTwigIdCalculator::class, 'id' => 'ux.live_component.deterministic_id_calculator'])
114132
->addTag('container.service_subscriber', ['key' => FingerprintCalculator::class, 'id' => 'ux.live_component.fingerprint_calculator'])
115-
->addTag('container.service_subscriber') // csrf, twig & router
133+
->addTag('container.service_subscriber') // csrf & router
116134
;
117135

118136
$container->register('ux.live_component.deterministic_id_calculator', DeterministicTwigIdCalculator::class);

src/LiveComponent/src/EventListener/AddLiveAttributesSubscriber.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
1919
use Symfony\UX\LiveComponent\DehydratedComponent;
2020
use Symfony\UX\LiveComponent\LiveComponentHydrator;
2121
use Symfony\UX\LiveComponent\Twig\DeterministicTwigIdCalculator;
22+
use Symfony\UX\LiveComponent\Util\TwigAttributeHelper;
2223
use Symfony\UX\LiveComponent\Util\FingerprintCalculator;
2324
use Symfony\UX\TwigComponent\ComponentAttributes;
2425
use Symfony\UX\TwigComponent\ComponentMetadata;
2526
use Symfony\UX\TwigComponent\ComponentStack;
26-
use Symfony\UX\TwigComponent\EventListener\PreRenderEvent;
27+
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
2728
use Symfony\UX\TwigComponent\MountedComponent;
2829
use Twig\Environment;
2930

@@ -80,7 +81,7 @@ public static function getSubscribedServices(): array
8081
return [
8182
LiveComponentHydrator::class,
8283
UrlGeneratorInterface::class,
83-
Environment::class,
84+
TwigAttributeHelper::class,
8485
ComponentStack::class,
8586
DeterministicTwigIdCalculator::class,
8687
FingerprintCalculator::class,
@@ -94,13 +95,14 @@ private function getLiveAttributes(MountedComponent $mounted, ComponentMetadata
9495
$url = $this->container->get(UrlGeneratorInterface::class)->generate('live_component', ['component' => $name]);
9596
/** @var DehydratedComponent $dehydratedComponent */
9697
$dehydratedComponent = $this->container->get(LiveComponentHydrator::class)->dehydrate($mounted);
97-
$twig = $this->container->get(Environment::class);
98+
/** @var TwigAttributeHelper $helper */
99+
$helper = $this->container->get(TwigAttributeHelper::class);
98100

99101
$attributes = [
100102
'data-controller' => 'live',
101-
'data-live-url-value' => twig_escape_filter($twig, $url, 'html_attr'),
102-
'data-live-data-value' => twig_escape_filter($twig, json_encode($dehydratedComponent->getData(), \JSON_THROW_ON_ERROR), 'html_attr'),
103-
'data-live-props-value' => twig_escape_filter($twig, json_encode($dehydratedComponent->getProps(), \JSON_THROW_ON_ERROR), 'html_attr'),
103+
'data-live-url-value' => $helper->escapeAttribute($url),
104+
'data-live-data-value' => $helper->escapeAttribute(json_encode($dehydratedComponent->getData(), \JSON_THROW_ON_ERROR)),
105+
'data-live-props-value' => $helper->escapeAttribute(json_encode($dehydratedComponent->getProps(), \JSON_THROW_ON_ERROR)),
104106
];
105107

106108
if ($this->container->has(CsrfTokenManagerInterface::class) && $metadata->get('csrf')) {
@@ -111,9 +113,10 @@ private function getLiveAttributes(MountedComponent $mounted, ComponentMetadata
111113

112114
if ($this->container->get(ComponentStack::class)->hasParentComponent()) {
113115
$id = $this->container->get(DeterministicTwigIdCalculator::class)->calculateDeterministicId();
116+
$attributes['data-live-id'] = $helper->escapeAttribute($id);
114117

115-
$attributes['data-live-id'] = $id;
116-
$attributes['data-live-value-fingerprint'] = $this->container->get(FingerprintCalculator::class)->calculateFingerprint($mounted->getInputProps());
118+
$fingerprint = $this->container->get(FingerprintCalculator::class)->calculateFingerprint($mounted->getInputProps());
119+
$attributes['data-live-value-fingerprint'] = $helper->escapeAttribute($fingerprint);
117120
}
118121

119122
return new ComponentAttributes($attributes);
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\UX\LiveComponent\LiveComponentHydrator;
16+
use Symfony\UX\LiveComponent\Twig\DeterministicTwigIdCalculator;
17+
use Symfony\UX\LiveComponent\Util\FingerprintCalculator;
18+
use Symfony\UX\LiveComponent\Util\TwigAttributeHelper;
19+
use Symfony\UX\TwigComponent\ComponentFactory;
20+
use Symfony\UX\TwigComponent\ComponentStack;
21+
use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;
22+
23+
/**
24+
* Responsible for rendering children as empty elements during a re-render.
25+
*
26+
* @author Ryan Weaver <[email protected]>
27+
*
28+
* @experimental
29+
* @internal
30+
*/
31+
class InterceptChildComponentRenderSubscriber implements EventSubscriberInterface
32+
{
33+
public const CHILDREN_FINGERPRINTS_METADATA_KEY = 'children_fingerprints';
34+
35+
public function __construct(
36+
private ComponentStack $componentStack,
37+
private DeterministicTwigIdCalculator $deterministicTwigIdCalculator,
38+
private FingerprintCalculator $fingerprintCalculator,
39+
private TwigAttributeHelper $twigAttributeHelper,
40+
private ComponentFactory $componentFactory,
41+
private LiveComponentHydrator $liveComponentHydrator,
42+
)
43+
{
44+
}
45+
46+
public function preComponentCreated(PreCreateForRenderEvent $event): void
47+
{
48+
if (!$this->componentStack->hasParentComponent()) {
49+
return;
50+
}
51+
52+
$parentComponent = $this->componentStack->getParentComponent();
53+
if (!$parentComponent->hasExtraMetadata(self::CHILDREN_FINGERPRINTS_METADATA_KEY)) {
54+
return;
55+
}
56+
57+
$childFingerprints = $parentComponent->getExtraMetadata(self::CHILDREN_FINGERPRINTS_METADATA_KEY);
58+
59+
// get the deterministic id for this child, but without incrementing the counter yet
60+
$deterministicId = $this->deterministicTwigIdCalculator->calculateDeterministicId(increment: false);
61+
if (!isset($childFingerprints[$deterministicId])) {
62+
// child fingerprint wasn't set, it is likely a new child, allow it to render fully
63+
return;
64+
}
65+
66+
// increment the internal counter now to keep "counter" consistency if we're
67+
// in a loop of children being rendered on the same line
68+
// we need to do this because this component will *not* ever hit
69+
// AddLiveAttributesSubscriber where the counter is normally incrememented
70+
$this->deterministicTwigIdCalculator->calculateDeterministicId(increment: true);
71+
72+
$newPropsFingerprint = $this->fingerprintCalculator->calculateFingerprint($event->getProps());
73+
74+
if ($childFingerprints[$deterministicId] === $newPropsFingerprint) {
75+
// the props passed to create this child have *not* changed
76+
// return an empty element so the frontend knows to keep the current child
77+
78+
$rendered = sprintf(
79+
'<div data-live-id="%s"></div>',
80+
$this->twigAttributeHelper->escapeAttribute($deterministicId)
81+
);
82+
$event->setRenderedString($rendered);
83+
84+
return;
85+
}
86+
87+
/*
88+
* The props passed to create this child HAVE changed.
89+
* Send back a fake element with:
90+
* * data-live-id
91+
* * data-live-fingerprint-value (new fingerprint)
92+
* * data-live-props-value (new dehydrated props)
93+
*/
94+
$mounted = $this->componentFactory->create($event->getName(), $event->getProps());
95+
$dehydratedComponent = $this->liveComponentHydrator->dehydrate($mounted);
96+
97+
$rendered = sprintf(
98+
'<div data-live-id="%s" data-live-fingerprint-value="%s" data-live-props-value="%s"></div>',
99+
$this->twigAttributeHelper->escapeAttribute($deterministicId),
100+
$this->twigAttributeHelper->escapeAttribute($newPropsFingerprint),
101+
$this->twigAttributeHelper->escapeAttribute(json_encode($dehydratedComponent->getProps(), \JSON_THROW_ON_ERROR))
102+
);
103+
$event->setRenderedString($rendered);
104+
}
105+
106+
public static function getSubscribedEvents(): array
107+
{
108+
return [
109+
PreCreateForRenderEvent::class => 'preComponentCreated'
110+
];
111+
}
112+
}

src/LiveComponent/src/EventListener/LiveComponentSubscriber.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,10 @@ private function hydrateComponent(object $component, string $componentName, Requ
326326
$componentName
327327
);
328328

329-
$mountedComponent->addExtraMetadata('childrenFingerprints', $this->parseDataFor($request)['childrenFingerprints']);
329+
$mountedComponent->addExtraMetadata(
330+
InterceptChildComponentRenderSubscriber::CHILDREN_FINGERPRINTS_METADATA_KEY,
331+
$this->parseDataFor($request)['childrenFingerprints']
332+
);
330333

331334
return $mountedComponent;
332335
}

src/LiveComponent/src/Twig/DeterministicTwigIdCalculator.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class DeterministicTwigIdCalculator
3737
* get the same value back if you call it 3 times on a future request for
3838
* that same file & line.
3939
*/
40-
public function calculateDeterministicId(): string
40+
public function calculateDeterministicId(bool $increment = true): string
4141
{
4242
$error = new Error('');
4343
$error->guess();
@@ -56,7 +56,10 @@ public function calculateDeterministicId(): string
5656
crc32($fileAndLine),
5757
$this->lineAndFileCounts[$fileAndLine]
5858
);
59-
++$this->lineAndFileCounts[$fileAndLine];
59+
60+
if ($increment) {
61+
++$this->lineAndFileCounts[$fileAndLine];
62+
}
6063

6164
return $id;
6265
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Util;
13+
14+
use Twig\Environment;
15+
16+
/**
17+
* @experimental
18+
*
19+
* @internal
20+
*/
21+
final class TwigAttributeHelper
22+
{
23+
public function __construct(private Environment $twig)
24+
{
25+
}
26+
27+
public function escapeAttribute(string $value): string
28+
{
29+
return twig_escape_filter($this->twig, $value, 'html_attr');
30+
}
31+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace Symfony\UX\LiveComponent\Tests\Integration\EventListener;
44

55
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
6-
use Symfony\UX\TwigComponent\EventListener\PreRenderEvent;
6+
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
77
use Twig\Environment;
88
use Twig\Error\RuntimeError;
99

src/TwigComponent/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 2.5
44

5+
- The `PreRenderEvent` namespace was changed from `Symfony\UX\TwigComponent\EventListener`
6+
to `Symfony\UX\TwigComponent\Event`.
7+
58
- Add new autowireable `ComponentRendererInterface`
69

710
## 2.2

src/TwigComponent/src/ComponentRenderer.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
1515
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
1616
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
17-
use Symfony\UX\TwigComponent\EventListener\PreRenderEvent;
17+
use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;
18+
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
1819
use Twig\Environment;
1920
use Twig\Extension\EscaperExtension;
2021

@@ -40,6 +41,14 @@ public function __construct(
4041

4142
public function createAndRender(string $name, array $props = []): string
4243
{
44+
$event = new PreCreateForRenderEvent($name, $props);
45+
$this->dispatcher->dispatch($event);
46+
47+
// allow the process to be short-circuited
48+
if (null !== $rendered = $event->getRenderedString()) {
49+
return $rendered;
50+
}
51+
4352
return $this->render($this->factory->create($name, $props));
4453
}
4554

0 commit comments

Comments
 (0)